Files
sx/issues/0080-global-array-struct-literal-initializer-zero.md
agra e93879816d 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.
2026-06-04 04:04:40 +03:00

4.5 KiB

0080 - global array of struct literals silently zero-initializes

RESOLVED. Root cause: Lowering.constExprValue (src/ir/lower.zig) — the constant- aggregate serializer for global initializers — handled primitive and nested- array leaves but had no .struct_literal arm. A module-global [N]Struct initialized with struct literals reached constArrayLiteralconstExprValue per element; each struct-literal element returned null, collapsing the whole array initializer to null. globalInitValue then emitted no payload, so the LLVM backend zero-initialized the global (@pairs = ... zeroinitializer), silently dropping every declared field — the same silent-zero class as 0071/0072, one level inside an array literal. (A global struct literal and a struct-with-array already worked, because constStructLiteral existed and was reached directly; the gap was specifically struct literals as array elements.) 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 falling through to a zeroed global. The downstream emitConstAggregate already recurses over nested aggregates, so const/AOT (sx build) and JIT (sx run) 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.

Symptom

A module-global fixed array whose elements are struct literals is emitted as zero-initialized storage instead of preserving the literal fields.

Observed: reading pairs[0].b and pairs[1].a prints 0. Expected: the global should contain the declared struct literal values (2 and 3), or the compiler should reject the initializer loudly if this constant shape is unsupported.

Reproduction

#import "modules/std.sx";

Pair :: struct {
    a: s64;
    b: s64;
}

pairs : [2]Pair = .[ .{ a = 1, b = 2 }, .{ a = 3, b = 4 } ];

main :: () -> s32 {
    print("pairs[0]={},{}\n", pairs[0].a, pairs[0].b);
    print("pairs[1]={},{}\n", pairs[1].a, pairs[1].b);
    if pairs[0].a == 1 and pairs[0].b == 2 and pairs[1].a == 3 and pairs[1].b == 4 {
        print("PASS\n");
        return 0;
    }
    print("FAIL: global array struct literal initializer zeroed\n");
    return 1;
}

On the current compiler this prints:

pairs[0]=0,0
pairs[1]=0,0
FAIL: global array struct literal initializer zeroed

sx ir <file> shows the global as:

@pairs = internal global [2 x { i64, i64 }] zeroinitializer

Investigation prompt

Fix issue 0080: a module-global array initialized with struct literal elements silently becomes zeroinitializer.

Suspected area:

  • src/ir/lower.zig, Lowering.globalInitValue.
  • src/ir/lower.zig, Lowering.constArrayLiteral.
  • src/ir/lower.zig, Lowering.constExprValue.
  • src/ir/lower.zig, Lowering.constStructLiteral.

Likely root cause: globalInitValue handles a top-level .array_literal by calling constArrayLiteral, and constArrayLiteral serializes each element via constExprValue. constExprValue handles primitive literals and nested arrays, but not .struct_literal, so an array whose element is a struct literal returns null. That null initializer payload is later emitted as zero-initialized storage, recreating the silent zero pattern from issues 0071/0072 one level inside an otherwise-supported array literal.

Likely fix:

  • Thread the expected element TypeId into constArrayLiteral, or otherwise make constExprValue type-aware for struct literals.
  • Serialize each struct element through constStructLiteral with the array's element type.
  • If any element shape is still unsupported, emit a diagnostic naming the global instead of returning null and allowing zero-initialization.

Verification:

  • Run the repro above and expect:
pairs[0]=1,2
pairs[1]=3,4
PASS
  • Add a pinned regression in the 01xx types block.
  • Run:
zig build
zig build test
bash tests/run_examples.sh