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.
4.2 KiB
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_literalarm. Anullin a pointer (or optional-pointer) field therefore returned no constant, which propagated up throughconstStructLiteral/constArrayLiteraland made the whole aggregate look non-constant, soglobalInitValuerejected it with "must be initialized by a compile-time constant". Anullis 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_valtoconstExprValueso a null leaf serializes to a constant zero pointer. Made the LLVM constant emitters exhaustive while at it:emitConstAggregateand the top-levelinit_valswitch insrc/ir/emit_llvm.zigpreviously ended in a silentelse => LLVMConstNull(...)catch-all (the precise silent-arm class CLAUDE.md mandates rooting out); they now handle everyConstantValuetag explicitly (.null_val/.zeroinit→ all-zero constant,.undef→LLVMGetUndef,.func_refresolved, nested.vtableis a hard@panictripwire 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 negativeexamples/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
#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:
error: global 'boxes' must be initialized by a compile-time constant
Expected:
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_literalarm, soconstStructLiteraltreats a pointer field initialized withnullas non-constant andglobalInitValuereports the whole aggregate.src/ir/emit_llvm.zig, top-levelglobal.init_valemission andLLVMEmitter.emitConstAggregate— both currently rely on catch-allelse => LLVMConstNull(...)for severalConstantValuetags. If.null_valis threaded through aggregate constants, add explicit.null_valhandling there (and explicit.zeroinit/.undefhandling as appropriate) rather than depending on the catch-all.
Likely fix:
- Add
.null_literal => .null_valtoconstExprValuefor constant aggregate serialization. - Ensure LLVM constant emission handles
.null_valexplicitly 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:
ptrs=true true markers=11 22
- Add a pinned regression in the
01xxtypes block covering a global array-of-struct with pointer-null fields (and, if straightforward, optional null fields too). - Run:
zig build
zig build test
bash tests/run_examples.sh