A module-global aggregate initializer rejected a `null` literal in a pointer (or optional-pointer) field as "must be initialized by a compile-time constant". `Lowering.constExprValue` had no `.null_literal` arm, so the null leaf returned no constant and the whole aggregate looked non-constant — even though `null` is the compile-time zero pointer (a top-level scalar `p : *s64 = null;` already serialized fine). Add `.null_literal => .null_val` to constExprValue. While here, make the two LLVM constant emitters exhaustive: emitConstAggregate and the top-level init_val switch in emit_llvm.zig previously ended in a silent `else => LLVMConstNull(...)` catch-all (the 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). The reject-loud path for genuinely non-constant fields is preserved. Regression: examples/0138 (array-of-struct null ptr fields, array of all-null pointers, nested struct-in-struct null ptr) and the negative examples/1126 (null ptr field beside a non-const field still errors). Fail-before/pass-after verified.
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 : *s64 = 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: *s64;
|
|
marker: s64;
|
|
}
|
|
|
|
boxes : [2]Box = .[
|
|
.{ p = null, marker = 11 },
|
|
.{ p = null, marker = 22 },
|
|
];
|
|
|
|
main :: () -> s32 {
|
|
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
|
|
```
|