From 679653fda82804032715f4f213d802011c0bf22e Mon Sep 17 00:00:00 2001 From: agra Date: Thu, 11 Jun 2026 12:58:17 +0300 Subject: [PATCH] lang: struct consts migrate to const globals, inline fallback (PLAN-CONST-AGG step 4) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A struct constant whose every field serializes — literals, enum tags, nested aggregates, and (new) const EXPRESSIONS over named consts / const-aggregate leaves ('r = K + 1', 'g = LIT.r', 'b = A[1]') — becomes an immutable global: one storage, reads load/GEP it, '@LIT' is addressable, dead-global elimination drops unused ones. constExprValue gained a fold-through tail (evalConstIntExpr/evalConstFloatExpr, source-aware), which also enables const-expression ELEMENTS in array consts. A const with a NON-serializable field (a call, a runtime read) keeps inline re-lowering, and that per-use evaluation is now the documented contract for the class (pinned: 'CALL.r' reads 1 then 2, side effects run per use; '#run' is the evaluate-once tool). Examples: 0180 (migrated shapes + @ptr + copy independence), 0181 (the inline-fallback contract). m3te (23/23) + game rebuilt green. --- examples/0180-types-struct-const-globals.sx | 23 ++++++++++++++ ...0181-types-struct-const-inline-fallback.sx | 18 +++++++++++ .../0180-types-struct-const-globals.exit | 1 + .../0180-types-struct-const-globals.stderr | 1 + .../0180-types-struct-const-globals.stdout | 3 ++ ...81-types-struct-const-inline-fallback.exit | 1 + ...-types-struct-const-inline-fallback.stderr | 1 + ...-types-struct-const-inline-fallback.stdout | 3 ++ src/ir/lower.zig | 1 + src/ir/lower/comptime.zig | 18 ++++++++++- src/ir/lower/decl.zig | 30 +++++++++++++++++-- 11 files changed, 97 insertions(+), 3 deletions(-) create mode 100644 examples/0180-types-struct-const-globals.sx create mode 100644 examples/0181-types-struct-const-inline-fallback.sx create mode 100644 examples/expected/0180-types-struct-const-globals.exit create mode 100644 examples/expected/0180-types-struct-const-globals.stderr create mode 100644 examples/expected/0180-types-struct-const-globals.stdout create mode 100644 examples/expected/0181-types-struct-const-inline-fallback.exit create mode 100644 examples/expected/0181-types-struct-const-inline-fallback.stderr create mode 100644 examples/expected/0181-types-struct-const-inline-fallback.stdout diff --git a/examples/0180-types-struct-const-globals.sx b/examples/0180-types-struct-const-globals.sx new file mode 100644 index 0000000..e9aae2f --- /dev/null +++ b/examples/0180-types-struct-const-globals.sx @@ -0,0 +1,23 @@ +// Serializable struct constants are IMMUTABLE GLOBALS (one storage, no +// per-use rebuild): literal fields, const-EXPRESSION fields (`K + 1`), +// another const's field (`LIT.r`), and a const array's element (`A[1]`) +// all serialize. The const is addressable (`@LIT`) and copies stay +// independent. + +#import "modules/std.sx"; + +Color :: struct { r, g, b: s64; } +K :: 10; +A : [2]s64 : .[7, 8]; +LIT :: Color.{ r = 255, g = 0, b = 0 }; +EXPR :: Color.{ r = K + 1, g = K * 2, b = A[1] }; +REF :: Color.{ r = LIT.r, g = 1, b = 2 }; + +main :: () { + print("lit={} expr={} {} {} ref={}\n", LIT.r, EXPR.r, EXPR.g, EXPR.b, REF.r); + p := @LIT; + print("via-ptr={}\n", p.r); + c := LIT; + c.r = 9; + print("copy={} const={}\n", c.r, LIT.r); +} diff --git a/examples/0181-types-struct-const-inline-fallback.sx b/examples/0181-types-struct-const-inline-fallback.sx new file mode 100644 index 0000000..f721439 --- /dev/null +++ b/examples/0181-types-struct-const-inline-fallback.sx @@ -0,0 +1,18 @@ +// A struct constant with a NON-serializable initializer field (a call, a +// runtime read) keeps INLINE RE-LOWERING semantics: the initializer is +// evaluated AT EACH USE. This is the documented contract for this class +// — `CALL.r` may differ between reads and side effects run per use. +// For evaluate-once semantics use `NAME :: #run f();`. + +#import "modules/std.sx"; + +Color :: struct { r, g, b: s64; } +counter : s64 = 0; +bump :: () -> s64 { counter += 1; counter } +CALL :: Color.{ r = bump(), g = 0, b = 0 }; + +main :: () { + print("use1={}\n", CALL.r); + print("use2={}\n", CALL.r); + print("counter={}\n", counter); +} diff --git a/examples/expected/0180-types-struct-const-globals.exit b/examples/expected/0180-types-struct-const-globals.exit new file mode 100644 index 0000000..573541a --- /dev/null +++ b/examples/expected/0180-types-struct-const-globals.exit @@ -0,0 +1 @@ +0 diff --git a/examples/expected/0180-types-struct-const-globals.stderr b/examples/expected/0180-types-struct-const-globals.stderr new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/examples/expected/0180-types-struct-const-globals.stderr @@ -0,0 +1 @@ + diff --git a/examples/expected/0180-types-struct-const-globals.stdout b/examples/expected/0180-types-struct-const-globals.stdout new file mode 100644 index 0000000..15de0a3 --- /dev/null +++ b/examples/expected/0180-types-struct-const-globals.stdout @@ -0,0 +1,3 @@ +lit=255 expr=11 20 8 ref=255 +via-ptr=255 +copy=9 const=255 diff --git a/examples/expected/0181-types-struct-const-inline-fallback.exit b/examples/expected/0181-types-struct-const-inline-fallback.exit new file mode 100644 index 0000000..573541a --- /dev/null +++ b/examples/expected/0181-types-struct-const-inline-fallback.exit @@ -0,0 +1 @@ +0 diff --git a/examples/expected/0181-types-struct-const-inline-fallback.stderr b/examples/expected/0181-types-struct-const-inline-fallback.stderr new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/examples/expected/0181-types-struct-const-inline-fallback.stderr @@ -0,0 +1 @@ + diff --git a/examples/expected/0181-types-struct-const-inline-fallback.stdout b/examples/expected/0181-types-struct-const-inline-fallback.stdout new file mode 100644 index 0000000..30ac443 --- /dev/null +++ b/examples/expected/0181-types-struct-const-inline-fallback.stdout @@ -0,0 +1,3 @@ +use1=1 +use2=2 +counter=2 diff --git a/src/ir/lower.zig b/src/ir/lower.zig index 4b1e16d..6e70c79 100644 --- a/src/ir/lower.zig +++ b/src/ir/lower.zig @@ -1610,6 +1610,7 @@ pub const Lowering = struct { pub const scanDecls = lower_decl.scanDecls; pub const registerTypedModuleConst = lower_decl.registerTypedModuleConst; pub const registerConstArrayGlobal = lower_decl.registerConstArrayGlobal; + pub const maybeRegisterConstStructGlobal = lower_decl.maybeRegisterConstStructGlobal; pub const inferConstArrayType = lower_decl.inferConstArrayType; pub const typedConstInitFits = lower_decl.typedConstInitFits; pub const constExprInitFits = lower_decl.constExprInitFits; diff --git a/src/ir/lower/comptime.zig b/src/ir/lower/comptime.zig index 15e9095..f22d6e2 100644 --- a/src/ir/lower/comptime.zig +++ b/src/ir/lower/comptime.zig @@ -91,7 +91,23 @@ pub fn constExprValue(self: *Lowering, expr: *const Node, expected_ty: TypeId) ? // an enum field inside a global struct) serializes to its tag int // against the leaf's declared enum type. .enum_literal => |el| self.constEnumLiteral(&el, expected_ty, expr.span), - else => null, + // Any other shape: a compile-time CONST EXPRESSION over named consts + // and const-aggregate leaves (`r = K + 1`, `g = LIT.r`, `b = K[1]`) + // serializes through the shared folders — source-aware, like every + // const fold. A non-foldable expression (a call, a runtime read) + // stays null: the caller keeps its fallback (inline re-lowering for + // struct consts, a loud diagnostic for globals/array consts). + else => blk: { + const ctx = SourceConstCtx{ .lowering = self, .frame = null }; + if (self.isIntEx(expected_ty)) { + if (program_index_mod.evalConstIntExpr(expr, ctx)) |v| break :blk inst_mod.ConstantValue{ .int = v }; + } + if (isFloat(expected_ty)) { + if (program_index_mod.evalConstFloatExpr(expr, ctx)) |v| break :blk inst_mod.ConstantValue{ .float = v }; + if (program_index_mod.evalConstIntExpr(expr, ctx)) |v| break :blk inst_mod.ConstantValue{ .float = @floatFromInt(v) }; + } + break :blk null; + }, }; } diff --git a/src/ir/lower/decl.zig b/src/ir/lower/decl.zig index 6b12852..4f36308 100644 --- a/src/ir/lower/decl.zig +++ b/src/ir/lower/decl.zig @@ -780,8 +780,10 @@ pub fn scanDecls(self: *Lowering, decls: []const *const Node) void { .var_decl => self.registerTopLevelGlobal(&decl.data.var_decl), .const_decl => |cd| if (cd.value.data == .array_literal) self.registerConstArrayGlobal(&cd) - else - self.registerTypedModuleConst(&cd), + else { + self.registerTypedModuleConst(&cd); + self.maybeRegisterConstStructGlobal(&cd); + }, else => {}, } } @@ -1045,6 +1047,30 @@ pub fn inferConstArrayType(self: *Lowering, name: []const u8, elements: []const return self.module.types.arrayOf(elem_ty.?, @intCast(elements.len)); } +/// Migrate a struct-literal constant to an IMMUTABLE GLOBAL when every +/// field serializes (literals, enum tags, nested aggregates, and — via the +/// fold helpers — const expressions over named consts / const-aggregate +/// leaves). One storage, reads GEP it, `@W` becomes addressable. A const +/// with a NON-serializable field (a call, a runtime read) keeps the inline +/// re-lowering path — per-use evaluation is that class's documented +/// contract. The module-const registration stays either way (folds + +/// fallback share it); bare reads hit the GLOBAL arm first when migrated. +pub fn maybeRegisterConstStructGlobal(self: *Lowering, cd: *const ast.ConstDecl) void { + if (cd.value.data != .struct_literal) return; + const src = self.current_source_file orelse self.main_file orelse return; + const ci = self.sourceModuleConst(src, cd.name) orelse return; + if (ci.ty.isBuiltin() or self.module.types.get(ci.ty) != .@"struct") return; + const init_val = self.constStructLiteral(&cd.value.data.struct_literal, ci.ty) orelse return; + const name_id = self.module.types.internString(cd.name); + const gid = self.module.addGlobal(.{ + .name = name_id, + .ty = ci.ty, + .init_val = init_val, + .is_const = true, + }); + self.putGlobal(self.current_source_file, cd.name, .{ .id = gid, .ty = ci.ty }); +} + /// Register a top-level mutable global (e.g., `context : Context = ---;`). /// Run AFTER `resolveForwardIdentifierAliases` so a forward identifier alias /// in the type annotation (`A :: B; B :: s32; g : A = 7;`) resolves to its