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:
121
issues/0080-global-array-struct-literal-initializer-zero.md
Normal file
121
issues/0080-global-array-struct-literal-initializer-zero.md
Normal file
@@ -0,0 +1,121 @@
|
||||
# 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
|
||||
```
|
||||
Reference in New Issue
Block a user