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.
122 lines
4.5 KiB
Markdown
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
|
|
```
|