lang: struct consts migrate to const globals, inline fallback (PLAN-CONST-AGG step 4)
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.
This commit is contained in:
23
examples/0180-types-struct-const-globals.sx
Normal file
23
examples/0180-types-struct-const-globals.sx
Normal file
@@ -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);
|
||||||
|
}
|
||||||
18
examples/0181-types-struct-const-inline-fallback.sx
Normal file
18
examples/0181-types-struct-const-inline-fallback.sx
Normal file
@@ -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);
|
||||||
|
}
|
||||||
1
examples/expected/0180-types-struct-const-globals.exit
Normal file
1
examples/expected/0180-types-struct-const-globals.exit
Normal file
@@ -0,0 +1 @@
|
|||||||
|
0
|
||||||
1
examples/expected/0180-types-struct-const-globals.stderr
Normal file
1
examples/expected/0180-types-struct-const-globals.stderr
Normal file
@@ -0,0 +1 @@
|
|||||||
|
|
||||||
3
examples/expected/0180-types-struct-const-globals.stdout
Normal file
3
examples/expected/0180-types-struct-const-globals.stdout
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
lit=255 expr=11 20 8 ref=255
|
||||||
|
via-ptr=255
|
||||||
|
copy=9 const=255
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
0
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
use1=1
|
||||||
|
use2=2
|
||||||
|
counter=2
|
||||||
@@ -1610,6 +1610,7 @@ pub const Lowering = struct {
|
|||||||
pub const scanDecls = lower_decl.scanDecls;
|
pub const scanDecls = lower_decl.scanDecls;
|
||||||
pub const registerTypedModuleConst = lower_decl.registerTypedModuleConst;
|
pub const registerTypedModuleConst = lower_decl.registerTypedModuleConst;
|
||||||
pub const registerConstArrayGlobal = lower_decl.registerConstArrayGlobal;
|
pub const registerConstArrayGlobal = lower_decl.registerConstArrayGlobal;
|
||||||
|
pub const maybeRegisterConstStructGlobal = lower_decl.maybeRegisterConstStructGlobal;
|
||||||
pub const inferConstArrayType = lower_decl.inferConstArrayType;
|
pub const inferConstArrayType = lower_decl.inferConstArrayType;
|
||||||
pub const typedConstInitFits = lower_decl.typedConstInitFits;
|
pub const typedConstInitFits = lower_decl.typedConstInitFits;
|
||||||
pub const constExprInitFits = lower_decl.constExprInitFits;
|
pub const constExprInitFits = lower_decl.constExprInitFits;
|
||||||
|
|||||||
@@ -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
|
// an enum field inside a global struct) serializes to its tag int
|
||||||
// against the leaf's declared enum type.
|
// against the leaf's declared enum type.
|
||||||
.enum_literal => |el| self.constEnumLiteral(&el, expected_ty, expr.span),
|
.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;
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -780,8 +780,10 @@ pub fn scanDecls(self: *Lowering, decls: []const *const Node) void {
|
|||||||
.var_decl => self.registerTopLevelGlobal(&decl.data.var_decl),
|
.var_decl => self.registerTopLevelGlobal(&decl.data.var_decl),
|
||||||
.const_decl => |cd| if (cd.value.data == .array_literal)
|
.const_decl => |cd| if (cd.value.data == .array_literal)
|
||||||
self.registerConstArrayGlobal(&cd)
|
self.registerConstArrayGlobal(&cd)
|
||||||
else
|
else {
|
||||||
self.registerTypedModuleConst(&cd),
|
self.registerTypedModuleConst(&cd);
|
||||||
|
self.maybeRegisterConstStructGlobal(&cd);
|
||||||
|
},
|
||||||
else => {},
|
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));
|
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 = ---;`).
|
/// Register a top-level mutable global (e.g., `context : Context = ---;`).
|
||||||
/// Run AFTER `resolveForwardIdentifierAliases` so a forward identifier alias
|
/// Run AFTER `resolveForwardIdentifierAliases` so a forward identifier alias
|
||||||
/// in the type annotation (`A :: B; B :: s32; g : A = 7;`) resolves to its
|
/// in the type annotation (`A :: B; B :: s32; g : A = 7;`) resolves to its
|
||||||
|
|||||||
Reference in New Issue
Block a user