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

122 lines
4.5 KiB
Markdown

# 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 `constArrayLiteral` → `constExprValue`
> 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
```sx
#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:
```text
pairs[0]=0,0
pairs[1]=0,0
FAIL: global array struct literal initializer zeroed
```
`sx ir <file>` shows the global as:
```llvm
@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:
```text
pairs[0]=1,2
pairs[1]=3,4
PASS
```
- Add a pinned regression in the `01xx` types block.
- Run:
```sh
zig build
zig build test
bash tests/run_examples.sh
```