fix(ir): materialize global aggregate struct-literal initializers (issue 0080)

A module-global array of struct literals (`pairs : [2]Pair = .[ .{...}, .{...} ]`)
was emitted as `zeroinitializer`, silently dropping every declared field — reads
returned 0 with no diagnostic. Global struct literals and struct-with-array
already worked; the gap was struct literals used as ARRAY elements.

Root cause: `Lowering.constExprValue` (the const-aggregate serializer for global
initializers) had no `.struct_literal` arm. `constArrayLiteral` serialized each
element through `constExprValue`, so a struct-literal element returned null,
collapsing the whole array initializer to null; `globalInitValue` then emitted no
payload and the LLVM backend zero-initialized the global — the same silent-zero
class as 0071/0072, one level inside an array literal.

Fix: make `constExprValue` type-aware — thread the destination element/field
TypeId so a struct-literal leaf routes through `constStructLiteral` and a nested
array-literal through `constArrayLiteral` with the correct element type.
`constArrayLiteral` derives its element type from the array TypeId;
`constStructLiteral` passes each field's type. A global aggregate initializer that
still does not fully reduce to a compile-time constant is now rejected loudly
(`diagnoseNonConstGlobal`) instead of silently zeroing. `emitConstAggregate`
already recurses over nested aggregates, so `sx run` (JIT) and `sx build` (AOT)
both materialize the declared values.

Regression: examples/0137-types-global-aggregate-literal-init.sx (global
[N]Struct literal, global struct literal, struct-with-array, nested
array-of-struct-with-array; values read back with no prior store, plus a store on
top). Fails on the pre-fix compiler (array-of-struct fields read 0), passes after.

Marks issues 0079 (already resolved) and 0080 RESOLVED.
This commit is contained in:
agra
2026-06-04 04:04:40 +03:00
parent 7306d37748
commit e93879816d
6 changed files with 212 additions and 11 deletions

View File

@@ -926,15 +926,15 @@ pub const Lowering = struct {
.bool_literal => |bl| .{ .boolean = bl.value },
.float_literal => |fl| .{ .float = fl.value },
.string_literal => |sl| .{ .string = self.module.types.internString(sl.raw) },
.array_literal => |al| self.constArrayLiteral(al.elements),
.struct_literal => |sl| self.constStructLiteral(&sl, var_ty),
.array_literal => |al| self.constArrayLiteral(al.elements, var_ty) orelse self.diagnoseNonConstGlobal(vd, v),
.struct_literal => |sl| self.constStructLiteral(&sl, var_ty) orelse self.diagnoseNonConstGlobal(vd, v),
.identifier => |id| blk: {
// A global initialized from a module constant copies the
// constant's recorded value (typed module consts land in
// `module_const_map` via `registerTypedModuleConst`, run in the
// same pass-2 before this).
if (self.program_index.module_const_map.get(id.name)) |ci| {
if (self.constExprValue(ci.value)) |cv| break :blk cv;
if (self.constExprValue(ci.value, var_ty)) |cv| break :blk cv;
}
if (self.diagnostics) |d|
d.addFmt(.err, v.span, "global '{s}' must be initialized by a compile-time constant; '{s}' is not a usable constant here", .{ vd.name, id.name });
@@ -957,6 +957,16 @@ pub const Lowering = struct {
};
}
/// A global aggregate initializer (array/struct literal) that does not fully
/// reduce to a compile-time constant is rejected loudly. Without this the
/// `null` payload would fall through to a zero-initialized global, silently
/// dropping the declared fields (issues 0071/0072/0080).
fn diagnoseNonConstGlobal(self: *Lowering, vd: *const ast.VarDecl, v: *const Node) ?inst_mod.ConstantValue {
if (self.diagnostics) |d|
d.addFmt(.err, v.span, "global '{s}' must be initialized by a compile-time constant", .{vd.name});
return null;
}
/// Resolve identifier-RHS type aliases whose target is declared LATER in the
/// file. The forward scan above only registers an alias (`A :: B`) when `B`
/// is already in `type_alias_map` / the `TypeTable`; a forward target isn't
@@ -993,19 +1003,30 @@ pub const Lowering = struct {
}
}
/// Try to convert an array literal's elements into a compile-time ConstantValue.aggregate.
/// Returns null if any element is not a compile-time constant.
fn constArrayLiteral(self: *Lowering, elements: []const *const Node) ?inst_mod.ConstantValue {
/// Try to convert an array literal's elements into a compile-time
/// ConstantValue.aggregate. `array_ty` is the array's resolved TypeId; its
/// element type drives type-aware serialization of struct-literal and
/// nested-array elements. Returns null if `array_ty` is not an array type or
/// any element is not a compile-time constant.
fn constArrayLiteral(self: *Lowering, elements: []const *const Node, array_ty: TypeId) ?inst_mod.ConstantValue {
if (array_ty.isBuiltin()) return null;
const elem_ty: TypeId = switch (self.module.types.get(array_ty)) {
.array => |a| a.element,
else => return null,
};
const vals = self.alloc.alloc(inst_mod.ConstantValue, elements.len) catch return null;
for (elements, 0..) |elem, i| {
vals[i] = self.constExprValue(elem) orelse return null;
vals[i] = self.constExprValue(elem, elem_ty) orelse return null;
}
return .{ .aggregate = vals };
}
/// Try to convert a single AST expression into a compile-time ConstantValue.
/// Returns null if the expression is not constant-foldable here.
fn constExprValue(self: *Lowering, expr: *const Node) ?inst_mod.ConstantValue {
/// `expected_ty` is the destination element/field type — it lets aggregate
/// leaves (struct literals, nested arrays) serialize with the correct shape
/// rather than collapsing to null (issue 0080). Returns null if the
/// expression is not constant-foldable here.
fn constExprValue(self: *Lowering, expr: *const Node, expected_ty: TypeId) ?inst_mod.ConstantValue {
return switch (expr.data) {
.int_literal => |il| .{ .int = il.value },
.bool_literal => |bl| .{ .boolean = bl.value },
@@ -1020,7 +1041,8 @@ pub const Lowering = struct {
},
else => null,
},
.array_literal => |al| self.constArrayLiteral(al.elements),
.array_literal => |al| self.constArrayLiteral(al.elements, expected_ty),
.struct_literal => |sl| self.constStructLiteral(&sl, expected_ty),
else => null,
};
}
@@ -1055,7 +1077,7 @@ pub const Lowering = struct {
break :blk null;
};
if (init_expr) |e| {
vals[fi] = self.constExprValue(e) orelse return null;
vals[fi] = self.constExprValue(e, sf.ty) orelse return null;
} else {
vals[fi] = .zeroinit;
}