Files
sx/issues/0072-global-field-access-const-initializer-silent-zero.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.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: i32;
    y: i32;
}

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

main :: () -> i32 {
    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 : i32 = 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