Files
sx/issues/0071-global-initializer-module-const-silent-zero.md
agra ad7200c196 fix(ir): materialize global initialized from module const (issue 0071)
registerTopLevelGlobal's init_val switch serialized only literal / array-
literal / struct-literal initializers. An identifier initializer
(`K : A : 42; g : A = K;`) fell through to `else => null`, so the global was
emitted with no payload and silently zero-initialized (printed g=0).

Extract the initializer serialization into globalInitValue and add an
.identifier arm that materializes the global's static value from
ProgramIndex.module_const_map (typed module consts are registered in the same
scanDecls pass-2 just before, via registerTypedModuleConst). An identifier
that names no usable constant now emits a diagnostic instead of silently
zeroing — a global has no run site for a dynamic initializer.

Other initializer shapes (enum-literal shorthand, etc.) keep their established
static-lowering behavior; enum-literal globals' zero-init is load-bearing for
`inline if OS == ...` in the stdlib, so it stays out of scope here. This pass
only closes the identifier/module-const hole.

Regression: examples/0134-types-global-init-from-module-const.sx (g=42, exit
42). Gate: zig build, zig build test, run_examples.sh -> 355/0.
2026-06-02 17:45:37 +03:00

3.9 KiB

0071 — global initialized from module const silently zero-initializes

RESOLVED. Root cause: Lowering.registerTopLevelGlobal's init_val switch serialized only literal / array-literal / struct-literal initializers; an identifier initializer (g : A = K;) fell through to else => null, so the global was emitted with no payload and silently zero-initialized. Fix: extracted the initializer serialization into Lowering.globalInitValue and added an .identifier arm that materializes the global's static value from ProgramIndex.module_const_map (typed module consts are registered in the same pass-2 just before, via registerTypedModuleConst). An identifier that names no usable constant now emits a diagnostic instead of silently zeroing. Other initializer shapes (enum-literal shorthand, etc.) keep their established static-lowering behavior — this pass only closes the identifier/module-const hole. Regression: examples/0134-types-global-init-from-module-const.sx (g=42 / exit 42).

Symptom

A top-level global initialized from a module constant compiles but is zero-initialized instead of receiving the constant's value.

Observed:

g=0

Expected: g should be initialized to 42, or the compiler should reject the initializer loudly if identifier/module-const global initializers are not supported.

Reproduction

#import "modules/std.sx";

A :: B;
B :: s32;

K : A : 42;
g : A = K;

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

Run:

./zig-out/bin/sx run .sx-tmp/probe-0070-global-init-from-const.sx

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

Investigation prompt

Fix issue 0071: a top-level global initialized from a module constant must not silently become zero.

Context:

  • This surfaced during Codex re-review of 932cdfa, the issue-0070 fix.
  • 932cdfa correctly defers top-level global and typed-module-const annotation resolution until after the forward-alias fixpoint.
  • The remaining bug is in the global initializer path, not the annotation path: K : A : 42; g : A = K; resolves A correctly, registers K in ProgramIndex.module_const_map, but g is emitted as zero.

Suspected area:

  • src/ir/lower.zig, Lowering.registerTopLevelGlobal.
  • Its init_val switch serializes literal / array / struct-literal initializers, but an identifier initializer falls through to else => null, and the global is emitted with no initializer payload. That silently becomes zero-initialized.
  • Related facts: ProgramIndex.module_const_map already records typed module constants via registerTypedModuleConst, now in pass 2 after alias convergence.

Likely fix:

  • Add explicit handling for identifier initializers that name a module constant, converting the recorded constant value into the global's ConstantValue with the global's declared type.
  • If some initializer shape cannot be represented as a global constant yet, emit a diagnostic instead of returning null / zero-initializing.
  • Do not regress issue 0070: A :: B; B :: s32; g : A = 7; and K : A : 35; must still resolve through the converged alias map.
  • Preserve literal, array literal, struct literal, and foreign-global behavior.

Verification:

  • Add a focused regression, likely in the 01xx types block:
#import "modules/std.sx";
A :: B;
B :: s32;
K : A : 42;
g : A = K;
main :: () -> s32 { print("g={}\n", g); return g; }
  • Keep these green:
    • examples/0133-types-forward-alias-global.sx
    • examples/0132-types-forward-type-alias.sx
    • examples/0116-types-type-alias-size-align.sx
    • examples/0201-generics-generic-struct.sx
    • examples/1117-diagnostics-value-const-as-type-rejected.sx
  • Run:
zig build
zig build test
bash tests/run_examples.sh

Expected result: the new regression prints g=42 and exits 42; unsupported global initializer shapes no longer silently zero-initialize; the full suite passes.