Surface rename of the signed integer family: s1..s64 become i1..i64
(u1..u64, usize, isize unchanged). 'string' keeps the s-prefix arm in
name classification; width parsing moves to the i-prefix arm next to
isize.
Internal TypeId tags follow the surface (.s8/.s16/.s32/.s64 ->
.i8/.i16/.i32/.i64), as do mono-key mangle fragments (ptr_i64,
tu_i64_bool) and all display/diagnostic formatting (i{d}).
Migrated in the same sweep: stdlib + examples + issue repros + FFI C
companions (shared symbol names like ffi_id_i64), expected
stdout/stderr/ir snapshots, specs.md, readme.md, CLAUDE.md/AGENTS.md,
implementation_plan.md, docs/, issue writeups. Vendored stb_image and
historical flow state left untouched.
zig build test: 426/426; examples suite: 595/595.
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: i64;
|
|
b: i64;
|
|
}
|
|
|
|
pairs : [2]Pair = .[ .{ a = 1, b = 2 }, .{ a = 3, b = 4 } ];
|
|
|
|
main :: () -> i32 {
|
|
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
|
|
```
|