From 263333bd260bec0f604dab1ae03427f580ecb646 Mon Sep 17 00:00:00 2001 From: agra Date: Thu, 4 Jun 2026 04:52:42 +0300 Subject: [PATCH] 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. --- .../0139-types-global-enum-literal-init.sx | 51 ++++++++ ...nostics-global-enum-literal-bad-variant.sx | 16 +++ .../expected/0031-basic-local-fn-return.ir | 4 +- .../expected/0032-basic-ufcs-return-type.ir | 4 +- .../0139-types-global-enum-literal-init.exit | 1 + ...0139-types-global-enum-literal-init.stderr | 1 + ...0139-types-global-enum-literal-init.stdout | 4 + .../0416-protocols-auto-type-erasure.ir | 4 +- ...stics-global-enum-literal-bad-variant.exit | 1 + ...ics-global-enum-literal-bad-variant.stderr | 5 + ...ics-global-enum-literal-bad-variant.stdout | 1 + .../1309-ffi-objc-class-method-lowering.ir | 4 +- .../1314-ffi-objc-class-dealloc-roundtrip.ir | 4 +- .../1319-ffi-objc-property-sx-defined.ir | 4 +- .../1329-ffi-objc-call-03-selector-sharing.ir | 4 +- .../1332-ffi-objc-call-06-sret-return.ir | 4 +- .../1347-ffi-objc-dsl-07-mangling-table.ir | 4 +- .../expected/1425-ffi-jni-main-03-ctor.ir | 4 +- ...-global-enum-literal-initializer-zeroes.md | 110 ++++++++++++++++++ src/ir/emit_llvm.zig | 52 +++++++-- src/ir/lower.zig | 45 ++++++- 21 files changed, 294 insertions(+), 33 deletions(-) create mode 100644 examples/0139-types-global-enum-literal-init.sx create mode 100644 examples/1127-diagnostics-global-enum-literal-bad-variant.sx create mode 100644 examples/expected/0139-types-global-enum-literal-init.exit create mode 100644 examples/expected/0139-types-global-enum-literal-init.stderr create mode 100644 examples/expected/0139-types-global-enum-literal-init.stdout create mode 100644 examples/expected/1127-diagnostics-global-enum-literal-bad-variant.exit create mode 100644 examples/expected/1127-diagnostics-global-enum-literal-bad-variant.stderr create mode 100644 examples/expected/1127-diagnostics-global-enum-literal-bad-variant.stdout create mode 100644 issues/0082-global-enum-literal-initializer-zeroes.md diff --git a/examples/0139-types-global-enum-literal-init.sx b/examples/0139-types-global-enum-literal-init.sx new file mode 100644 index 0000000..98cd432 --- /dev/null +++ b/examples/0139-types-global-enum-literal-init.sx @@ -0,0 +1,51 @@ +// A module-global initialized with an enum literal (`.Variant`) reads back the +// declared tag — scalar, inside a global array, and as a struct field, for both +// a plain enum (tag == declaration index) and an explicit-value enum (`enum u16 +// { ok :: 200; ... }`, larger backing for element-stride coverage). +// Regression (issue 0082): `globalInitValue` had an `.enum_literal => null` +// carve-out (kept for the compiler-injected `OS`/`ARCH` globals) that silently +// zero-initialized EVERY enum global to the first tag — so `chosen : Color = +// .green` read back as `.red` — while a global array/struct of enums was +// rejected outright as non-constant. The fix serializes the enum literal to its +// tag value (respecting explicit variant values) against the destination enum +// type, for the scalar global, the array element, and the nested aggregate +// field. (Explicit-value enums print as `.` because the `{}` formatter indexes +// variants by position — a separate, pre-existing limitation — so those are +// asserted by equality, not by their printed name.) + +#import "modules/std.sx"; + +Color :: enum u8 { red; green; blue; } +Code :: enum u16 { ok :: 200; not_found :: 404; teapot :: 418; } + +Pair :: struct { a: Color; b: Color; } +Row :: struct { status: Code; pad: s64; } + +// scalar enum global +chosen : Color = .green; +// global array of enum +palette : [3]Color = .[ .blue, .green, .red ]; +// enum field(s) inside a global struct +pair : Pair = .{ a = .blue, b = .green }; +// explicit-value enum: scalar, array (2-byte stride), and inside a struct array +status : Code = .teapot; +codes : [3]Code = .[ .ok, .not_found, .teapot ]; +rows : [2]Row = .[ .{ status = .not_found, pad = 11 }, .{ status = .teapot, pad = 22 } ]; + +main :: () { + print("chosen={}\n", chosen); + print("palette={},{},{}\n", palette[0], palette[1], palette[2]); + print("pair.a={} pair.b={}\n", pair.a, pair.b); + + if chosen == .green + and palette[0] == .blue and palette[1] == .green and palette[2] == .red + and pair.a == .blue and pair.b == .green + and status == .teapot + and codes[0] == .ok and codes[1] == .not_found and codes[2] == .teapot + and rows[0].status == .not_found and rows[0].pad == 11 + and rows[1].status == .teapot and rows[1].pad == 22 { + print("PASS\n"); + } else { + print("FAIL: global enum-literal initializer mis-serialized\n"); + } +} diff --git a/examples/1127-diagnostics-global-enum-literal-bad-variant.sx b/examples/1127-diagnostics-global-enum-literal-bad-variant.sx new file mode 100644 index 0000000..01891ef --- /dev/null +++ b/examples/1127-diagnostics-global-enum-literal-bad-variant.sx @@ -0,0 +1,16 @@ +// A module-global enum-literal initializer naming a variant that does not exist +// must be rejected loudly — never silently zero-initialized to the first tag. +// Regression (issue 0082): the enum-literal global serializer resolves the tag +// against the destination enum type; an unknown variant emits a diagnostic and +// fails the build instead of falling back to a null (zero-tag) initializer. +// Expected: "'.purple' is not a variant of enum 'Color'"; exit 1. + +#import "modules/std.sx"; + +Color :: enum u8 { red; green; blue; } +bad : Color = .purple; + +main :: () -> s32 { + print("{}\n", bad); + return 0; +} diff --git a/examples/expected/0031-basic-local-fn-return.ir b/examples/expected/0031-basic-local-fn-return.ir index 5dd035a..c6df18f 100644 --- a/examples/expected/0031-basic-local-fn-return.ir +++ b/examples/expected/0031-basic-local-fn-return.ir @@ -1,6 +1,6 @@ -@OS = internal global i64 0 -@ARCH = internal global i64 0 +@OS = internal global i64 6 +@ARCH = internal global i64 4 @POINTER_SIZE = internal global i64 8 @__sx_default_context = internal global { { ptr, ptr, ptr }, ptr } { { ptr, ptr, ptr } { ptr null, ptr @__thunk_CAllocator_Allocator_alloc, ptr @__thunk_CAllocator_Allocator_dealloc }, ptr null } @str = private unnamed_addr constant [2 x i8] c"0\00", align 1 diff --git a/examples/expected/0032-basic-ufcs-return-type.ir b/examples/expected/0032-basic-ufcs-return-type.ir index 7a598b8..0665515 100644 --- a/examples/expected/0032-basic-ufcs-return-type.ir +++ b/examples/expected/0032-basic-ufcs-return-type.ir @@ -1,6 +1,6 @@ -@OS = internal global i64 0 -@ARCH = internal global i64 0 +@OS = internal global i64 6 +@ARCH = internal global i64 4 @POINTER_SIZE = internal global i64 8 @__sx_default_context = internal global { { ptr, ptr, ptr }, ptr } { { ptr, ptr, ptr } { ptr null, ptr @__thunk_CAllocator_Allocator_alloc, ptr @__thunk_CAllocator_Allocator_dealloc }, ptr null } @str = private unnamed_addr constant [2 x i8] c"0\00", align 1 diff --git a/examples/expected/0139-types-global-enum-literal-init.exit b/examples/expected/0139-types-global-enum-literal-init.exit new file mode 100644 index 0000000..573541a --- /dev/null +++ b/examples/expected/0139-types-global-enum-literal-init.exit @@ -0,0 +1 @@ +0 diff --git a/examples/expected/0139-types-global-enum-literal-init.stderr b/examples/expected/0139-types-global-enum-literal-init.stderr new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/examples/expected/0139-types-global-enum-literal-init.stderr @@ -0,0 +1 @@ + diff --git a/examples/expected/0139-types-global-enum-literal-init.stdout b/examples/expected/0139-types-global-enum-literal-init.stdout new file mode 100644 index 0000000..d62dc9c --- /dev/null +++ b/examples/expected/0139-types-global-enum-literal-init.stdout @@ -0,0 +1,4 @@ +chosen=.green +palette=.blue,.green,.red +pair.a=.blue pair.b=.green +PASS diff --git a/examples/expected/0416-protocols-auto-type-erasure.ir b/examples/expected/0416-protocols-auto-type-erasure.ir index ba8e0a9..acad6cc 100644 --- a/examples/expected/0416-protocols-auto-type-erasure.ir +++ b/examples/expected/0416-protocols-auto-type-erasure.ir @@ -1,6 +1,6 @@ -@OS = internal global i64 0 -@ARCH = internal global i64 0 +@OS = internal global i64 6 +@ARCH = internal global i64 4 @POINTER_SIZE = internal global i64 8 @__sx_default_context = internal global { { ptr, ptr, ptr }, ptr } { { ptr, ptr, ptr } { ptr null, ptr @__thunk_CAllocator_Allocator_alloc, ptr @__thunk_CAllocator_Allocator_dealloc }, ptr null } @__Counter__SimpleCounter__vtable = internal constant { ptr, ptr } { ptr @__thunk_SimpleCounter_Counter_inc, ptr @__thunk_SimpleCounter_Counter_get } diff --git a/examples/expected/1127-diagnostics-global-enum-literal-bad-variant.exit b/examples/expected/1127-diagnostics-global-enum-literal-bad-variant.exit new file mode 100644 index 0000000..d00491f --- /dev/null +++ b/examples/expected/1127-diagnostics-global-enum-literal-bad-variant.exit @@ -0,0 +1 @@ +1 diff --git a/examples/expected/1127-diagnostics-global-enum-literal-bad-variant.stderr b/examples/expected/1127-diagnostics-global-enum-literal-bad-variant.stderr new file mode 100644 index 0000000..6380abf --- /dev/null +++ b/examples/expected/1127-diagnostics-global-enum-literal-bad-variant.stderr @@ -0,0 +1,5 @@ +error: '.purple' is not a variant of enum 'Color' + --> examples/1127-diagnostics-global-enum-literal-bad-variant.sx:11:15 + | +11 | bad : Color = .purple; + | ^^^^^^^ diff --git a/examples/expected/1127-diagnostics-global-enum-literal-bad-variant.stdout b/examples/expected/1127-diagnostics-global-enum-literal-bad-variant.stdout new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/examples/expected/1127-diagnostics-global-enum-literal-bad-variant.stdout @@ -0,0 +1 @@ + diff --git a/examples/expected/1309-ffi-objc-class-method-lowering.ir b/examples/expected/1309-ffi-objc-class-method-lowering.ir index fbc26d3..25acc9e 100644 --- a/examples/expected/1309-ffi-objc-class-method-lowering.ir +++ b/examples/expected/1309-ffi-objc-class-method-lowering.ir @@ -1,8 +1,8 @@ @__SxFoo_state_ivar = internal global ptr null @__SxFoo_class = internal global ptr null -@OS = internal global i64 0 -@ARCH = internal global i64 0 +@OS = internal global i64 6 +@ARCH = internal global i64 4 @POINTER_SIZE = internal global i64 8 @__sx_default_context = internal global { { ptr, ptr, ptr }, ptr } { { ptr, ptr, ptr } { ptr null, ptr @__thunk_CAllocator_Allocator_alloc, ptr @__thunk_CAllocator_Allocator_dealloc }, ptr null } @__sx_objc_cstr_dealloc = internal global [8 x i8] c"dealloc\00" diff --git a/examples/expected/1314-ffi-objc-class-dealloc-roundtrip.ir b/examples/expected/1314-ffi-objc-class-dealloc-roundtrip.ir index 40da071..6df1dbe 100644 --- a/examples/expected/1314-ffi-objc-class-dealloc-roundtrip.ir +++ b/examples/expected/1314-ffi-objc-class-dealloc-roundtrip.ir @@ -1,8 +1,8 @@ @__SxFoo_state_ivar = internal global ptr null @__SxFoo_class = internal global ptr null -@OS = internal global i64 0 -@ARCH = internal global i64 0 +@OS = internal global i64 6 +@ARCH = internal global i64 4 @POINTER_SIZE = internal global i64 8 @__sx_default_context = internal global { { ptr, ptr, ptr }, ptr } { { ptr, ptr, ptr } { ptr null, ptr @__thunk_CAllocator_Allocator_alloc, ptr @__thunk_CAllocator_Allocator_dealloc }, ptr null } @__sx_objc_cstr_dealloc = internal global [8 x i8] c"dealloc\00" diff --git a/examples/expected/1319-ffi-objc-property-sx-defined.ir b/examples/expected/1319-ffi-objc-property-sx-defined.ir index 4e94c05..d54311f 100644 --- a/examples/expected/1319-ffi-objc-property-sx-defined.ir +++ b/examples/expected/1319-ffi-objc-property-sx-defined.ir @@ -1,8 +1,8 @@ @__SxBox_state_ivar = internal global ptr null @__SxBox_class = internal global ptr null -@OS = internal global i64 0 -@ARCH = internal global i64 0 +@OS = internal global i64 6 +@ARCH = internal global i64 4 @POINTER_SIZE = internal global i64 8 @__sx_default_context = internal global { { ptr, ptr, ptr }, ptr } { { ptr, ptr, ptr } { ptr null, ptr @__thunk_CAllocator_Allocator_alloc, ptr @__thunk_CAllocator_Allocator_dealloc }, ptr null } @OBJC_CLASSLIST_REFERENCES_SxBox = internal global ptr null diff --git a/examples/expected/1329-ffi-objc-call-03-selector-sharing.ir b/examples/expected/1329-ffi-objc-call-03-selector-sharing.ir index d5c6860..1a5385c 100644 --- a/examples/expected/1329-ffi-objc-call-03-selector-sharing.ir +++ b/examples/expected/1329-ffi-objc-call-03-selector-sharing.ir @@ -1,6 +1,6 @@ -@OS = internal global i64 0 -@ARCH = internal global i64 0 +@OS = internal global i64 6 +@ARCH = internal global i64 4 @POINTER_SIZE = internal global i64 8 @__sx_default_context = internal global { { ptr, ptr, ptr }, ptr } { { ptr, ptr, ptr } { ptr null, ptr @__thunk_CAllocator_Allocator_alloc, ptr @__thunk_CAllocator_Allocator_dealloc }, ptr null } @OBJC_SELECTOR_REFERENCES_init = internal global ptr null diff --git a/examples/expected/1332-ffi-objc-call-06-sret-return.ir b/examples/expected/1332-ffi-objc-call-06-sret-return.ir index cadb7e6..9420bf0 100644 --- a/examples/expected/1332-ffi-objc-call-06-sret-return.ir +++ b/examples/expected/1332-ffi-objc-call-06-sret-return.ir @@ -1,6 +1,6 @@ -@OS = internal global i64 0 -@ARCH = internal global i64 0 +@OS = internal global i64 6 +@ARCH = internal global i64 4 @POINTER_SIZE = internal global i64 8 @__sx_default_context = internal global { { ptr, ptr, ptr }, ptr } { { ptr, ptr, ptr } { ptr null, ptr @__thunk_CAllocator_Allocator_alloc, ptr @__thunk_CAllocator_Allocator_dealloc }, ptr null } @OBJC_SELECTOR_REFERENCES_tripleValue = internal global ptr null diff --git a/examples/expected/1347-ffi-objc-dsl-07-mangling-table.ir b/examples/expected/1347-ffi-objc-dsl-07-mangling-table.ir index 127943b..d6958ab 100644 --- a/examples/expected/1347-ffi-objc-dsl-07-mangling-table.ir +++ b/examples/expected/1347-ffi-objc-dsl-07-mangling-table.ir @@ -1,6 +1,6 @@ -@OS = internal global i64 0 -@ARCH = internal global i64 0 +@OS = internal global i64 6 +@ARCH = internal global i64 4 @POINTER_SIZE = internal global i64 8 @__sx_default_context = internal global { { ptr, ptr, ptr }, ptr } { { ptr, ptr, ptr } { ptr null, ptr @__thunk_CAllocator_Allocator_alloc, ptr @__thunk_CAllocator_Allocator_dealloc }, ptr null } @OBJC_SELECTOR_REFERENCES_length = internal global ptr null diff --git a/examples/expected/1425-ffi-jni-main-03-ctor.ir b/examples/expected/1425-ffi-jni-main-03-ctor.ir index 9068155..69ca251 100644 --- a/examples/expected/1425-ffi-jni-main-03-ctor.ir +++ b/examples/expected/1425-ffi-jni-main-03-ctor.ir @@ -1,6 +1,6 @@ -@OS = internal global i64 0 -@ARCH = internal global i64 0 +@OS = internal global i64 6 +@ARCH = internal global i64 4 @POINTER_SIZE = internal global i64 8 @g_held_view = internal global ptr null @__sx_default_context = internal global { { ptr, ptr, ptr }, ptr } { { ptr, ptr, ptr } { ptr null, ptr @__thunk_CAllocator_Allocator_alloc, ptr @__thunk_CAllocator_Allocator_dealloc }, ptr null } diff --git a/issues/0082-global-enum-literal-initializer-zeroes.md b/issues/0082-global-enum-literal-initializer-zeroes.md new file mode 100644 index 0000000..4f9893b --- /dev/null +++ b/issues/0082-global-enum-literal-initializer-zeroes.md @@ -0,0 +1,110 @@ +# 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 + +```sx +#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: + +```text +chosen=.red +FAIL +``` + +Expected: + +```text +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: + +```sh +zig build +zig build test +bash tests/run_examples.sh +``` diff --git a/src/ir/emit_llvm.zig b/src/ir/emit_llvm.zig index e17a91b..d7458a4 100644 --- a/src/ir/emit_llvm.zig +++ b/src/ir/emit_llvm.zig @@ -900,14 +900,16 @@ pub const LLVMEmitter = struct { .float => |v| c.LLVMConstReal(llvm_ty, v), .boolean => |v| c.LLVMConstInt(llvm_ty, @intFromBool(v), 0), .string => |sid| self.emitConstStringGlobal(self.ir_mod.types.getString(sid)), - .aggregate => |agg| self.emitConstAggregate(agg, llvm_ty), + .aggregate => |agg| self.emitConstAggregate(agg, llvm_ty, false), .vtable => c.LLVMConstNull(llvm_ty), // placeholder — initialized in initVtableGlobals after function declarations // A top-level null-pointer global (`p : *s64 = null;`) and a // zero-initialized global both emit as the all-zero constant // of the global's type (issue 0081). .null_val, .zeroinit => c.LLVMConstNull(llvm_ty), .undef => c.LLVMGetUndef(llvm_ty), - .func_ref => |fid| self.func_map.get(fid.index()) orelse c.LLVMConstNull(llvm_ty), + // func_map is empty in Pass 0 (functions are declared in + // Pass 1). Emit a placeholder and resolve in initVtableGlobals. + .func_ref => c.LLVMConstNull(llvm_ty), }; c.LLVMSetInitializer(llvm_global, init_val); } else { @@ -946,10 +948,22 @@ pub const LLVMEmitter = struct { .aggregate => |agg| { // Re-emit. The first pass in `emitGlobals` already ran, // but func_ref leaves resolved to null then (func_map - // wasn't populated yet). Now they resolve properly. - const init_val = self.emitConstAggregate(agg, llvm_ty); + // wasn't populated yet). Now they must resolve — a still- + // unresolved func_ref here is a loud diagnostic, never a + // silent null. + const init_val = self.emitConstAggregate(agg, llvm_ty, true); c.LLVMSetInitializer(llvm_global, init_val); }, + .func_ref => |fid| { + const llvm_func = self.func_map.get(fid.index()) orelse { + std.debug.print( + "error: global '{s}' references function #{d} which has no declaration\n", + .{ self.ir_mod.types.getString(global.name), fid.index() }, + ); + continue; + }; + c.LLVMSetInitializer(llvm_global, llvm_func); + }, else => continue, } } @@ -1009,7 +1023,17 @@ pub const LLVMEmitter = struct { .boolean => |v| c.LLVMConstInt(llvm_ty, @intFromBool(v), 0), .null_val => c.LLVMConstNull(llvm_ty), .void_val, .undef => c.LLVMGetUndef(llvm_ty), - .func_ref => |fid| self.func_map.get(fid.index()) orelse c.LLVMConstNull(llvm_ty), + // Comptime globals are serialized here in Pass 0, before functions + // are declared (Pass 1) and with no later re-emit. A func_ref can + // therefore never resolve to a real function pointer at this point; + // bail loudly rather than ship a silently-null function pointer. + .func_ref => |fid| blk: { + std.debug.print( + "error: comptime init of '{s}' produced a reference to function #{d}, which cannot be serialized as a static constant (function declarations are not available at global-init time)\n", + .{ global_name, fid.index() }, + ); + break :blk c.LLVMGetUndef(llvm_ty); + }, .string => |s| self.emitConstStringGlobal(s), .aggregate => |fields| self.serializeAggregateValue(fields, ty, interp, global_name), // The remaining Value variants cannot become static binary @@ -2420,7 +2444,13 @@ pub const LLVMEmitter = struct { return c.LLVMConstStructInContext(self.context, &fields, 2, 0); } - fn emitConstAggregate(self: *LLVMEmitter, agg: []const ir_inst.ConstantValue, llvm_ty: c.LLVMTypeRef) c.LLVMValueRef { + /// Serialize a constant aggregate to an LLVM constant. `require_resolved` + /// governs the func_ref leaves: in Pass 0 (`emitGlobals`) func_map is empty, + /// so func_refs are left as a transient null placeholder (`false`) and the + /// whole aggregate is re-emitted by `initVtableGlobals` after Pass 1 with + /// `true`, where any still-unresolved func_ref is a loud diagnostic — never + /// a silently-null function pointer. + fn emitConstAggregate(self: *LLVMEmitter, agg: []const ir_inst.ConstantValue, llvm_ty: c.LLVMTypeRef, require_resolved: bool) c.LLVMValueRef { const kind = c.LLVMGetTypeKind(llvm_ty); const is_struct = kind == c.LLVMStructTypeKind; const n: c_uint = @intCast(agg.len); @@ -2436,8 +2466,14 @@ pub const LLVMEmitter = struct { .float => |v| c.LLVMConstReal(elem_ty, v), .boolean => |v| c.LLVMConstInt(elem_ty, @intFromBool(v), 0), .string => |sid| self.emitConstStringGlobal(self.ir_mod.types.getString(sid)), - .aggregate => |inner| self.emitConstAggregate(inner, elem_ty), - .func_ref => |fid| self.func_map.get(fid.index()) orelse c.LLVMConstNull(elem_ty), + .aggregate => |inner| self.emitConstAggregate(inner, elem_ty, require_resolved), + .func_ref => |fid| self.func_map.get(fid.index()) orelse blk: { + if (require_resolved) std.debug.print( + "error: static initializer references function #{d} which has no declaration\n", + .{fid.index()}, + ); + break :blk c.LLVMConstNull(elem_ty); + }, // A null pointer field and a zero-initialized field both emit as // the all-zero constant of the leaf type (issue 0081). .null_val, .zeroinit => c.LLVMConstNull(elem_ty), diff --git a/src/ir/lower.zig b/src/ir/lower.zig index fa8987a..6877407 100644 --- a/src/ir/lower.zig +++ b/src/ir/lower.zig @@ -940,11 +940,12 @@ pub const Lowering = struct { d.addFmt(.err, v.span, "global '{s}' must be initialized by a compile-time constant; '{s}' is not a usable constant here", .{ vd.name, id.name }); break :blk null; }, - // Enum-literal shorthand globals (`OS : OperatingSystem = .unknown;`) - // keep their established zero-init: it is load-bearing for - // compile-time `inline if OS == .X` in the stdlib (issue 0071 scope - // note). Carved out explicitly — not folded into a silent fallthrough. - .enum_literal => null, + // An enum-literal global (`chosen : Color = .green;`) serializes to + // the variant's tag value against the destination enum type (issue + // 0082). The compiler-injected `OS`/`ARCH` globals flow through here + // too; their runtime reads resolve via `comptime_constants`, so the + // serialized tag only affects the static initializer. + .enum_literal => |el| self.constEnumLiteral(&el, var_ty, v.span), // Any other initializer shape (`.field_access` on a const, a call, an // arithmetic expression, …) is not a static constant the compiler can // evaluate here. Diagnose loudly rather than emit a null payload that @@ -1047,10 +1048,44 @@ pub const Lowering = struct { }, .array_literal => |al| self.constArrayLiteral(al.elements, expected_ty), .struct_literal => |sl| self.constStructLiteral(&sl, expected_ty), + // An enum tag as an aggregate leaf (`[2]Color = .[.green, .blue]`, or + // an enum field inside a global struct) serializes to its tag int + // against the leaf's declared enum type (issue 0082). + .enum_literal => |el| self.constEnumLiteral(&el, expected_ty, expr.span), else => null, }; } + /// Serialize an enum-literal initializer (`.Variant`) into a static + /// `ConstantValue.int` holding the variant's tag value, resolved against the + /// destination enum type `ty`. The tag respects explicit variant values + /// (`enum { a; b :: 5; }`); the enum's backing width is applied by the + /// const emitters via the destination type's LLVM type. Plain enums only — + /// a tagged-union or non-enum destination is diagnosed loudly rather than + /// silently zero-initialized (issue 0082). + fn constEnumLiteral(self: *Lowering, el: *const ast.EnumLiteral, ty: TypeId, span: ast.Span) ?inst_mod.ConstantValue { + if (!ty.isBuiltin()) { + const info = self.module.types.get(ty); + if (info == .@"enum") { + const e = info.@"enum"; + const name_id = self.module.types.internString(el.name); + for (e.variants, 0..) |variant, i| { + if (variant != name_id) continue; + if (e.explicit_values) |vals| { + if (i < vals.len) return .{ .int = vals[i] }; + } + return .{ .int = @intCast(i) }; + } + if (self.diagnostics) |d| + d.addFmt(.err, span, "'.{s}' is not a variant of enum '{s}'", .{ el.name, self.module.types.getString(e.name) }); + return null; + } + } + if (self.diagnostics) |d| + d.addFmt(.err, span, "enum-literal global initializer '.{s}' is only supported for a plain enum destination type", .{el.name}); + return null; + } + /// Try to convert a struct literal into a compile-time ConstantValue.aggregate of the /// struct's fields in declaration order, filling missing fields from the struct's /// field defaults. Returns null if any value is not constant-foldable.