Files
sx/issues/0082-global-enum-literal-initializer-zeroes.md
agra 263333bd26 fix(ir): serialize enum-literal global initializers (issue 0082)
A module-global initialized with an enum literal silently zero-initialized
to the first tag (`chosen : Color = .green` read back as `.red`), and an
enum tag inside a global array/struct was rejected as non-constant. The
constant serializer had no enum-literal arm.

Add `Lowering.constEnumLiteral`: serialize an enum literal to a
`ConstantValue.int` holding the variant's tag value, resolved against the
destination enum type and respecting explicit variant values; the global's
type drives the backing width at emit time. Wired into `globalInitValue`
(scalar global) and `constExprValue` (array element / struct field / nested
aggregate). A non-enum destination or unknown variant is diagnosed loudly,
never silently zero-initialized. The compiler-injected OS/ARCH globals now
serialize to their real `.unknown` tag (6 / 4); runtime reads are unchanged
(they resolve through comptime_constants), so only the static initializer in
the pinned .ir snapshots changes.

Remove the silent `func_ref => orelse LLVMConstNull` fallbacks in the LLVM
constant emitters: aggregate func_ref leaves carry a `require_resolved` flag
(transient null in Pass 0, loud diagnostic if still unresolved in the
Pass-1.5 re-emit), a top-level func_ref global is resolved in
initVtableGlobals, and the comptime (#run) path bails loudly instead of
emitting a null function pointer.

Regression: examples/0139-types-global-enum-literal-init.sx (scalar, array,
struct field, explicit-value enum u16 stride, struct-array with enum field);
negative: examples/1127-diagnostics-global-enum-literal-bad-variant.sx.
Mark issue 0082 RESOLVED.
2026-06-04 04:52:42 +03:00

4.3 KiB

0082 - global enum-literal initializer silently zero-initializes

RESOLVED. Root cause: Lowering.globalInitValue (src/ir/lower.zig) carried an .enum_literal => null carve-out: any enum-literal global initializer returned a null payload, which the LLVM/interp emitters turn into a zero-initialized global — so chosen : Color = .green read back as the first tag (.red). constExprValue had no enum-literal arm either, so an enum tag inside a global array ([2]Color = .[.green, .blue]) or struct field made the whole aggregate look non-constant and the global was rejected outright. Fix: a new Lowering.constEnumLiteral serializes an enum literal to a ConstantValue.int holding the variant's tag value, resolved against the destination enum type and respecting explicit variant values (enum { a; b :: 5; }); the global's type drives the backing width at emit time. Wired into both globalInitValue (scalar global) and constExprValue (array element / struct field / nested aggregate). A non-enum destination or an unknown variant is diagnosed loudly — never silently zero-initialized. The compiler-injected OS/ARCH globals now serialize to their real .unknown tag (6 / 4) instead of relying on the null→zero fallback; runtime reads are unchanged because they resolve through comptime_constants. As part of the same exhaustiveness pass, the silent func_ref => … orelse LLVMConstNull fallbacks in the LLVM constant emitters (src/ir/emit_llvm.zig) were removed: aggregate func_ref leaves carry a require_resolved flag (transient null in Pass 0, loud diagnostic if still unresolved in the Pass-1.5 re-emit), a top-level func_ref global is resolved in initVtableGlobals, and the comptime (#run) path bails loudly instead of emitting a null function pointer. Regression: examples/0139-types-global-enum-literal-init.sx (scalar enum global, global array of enum, enum struct field, explicit-value enum u16 for element-stride, struct-array with enum field) — FAILS on the pre-fix compiler (wrong tag / rejected as non-constant), PASSES after. Negative: examples/1127-diagnostics-global-enum-literal-bad-variant.sx (unknown variant rejected loudly, exit 1).

Symptom

A module-global enum initialized with a non-zero enum literal silently reads back as the zero tag. Observed: chosen : Color = .green; prints .red and the program exits 1. Expected: it should print .green and exit 0, or the compiler should reject unsupported enum-literal global initializers loudly instead of zero-initializing.

Reproduction

#import "modules/std.sx";

Color :: enum u8 { red; green; blue; }

chosen : Color = .green;

main :: () -> s32 {
    print("chosen={}\n", chosen);
    if chosen == .green {
        print("PASS\n");
        return 0;
    }
    print("FAIL\n");
    return 1;
}

Observed:

chosen=.red
FAIL

Expected:

chosen=.green
PASS

Investigation prompt

Fix issue 0082: module-global enum literal initializers silently become the zero tag.

Suspected area:

  • src/ir/lower.zig, Lowering.globalInitValue: the .enum_literal => null carve-out preserves the stdlib's historical zero-init path for compiler- injected OS : OperatingSystem = .unknown, but it also silently drops any user-written non-zero enum literal such as .green.
  • src/ir/lower.zig, Lowering.constExprValue: aggregate enum-literal fields are currently not serialized either, so audit both top-level and aggregate enum literals.

Likely fix:

  • Resolve the destination enum type from var_ty / expected_ty and serialize the enum tag as a ConstantValue.int with the variant index/value.
  • If a particular enum literal shape cannot be serialized yet (payload variants, unsupported explicit tag values, etc.), emit a diagnostic instead of returning null.
  • Preserve the compiler-injected OperatingSystem / Architecture behavior by making those globals real constants, not by relying on null initializer fallback.

Verification:

  • Run the repro above and expect chosen=.green / PASS / exit 0.
  • Add a pinned regression in the 01xx types block for a non-zero enum global and, if supported by the fix, an enum field inside a global aggregate.
  • Run:
zig build
zig build test
bash tests/run_examples.sh