Files
sx/issues/0081-global-aggregate-null-literal-rejected.md
agra d8076b9333 lang: rename signed integer types sN -> iN
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.
2026-06-12 09:31:53 +03:00

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_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, .undefLLVMGetUndef, .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

#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:

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_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:
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:
zig build
zig build test
bash tests/run_examples.sh