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:
agra
2026-06-11 12:58:17 +03:00
parent c23b76c7d6
commit 679653fda8
11 changed files with 97 additions and 3 deletions

View File

@@ -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