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.
117 lines
4.2 KiB
Markdown
117 lines
4.2 KiB
Markdown
# 0081 - global aggregate null literal rejected as non-constant
|
|
|
|
> **RESOLVED.**
|
|
> **Root cause:** `Lowering.constExprValue` (`src/ir/lower.zig`) — the
|
|
> constant-aggregate serializer for global initializers — had no
|
|
> `.null_literal` arm. A `null` in a pointer (or optional-pointer) field
|
|
> therefore returned no constant, which propagated up through
|
|
> `constStructLiteral` / `constArrayLiteral` and made the whole aggregate look
|
|
> non-constant, so `globalInitValue` rejected it with "must be initialized by a
|
|
> compile-time constant". A `null` is a compile-time constant (the zero
|
|
> pointer) and a top-level scalar pointer global (`p : *i64 = null;`) already
|
|
> serialized fine — only the nested-aggregate path was wrong.
|
|
> **Fix:** add `.null_literal => .null_val` to `constExprValue` so a null leaf
|
|
> serializes to a constant zero pointer. Made the LLVM constant emitters
|
|
> exhaustive while at it: `emitConstAggregate` and the top-level `init_val`
|
|
> switch in `src/ir/emit_llvm.zig` previously ended in a silent
|
|
> `else => LLVMConstNull(...)` catch-all (the precise silent-arm class CLAUDE.md
|
|
> mandates rooting out); they now handle every `ConstantValue` tag explicitly
|
|
> (`.null_val`/`.zeroinit` → all-zero constant, `.undef` → `LLVMGetUndef`,
|
|
> `.func_ref` resolved, nested `.vtable` is a hard `@panic` tripwire since
|
|
> vtables are top-level-only). The reject-loud path for genuinely non-constant
|
|
> fields (a runtime call, etc.) is preserved.
|
|
> **Regression:** `examples/0138-types-global-aggregate-null-pointer-field.sx`
|
|
> (array-of-struct with null pointer fields, global array of all-null pointers,
|
|
> nested struct-in-struct null pointer — asserts null reads + correct neighbors)
|
|
> and the negative `examples/1126-diagnostics-global-aggregate-non-const-field-rejected.sx`
|
|
> (a null pointer field beside a non-constant field still errors loudly).
|
|
> Verified fail-before (pre-fix rejects 0138) / pass-after.
|
|
|
|
## Symptom
|
|
|
|
A module-global aggregate initializer rejects a `null` literal in a pointer
|
|
field as "not a compile-time constant"; expected the null pointer to serialize
|
|
as a constant zero pointer the same way a top-level pointer global does.
|
|
|
|
## Reproduction
|
|
|
|
```sx
|
|
#import "modules/std.sx";
|
|
|
|
Box :: struct {
|
|
p: *i64;
|
|
marker: i64;
|
|
}
|
|
|
|
boxes : [2]Box = .[
|
|
.{ p = null, marker = 11 },
|
|
.{ p = null, marker = 22 },
|
|
];
|
|
|
|
main :: () -> i32 {
|
|
print("ptrs={} {} markers={} {}\n",
|
|
boxes[0].p == null,
|
|
boxes[1].p == null,
|
|
boxes[0].marker,
|
|
boxes[1].marker);
|
|
if boxes[0].p == null and boxes[1].p == null and boxes[0].marker == 11 and boxes[1].marker == 22 {
|
|
return 0;
|
|
}
|
|
return 1;
|
|
}
|
|
```
|
|
|
|
Observed:
|
|
|
|
```text
|
|
error: global 'boxes' must be initialized by a compile-time constant
|
|
```
|
|
|
|
Expected:
|
|
|
|
```text
|
|
ptrs=true true markers=11 22
|
|
```
|
|
|
|
## Investigation prompt
|
|
|
|
Fix issue 0081: module-global aggregate initializers reject `null` literals in
|
|
pointer fields even though `null` is a compile-time constant pointer value.
|
|
|
|
Suspected area:
|
|
- `src/ir/lower.zig`, `Lowering.constExprValue` — the switch has no
|
|
`.null_literal` arm, so `constStructLiteral` treats a pointer field initialized
|
|
with `null` as non-constant and `globalInitValue` reports the whole aggregate.
|
|
- `src/ir/emit_llvm.zig`, top-level `global.init_val` emission and
|
|
`LLVMEmitter.emitConstAggregate` — both currently rely on catch-all
|
|
`else => LLVMConstNull(...)` for several `ConstantValue` tags. If `.null_val`
|
|
is threaded through aggregate constants, add explicit `.null_val` handling
|
|
there (and explicit `.zeroinit` / `.undef` handling as appropriate) rather
|
|
than depending on the catch-all.
|
|
|
|
Likely fix:
|
|
- Add `.null_literal => .null_val` to `constExprValue` for constant aggregate
|
|
serialization.
|
|
- Ensure LLVM constant emission handles `.null_val` explicitly for both
|
|
top-level constants and nested aggregate leaves.
|
|
- Keep unsupported aggregate expressions loud: non-constant calls/field-accesses
|
|
should still diagnose instead of zero-initializing.
|
|
|
|
Verification:
|
|
- Run the repro above and expect:
|
|
|
|
```text
|
|
ptrs=true true markers=11 22
|
|
```
|
|
|
|
- Add a pinned regression in the `01xx` types block covering a global
|
|
array-of-struct with pointer-null fields (and, if straightforward, optional
|
|
null fields too).
|
|
- Run:
|
|
|
|
```sh
|
|
zig build
|
|
zig build test
|
|
bash tests/run_examples.sh
|
|
```
|