Files
sx/issues/0072-global-field-access-const-initializer-silent-zero.md
agra b72d49073e fix(ir): diagnose non-constant global initializers loudly (issue 0072)
globalInitValue's issue-0071 .identifier arm closed the bare-identifier hole,
but .field_access (and every other non-literal expression shape) still fell
through to `else => null`, so a global like `g : s32 = K.x;` was emitted with
no payload and silently zero-initialized (g=0).

Make the `else` emit a diagnostic — "global '<name>' must be initialized by a
compile-time constant" — instead of a null payload, so no unsupported shape can
silently zero. Two arms added alongside:

- `.null_literal => .null_val`: a `*void = null` global was previously a
  no-payload zero-init; this preserves the exact LLVMConstNull emission (fixes
  3 ffi examples that regressed on the first cut).
- explicit `.enum_literal => null` carve-out: the stdlib's
  `OS : OperatingSystem = .unknown;` zero-init is load-bearing for compile-time
  `inline if OS == .X`; documented, not folded into a silent fallthrough.

Field-access constant *evaluation* (materializing K.x -> 9) is intentionally
not implemented: a typed struct const like K is not registered in
module_const_map, so it would require new plumbing whose writes are read at
runtime — out of scope. The diagnostic is the issue-sanctioned outcome.

Regression: examples/1118-diagnostics-global-non-const-initializer-rejected.sx
(exit 1). Gate: zig build, zig build test, run_examples.sh -> 356/0.
2026-06-02 17:57:17 +03:00

4.5 KiB

0072 — global field-access constant initializer silently zero-initializes

RESOLVED. Root cause: Lowering.globalInitValue's issue-0071 .identifier arm closed the bare-identifier hole, but .field_access (and every other non-literal expression shape) still fell through to else => null, so the global was emitted with no payload and silently zero-initialized (g=0). Fix: the else now emits a diagnostic — "global '' must be initialized by a compile-time constant" — instead of returning a null payload, so an unsupported initializer shape can never silently zero. Two arms were added alongside it: .null_literal => .null_val (a *void = null global was previously a no-payload zero-init; this preserves that exact emission — LLVMConstNull), and an explicit .enum_literal => null carve-out (the stdlib's OS : OperatingSystem = .unknown; zero-init is load-bearing for compile-time inline if OS == .X; documented, not folded into a silent fallthrough). Field-access constant evaluation (materializing K.x → 9) was intentionally not implemented: a typed struct const like K is not registered in module_const_map, so it would require new plumbing whose module_const_map writes are read at runtime — out of scope; the diagnostic is the chosen, issue-sanctioned outcome. Regression examples/1118-diagnostics-global-non-const-initializer-rejected.sx (exit 1).

Symptom

A top-level global initialized from a field access on a module constant compiles but is zero-initialized.

Observed: g=0

Expected: g should be initialized to 9, or the compiler should reject the initializer loudly if field-access global constants are not supported yet.

Reproduction

#import "modules/std.sx";

Point :: struct {
    x: s32;
    y: s32;
}

K : Point : Point.{ x = 9, y = 4 };
g : s32 = K.x;

main :: () -> s32 {
    print("g={}\n", g);
    return g;
}

Run:

./zig-out/bin/sx run .sx-tmp/review-0071-field-access-const.sx

The repro is standalone; the inline source above is sufficient to recreate the scratch file under .sx-tmp/.

Investigation prompt

Fix issue 0072: a top-level global initialized from a field access on a module constant must not silently become zero.

Context:

  • This surfaced during Codex re-review of commit ad7200c, the issue-0071 fix.
  • ad7200c correctly handles identifier initializers that name module constants (K : A : 42; g : A = K;) and diagnoses identifiers that are not usable constants.
  • A remaining non-identifier expression shape still falls through silently: K : Point : Point.{ x = 9, y = 4 }; g : s32 = K.x; emits a null global initializer payload and runs as g=0.

Suspected area:

  • src/ir/lower.zig, Lowering.globalInitValue.
  • The .identifier arm now diagnoses unusable identifier initializers, but the generic else => null still covers .field_access and other expression forms.
  • constExprValue currently handles literals, negative numeric literals, and array literals; it does not evaluate field access or struct-literal module constants. constStructLiteral can serialize direct struct literals when called with the destination type, but that machinery is not used for K.x.

Likely fix:

  • Add a loud diagnostic for unsupported top-level global initializer expression shapes, or implement field-access constant evaluation in the same step.
  • If implementing it, resolve the base identifier through ProgramIndex.module_const_map, materialize the base constant with its recorded type, then extract the named field using the IR struct layout.
  • Do not replace the current bug with a real-type sentinel such as .void; an unsupported initializer should either produce a concrete ConstantValue or a user-visible diagnostic.
  • Preserve the issue-0071 behavior: K : A : 42; g : A = K; must still emit g=42.
  • Preserve the issue-0070/0068 regressions: examples/0133-types-forward-alias-global.sx and examples/1117-diagnostics-value-const-as-type-rejected.sx must stay green.
  • Scope-check enum-literal globals separately. OS : OperatingSystem = .unknown currently relies on compile-time constant injection and pre-existing zero-init behavior; decide explicitly whether that stays carved out or gets its own separate issue.

Verification:

  • Repro above should either print g=9 and exit 9, or fail with a clear "global initializer must be a compile-time constant" diagnostic. It must not print g=0.
  • Run:
zig build
zig build test
bash tests/run_examples.sh