diff --git a/examples/0136-types-global-array-element-store.sx b/examples/0136-types-global-array-element-store.sx new file mode 100644 index 0000000..1f41eb7 --- /dev/null +++ b/examples/0136-types-global-array-element-store.sx @@ -0,0 +1,57 @@ +// A store to a module-global array element writes the global's live storage, +// so a subsequent read sees the stored value — not the array initializer. +// Covers constant index, variable index, and a cross-function store, on a +// scalar global array, a struct-element global array (element-stride), and a +// nested-array global (recursive lvalue). +// Regression (issue 0079): global-array element stores were silently dropped +// (read returned the initializer) because the indexed lvalue base loaded the +// global by value into a temp instead of addressing the global's storage. + +#import "modules/std.sx"; + +g : [3]s64 = .[10, 20, 30]; + +Pair :: struct { a: s64; b: s64; } +gp : [2]Pair = .[ .{ a = 1, b = 2 }, .{ a = 3, b = 4 } ]; + +grid : [2][3]s64 = .[ .[0, 0, 0], .[0, 0, 0] ]; + +write_global :: (i: s64, v: s64) { g[i] = v; } + +main :: () { + // Scalar global array — const index. + g[1] = 222; + print("g[1]={}\n", g[1]); // 222 + + // Scalar global array — variable index. + k := 2; + g[k] = 333; + print("g[k]={}\n", g[k]); // 333 + + // Scalar global array — store from another function. + write_global(0, 111); + print("g[0]={}\n", g[0]); // 111 + + // Struct-element global array (16-byte stride) — const and var index. + gp[0] = .{ a = 10, b = 20 }; + j := 1; + gp[j] = .{ a = 30, b = 40 }; + print("gp[0]={},{}\n", gp[0].a, gp[0].b); // 10,20 + print("gp[j]={},{}\n", gp[j].a, gp[j].b); // 30,40 + + // Nested-array global — element is [3]s64, recursive indexed lvalue. + grid[1][2] = 7; + r := 0; + grid[r][0] = 5; + print("grid[1][2]={}\n", grid[1][2]); // 7 + print("grid[0][0]={}\n", grid[r][0]); // 5 + + if g[1] == 222 and g[2] == 333 and g[0] == 111 + and gp[0].a == 10 and gp[0].b == 20 + and gp[1].a == 30 and gp[1].b == 40 + and grid[1][2] == 7 and grid[0][0] == 5 { + print("PASS\n"); + } else { + print("FAIL: global array element store dropped\n"); + } +} diff --git a/examples/0137-types-global-aggregate-literal-init.sx b/examples/0137-types-global-aggregate-literal-init.sx new file mode 100644 index 0000000..8987220 --- /dev/null +++ b/examples/0137-types-global-aggregate-literal-init.sx @@ -0,0 +1,49 @@ +// A module-global aggregate (array of struct literals, a struct literal, and +// nested array/struct shapes) materializes its DECLARED field values into the +// global's static initializer, so reading the fields without any prior store +// returns the literal values — not zero. +// Regression (issue 0080): a global `[N]Struct` initialized with struct literals +// was emitted as `zeroinitializer`, silently dropping every field, because the +// constant-aggregate serializer had no struct-literal arm and collapsed the +// whole initializer to null. The fix threads the element/field type so struct +// and nested-array leaves serialize correctly; a genuinely non-constant +// initializer is now rejected loudly instead of silently zeroed. + +#import "modules/std.sx"; + +Pair :: struct { a: s64; b: s64; } +WithArr :: struct { id: s64; xs: [3]s64; } + +// global array of struct literals +pairs : [2]Pair = .[ .{ a = 1, b = 2 }, .{ a = 3, b = 4 } ]; +// global struct literal +solo : Pair = .{ a = 7, b = 9 }; +// global struct containing a fixed array (struct-with-array) +wa : WithArr = .{ id = 5, xs = .[ 11, 22, 33 ] }; +// nested: global array of structs each containing an array +nested : [2]WithArr = .[ .{ id = 1, xs = .[ 1, 2, 3 ] }, .{ id = 2, xs = .[ 4, 5, 6 ] } ]; + +main :: () { + // Read the declared initializer values back with NO prior store. + print("pairs={},{} {},{}\n", pairs[0].a, pairs[0].b, pairs[1].a, pairs[1].b); + print("solo={},{}\n", solo.a, solo.b); + print("wa={} xs={},{},{}\n", wa.id, wa.xs[0], wa.xs[1], wa.xs[2]); + print("nested0={} xs={},{},{}\n", nested[0].id, nested[0].xs[0], nested[0].xs[1], nested[0].xs[2]); + print("nested1={} xs={},{},{}\n", nested[1].id, nested[1].xs[0], nested[1].xs[1], nested[1].xs[2]); + + // A store on top of the materialized initializer still works (live storage). + pairs[0].a = 100; + nested[1].xs[2] = 999; + print("after-store={} {}\n", pairs[0].a, nested[1].xs[2]); + + if pairs[0].b == 2 and pairs[1].a == 3 and pairs[1].b == 4 + and solo.a == 7 and solo.b == 9 + and wa.id == 5 and wa.xs[0] == 11 and wa.xs[2] == 33 + and nested[0].id == 1 and nested[0].xs[0] == 1 and nested[0].xs[2] == 3 + and nested[1].id == 2 and nested[1].xs[0] == 4 + and pairs[0].a == 100 and nested[1].xs[2] == 999 { + print("PASS\n"); + } else { + print("FAIL: global aggregate literal initializer zeroed\n"); + } +} diff --git a/examples/0138-types-global-aggregate-null-pointer-field.sx b/examples/0138-types-global-aggregate-null-pointer-field.sx new file mode 100644 index 0000000..a292e57 --- /dev/null +++ b/examples/0138-types-global-aggregate-null-pointer-field.sx @@ -0,0 +1,48 @@ +// A module-global aggregate initializer may carry `null` in a pointer field: +// `null` is a compile-time constant (the zero pointer), so the field reads back +// as null with NO prior store, and its non-pointer neighbors keep their declared +// values. Covered shapes: an array-of-struct with a null pointer field, a global +// array of all-null pointers, and a nested struct-in-struct with a null pointer. +// Regression (issue 0081): the constant-aggregate serializer had no +// `.null_literal` arm, so a `null` in a pointer field made the whole aggregate +// look non-constant and the global was rejected with "must be initialized by a +// compile-time constant". The fix serializes a null literal to a constant zero +// pointer (the same way a top-level pointer global `p : *s64 = null;` does) +// while still rejecting genuinely non-constant fields (see diagnostics 1126). + +#import "modules/std.sx"; + +Box :: struct { p: *s64; marker: s64; } +Inner :: struct { q: *s64; tag: s64; } +Outer :: struct { inner: Inner; label: s64; } + +// array-of-struct with null pointer fields + scalar neighbors +boxes : [2]Box = .[ .{ p = null, marker = 11 }, .{ p = null, marker = 22 } ]; +// global array of all-null pointers +ptrs : [3]*s64 = .[ null, null, null ]; +// nested: struct containing a struct with a null pointer field +nested : [2]Outer = .[ + .{ inner = .{ q = null, tag = 1 }, label = 100 }, + .{ inner = .{ q = null, tag = 2 }, label = 200 }, +]; + +main :: () { + print("boxes ptrs={},{} markers={},{}\n", + boxes[0].p == null, boxes[1].p == null, boxes[0].marker, boxes[1].marker); + print("ptr arr nulls={},{},{}\n", ptrs[0] == null, ptrs[1] == null, ptrs[2] == null); + print("nested q nulls={},{} tags={},{} labels={},{}\n", + nested[0].inner.q == null, nested[1].inner.q == null, + nested[0].inner.tag, nested[1].inner.tag, + nested[0].label, nested[1].label); + + if boxes[0].p == null and boxes[1].p == null + and boxes[0].marker == 11 and boxes[1].marker == 22 + and ptrs[0] == null and ptrs[1] == null and ptrs[2] == null + and nested[0].inner.q == null and nested[1].inner.q == null + and nested[0].inner.tag == 1 and nested[1].inner.tag == 2 + and nested[0].label == 100 and nested[1].label == 200 { + print("PASS\n"); + } else { + print("FAIL: global aggregate null pointer field mis-serialized\n"); + } +} 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/1126-diagnostics-global-aggregate-non-const-field-rejected.sx b/examples/1126-diagnostics-global-aggregate-non-const-field-rejected.sx new file mode 100644 index 0000000..d2dd9e2 --- /dev/null +++ b/examples/1126-diagnostics-global-aggregate-non-const-field-rejected.sx @@ -0,0 +1,21 @@ +// A module-global aggregate with a NULL pointer field is fine (null is a +// compile-time constant), but a sibling field initialized from a NON-constant +// expression (here a runtime function call) must still be rejected loudly. The +// presence of an accepted `null` must NOT widen the gate to admit the +// non-constant neighbor. +// Regression (issue 0081): the null-pointer fix must not regress the +// reject-loud behavior for genuinely non-constant initializers (issues +// 0072/0080). Expected: "global 'boxes' must be initialized by a compile-time +// constant"; exit 1. + +#import "modules/std.sx"; + +runtime_marker :: () -> s64 { return 7; } + +Box :: struct { p: *s64; marker: s64; } +boxes : [1]Box = .[ .{ p = null, marker = runtime_marker() } ]; + +main :: () -> s32 { + print("marker={}\n", boxes[0].marker); + return 0; +} 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/1128-diagnostics-comptime-global-funcref-rejected.sx b/examples/1128-diagnostics-comptime-global-funcref-rejected.sx new file mode 100644 index 0000000..38be1aa --- /dev/null +++ b/examples/1128-diagnostics-comptime-global-funcref-rejected.sx @@ -0,0 +1,26 @@ +// A comptime `#run` global initializer that yields a function reference cannot +// be serialized to a static constant: at global-init time (Pass 0) functions +// are not yet declared, and the comptime serialization path has no later +// re-emit, so the func_ref can never resolve to a real function pointer. The +// compiler must reject this with a diagnostic AND a CLEAN non-zero exit — never +// print the error and then fall through into an undef initializer that crashes +// (pre-fix: the diagnostic printed, emission continued, and the JIT segfaulted +// calling through the undef pointer → exit 134). +// Regression (issue 0079 follow-up): every global-init serialization bail now +// routes through `failGlobalInit`, which sets the halt flag so the driver aborts +// after emit() instead of shipping the placeholder. +// Expected: "comptime init of 'fp' produced a reference to function 'add'…"; +// exit 1, no segfault. + +#import "modules/std.sx"; + +add :: (a: s32, b: s32) -> s32 { a + b } + +pick :: () -> (s32, s32) -> s32 { return add; } + +fp :: #run pick(); + +main :: () -> s32 { + print("{}\n", fp(3, 4)); + 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/0136-types-global-array-element-store.exit b/examples/expected/0136-types-global-array-element-store.exit new file mode 100644 index 0000000..573541a --- /dev/null +++ b/examples/expected/0136-types-global-array-element-store.exit @@ -0,0 +1 @@ +0 diff --git a/examples/expected/0136-types-global-array-element-store.stderr b/examples/expected/0136-types-global-array-element-store.stderr new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/examples/expected/0136-types-global-array-element-store.stderr @@ -0,0 +1 @@ + diff --git a/examples/expected/0136-types-global-array-element-store.stdout b/examples/expected/0136-types-global-array-element-store.stdout new file mode 100644 index 0000000..9388ff7 --- /dev/null +++ b/examples/expected/0136-types-global-array-element-store.stdout @@ -0,0 +1,8 @@ +g[1]=222 +g[k]=333 +g[0]=111 +gp[0]=10,20 +gp[j]=30,40 +grid[1][2]=7 +grid[0][0]=5 +PASS diff --git a/examples/expected/0137-types-global-aggregate-literal-init.exit b/examples/expected/0137-types-global-aggregate-literal-init.exit new file mode 100644 index 0000000..573541a --- /dev/null +++ b/examples/expected/0137-types-global-aggregate-literal-init.exit @@ -0,0 +1 @@ +0 diff --git a/examples/expected/0137-types-global-aggregate-literal-init.stderr b/examples/expected/0137-types-global-aggregate-literal-init.stderr new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/examples/expected/0137-types-global-aggregate-literal-init.stderr @@ -0,0 +1 @@ + diff --git a/examples/expected/0137-types-global-aggregate-literal-init.stdout b/examples/expected/0137-types-global-aggregate-literal-init.stdout new file mode 100644 index 0000000..02dc394 --- /dev/null +++ b/examples/expected/0137-types-global-aggregate-literal-init.stdout @@ -0,0 +1,7 @@ +pairs=1,2 3,4 +solo=7,9 +wa=5 xs=11,22,33 +nested0=1 xs=1,2,3 +nested1=2 xs=4,5,6 +after-store=100 999 +PASS diff --git a/examples/expected/0138-types-global-aggregate-null-pointer-field.exit b/examples/expected/0138-types-global-aggregate-null-pointer-field.exit new file mode 100644 index 0000000..573541a --- /dev/null +++ b/examples/expected/0138-types-global-aggregate-null-pointer-field.exit @@ -0,0 +1 @@ +0 diff --git a/examples/expected/0138-types-global-aggregate-null-pointer-field.stderr b/examples/expected/0138-types-global-aggregate-null-pointer-field.stderr new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/examples/expected/0138-types-global-aggregate-null-pointer-field.stderr @@ -0,0 +1 @@ + diff --git a/examples/expected/0138-types-global-aggregate-null-pointer-field.stdout b/examples/expected/0138-types-global-aggregate-null-pointer-field.stdout new file mode 100644 index 0000000..972574d --- /dev/null +++ b/examples/expected/0138-types-global-aggregate-null-pointer-field.stdout @@ -0,0 +1,4 @@ +boxes ptrs=true,true markers=11,22 +ptr arr nulls=true,true,true +nested q nulls=true,true tags=1,2 labels=100,200 +PASS 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/1126-diagnostics-global-aggregate-non-const-field-rejected.exit b/examples/expected/1126-diagnostics-global-aggregate-non-const-field-rejected.exit new file mode 100644 index 0000000..d00491f --- /dev/null +++ b/examples/expected/1126-diagnostics-global-aggregate-non-const-field-rejected.exit @@ -0,0 +1 @@ +1 diff --git a/examples/expected/1126-diagnostics-global-aggregate-non-const-field-rejected.stderr b/examples/expected/1126-diagnostics-global-aggregate-non-const-field-rejected.stderr new file mode 100644 index 0000000..b2df99e --- /dev/null +++ b/examples/expected/1126-diagnostics-global-aggregate-non-const-field-rejected.stderr @@ -0,0 +1,5 @@ +error: global 'boxes' must be initialized by a compile-time constant + --> examples/1126-diagnostics-global-aggregate-non-const-field-rejected.sx:16:18 + | +16 | boxes : [1]Box = .[ .{ p = null, marker = runtime_marker() } ]; + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/examples/expected/1126-diagnostics-global-aggregate-non-const-field-rejected.stdout b/examples/expected/1126-diagnostics-global-aggregate-non-const-field-rejected.stdout new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/examples/expected/1126-diagnostics-global-aggregate-non-const-field-rejected.stdout @@ -0,0 +1 @@ + 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/1128-diagnostics-comptime-global-funcref-rejected.exit b/examples/expected/1128-diagnostics-comptime-global-funcref-rejected.exit new file mode 100644 index 0000000..d00491f --- /dev/null +++ b/examples/expected/1128-diagnostics-comptime-global-funcref-rejected.exit @@ -0,0 +1 @@ +1 diff --git a/examples/expected/1128-diagnostics-comptime-global-funcref-rejected.stderr b/examples/expected/1128-diagnostics-comptime-global-funcref-rejected.stderr new file mode 100644 index 0000000..dea1cc8 --- /dev/null +++ b/examples/expected/1128-diagnostics-comptime-global-funcref-rejected.stderr @@ -0,0 +1 @@ +error: comptime init of 'fp' produced a reference to function 'add', which cannot be serialized as a static constant (function declarations are not available at global-init time) diff --git a/examples/expected/1128-diagnostics-comptime-global-funcref-rejected.stdout b/examples/expected/1128-diagnostics-comptime-global-funcref-rejected.stdout new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/examples/expected/1128-diagnostics-comptime-global-funcref-rejected.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/0079-global-array-element-store-dropped.md b/issues/0079-global-array-element-store-dropped.md new file mode 100644 index 0000000..e6d6ee7 --- /dev/null +++ b/issues/0079-global-array-element-store-dropped.md @@ -0,0 +1,102 @@ +# 0079 — stores to module-global array elements are silently dropped + +> **RESOLVED.** +> **Root cause:** `Lowering.lowerExprAsPtr` (`src/ir/lower.zig`) — the lvalue/ +> address path — handled only *local* identifiers (alloca pointers). A +> module-global identifier fell through to the value fallback `lowerExpr`, +> which emits `global_get` (loads the whole array *by value*). The LLVM +> backend's `emitIndexGep` then sees an array *value*, allocas a throwaway +> temp, copies the value in, and GEPs into the temp — so `g[i] = v` wrote a +> discarded copy and a later `g[i]` read the global's untouched initializer. +> Local arrays worked because they hit the alloca-pointer path; global +> *scalar* stores worked via `global_set`. +> **Fix:** teach `lowerExprAsPtr`'s identifier arm about globals — emit +> `global_addr` (a pointer into the global's live storage) for a normal +> global, or `global_get` for a pointer-typed global (mirroring the local +> pointer case). The same array-base resolution in the `address_of(index_expr)` +> path now routes through `lowerExprAsPtr` so `&g[i]` is also an lvalue into +> the global. `index_gep` then GEPs directly into `@g` for const AND variable +> index, across functions, in both `sx run` and `sx build`. +> **Regression:** `examples/0136-types-global-array-element-store.sx` +> (const-index, var-index, cross-function store on a scalar global array; a +> struct-element global array for element-stride; a nested-array global for the +> recursive indexed lvalue). FAILS on the pre-fix compiler (reads return the +> initializer / zero), PASSES after. + +## Symptom + +A store to a module-global (file-scope) ARRAY element is silently lost: after +`g[i] = v`, reading `g[i]` returns the array's INITIALIZER value, not `v`. No +diagnostic. Reproduces with a constant index, a variable index, and a store from +another function, in BOTH `sx run` (JIT) and `sx build` (AOT). Local-array stores +and global-SCALAR stores work correctly — so the indexed load/store on a *global* +array appears to read/write the initializer constant rather than the global's +live storage. This is a SILENT data-corruption bug (the dangerous class per the +project rules), not a crash. + +Manager-reproduced output (JIT): +``` +global[1] const-idx=20 (want 222) +global[k] var-idx=30 (want 333) +global[0] via fn=10 (want 111) +``` + +## Reproduction + +```sx +#import "modules/std.sx"; + +g : [3]s64 = .[10, 20, 30]; +write_global :: (i: s64, v: s64) { g[i] = v; } + +main :: () { + loc : [3]s64 = .[10, 20, 30]; + loc[1] = 222; + print("local[1]={}\n", loc[1]); // 222 (correct) + g[1] = 222; + print("global[1] const-idx={}\n", g[1]); // 20 (WRONG, want 222) + k := 2; + g[k] = 333; + print("global[k] var-idx={}\n", g[k]); // 30 (WRONG, want 333) + write_global(0, 111); + print("global[0] via fn={}\n", g[0]); // 10 (WRONG, want 111) +} +``` + +`./zig-out/bin/sx run ` → prints the WRONG values above. Expected: +`local[1]=222 / global[1]=222 / global[k]=333 / global[0]=111`. + +## Investigation prompt + +A store to a module-global array element (`g[i] = v`) is silently lost: a +subsequent `g[i]` yields the initializer value, in BOTH the JIT (`sx run`) and +compiled (`sx build`) paths. Global SCALAR stores and LOCAL array stores both +work, so the defect is specific to INDEXED lvalue access on a GLOBAL array. +Suspect the IR-gen / codegen for an indexed expression on a global symbol: +the load of `global_array[idx]` is likely decaying / constant-folding to the +array's initializer constant (or the address computation for the store targets a +private copy of the initializer rather than the global's live storage). Look in +the index-expression lowering and global-symbol address resolution (and how +global array initializers are materialized) — `src/ir/lower.zig` / +`src/ir/emit_llvm.zig` and the global-symbol/constant-initializer paths. + +The fix must make `load(global_array[idx])` read the global's live storage and +`store(global_array[idx], v)` write it — for constant AND variable indices, and +when the store happens in a different function than the read. Do NOT special-case; +fix the address resolution so a global array element is an lvalue into the +global's storage like any other. + +## Verification (once fixed) +- The repro above prints `222 / 333 / 111` for the global cases (exit 0). +- Add a pinned regression `examples/NNNN-*.sx` covering const-index, var-index, + and cross-function store to a global array (+ a global array of a struct/larger + element if practical). +- `zig build && zig build test && bash tests/run_examples.sh` all green. + +## Provenance + +Discovered by the `distribution` flow (sx-foundation step F3.1, std.cli argv +accessor) while exploring argv backing strategies. The shipped F3.1 code does NOT +depend on this (it uses caller-provided buffers + zero-copy views over the C argv +block), so F3.1 is correct independently — this is a separate latent +silent-miscompile surfaced per the STOP-on-compiler-bug rule. diff --git a/issues/0080-global-array-struct-literal-initializer-zero.md b/issues/0080-global-array-struct-literal-initializer-zero.md new file mode 100644 index 0000000..e5dddb7 --- /dev/null +++ b/issues/0080-global-array-struct-literal-initializer-zero.md @@ -0,0 +1,121 @@ +# 0080 - global array of struct literals silently zero-initializes + +> **RESOLVED.** +> **Root cause:** `Lowering.constExprValue` (`src/ir/lower.zig`) — the constant- +> aggregate serializer for global initializers — handled primitive and nested- +> array leaves but had **no `.struct_literal` arm**. A module-global `[N]Struct` +> initialized with struct literals reached `constArrayLiteral` → `constExprValue` +> per element; each struct-literal element returned `null`, collapsing the whole +> array initializer to `null`. `globalInitValue` then emitted no payload, so the +> LLVM backend zero-initialized the global (`@pairs = ... zeroinitializer`), +> silently dropping every declared field — the same silent-zero class as +> 0071/0072, one level inside an array literal. (A global *struct* literal and a +> *struct-with-array* already worked, because `constStructLiteral` existed and was +> reached directly; the gap was specifically struct literals *as array elements*.) +> **Fix:** make `constExprValue` type-aware — thread the destination element/field +> `TypeId` so a `.struct_literal` leaf routes through `constStructLiteral` and a +> nested `.array_literal` through `constArrayLiteral` with the correct element +> type. `constArrayLiteral` derives its element type from the array `TypeId`; +> `constStructLiteral` passes each field's type. A global aggregate initializer +> that still does not fully reduce to a compile-time constant is now **rejected +> loudly** (`diagnoseNonConstGlobal`) instead of falling through to a zeroed +> global. The downstream `emitConstAggregate` already recurses over nested +> aggregates, so const/AOT (`sx build`) and JIT (`sx run`) both materialize the +> declared values. +> **Regression:** `examples/0137-types-global-aggregate-literal-init.sx` (global +> `[N]Struct` literal, global struct literal, struct-with-array, nested array-of- +> struct-with-array; values read back with no prior store, plus a store on top). +> FAILS on the pre-fix compiler (array-of-struct fields read 0), PASSES after. + +## Symptom + +A module-global fixed array whose elements are struct literals is emitted as +zero-initialized storage instead of preserving the literal fields. + +Observed: reading `pairs[0].b` and `pairs[1].a` prints `0`. +Expected: the global should contain the declared struct literal values +(`2` and `3`), or the compiler should reject the initializer loudly if this +constant shape is unsupported. + +## Reproduction + +```sx +#import "modules/std.sx"; + +Pair :: struct { + a: s64; + b: s64; +} + +pairs : [2]Pair = .[ .{ a = 1, b = 2 }, .{ a = 3, b = 4 } ]; + +main :: () -> s32 { + print("pairs[0]={},{}\n", pairs[0].a, pairs[0].b); + print("pairs[1]={},{}\n", pairs[1].a, pairs[1].b); + if pairs[0].a == 1 and pairs[0].b == 2 and pairs[1].a == 3 and pairs[1].b == 4 { + print("PASS\n"); + return 0; + } + print("FAIL: global array struct literal initializer zeroed\n"); + return 1; +} +``` + +On the current compiler this prints: + +```text +pairs[0]=0,0 +pairs[1]=0,0 +FAIL: global array struct literal initializer zeroed +``` + +`sx ir ` shows the global as: + +```llvm +@pairs = internal global [2 x { i64, i64 }] zeroinitializer +``` + +## Investigation prompt + +Fix issue 0080: a module-global array initialized with struct literal elements +silently becomes `zeroinitializer`. + +Suspected area: +- `src/ir/lower.zig`, `Lowering.globalInitValue`. +- `src/ir/lower.zig`, `Lowering.constArrayLiteral`. +- `src/ir/lower.zig`, `Lowering.constExprValue`. +- `src/ir/lower.zig`, `Lowering.constStructLiteral`. + +Likely root cause: `globalInitValue` handles a top-level `.array_literal` by +calling `constArrayLiteral`, and `constArrayLiteral` serializes each element via +`constExprValue`. `constExprValue` handles primitive literals and nested arrays, +but not `.struct_literal`, so an array whose element is a struct literal returns +`null`. That null initializer payload is later emitted as zero-initialized +storage, recreating the silent zero pattern from issues 0071/0072 one level +inside an otherwise-supported array literal. + +Likely fix: +- Thread the expected element `TypeId` into `constArrayLiteral`, or otherwise + make `constExprValue` type-aware for struct literals. +- Serialize each struct element through `constStructLiteral` with the array's + element type. +- If any element shape is still unsupported, emit a diagnostic naming the global + instead of returning `null` and allowing zero-initialization. + +Verification: +- Run the repro above and expect: + +```text +pairs[0]=1,2 +pairs[1]=3,4 +PASS +``` + +- Add a pinned regression in the `01xx` types block. +- Run: + +```sh +zig build +zig build test +bash tests/run_examples.sh +``` diff --git a/issues/0081-global-aggregate-null-literal-rejected.md b/issues/0081-global-aggregate-null-literal-rejected.md new file mode 100644 index 0000000..863f9d5 --- /dev/null +++ b/issues/0081-global-aggregate-null-literal-rejected.md @@ -0,0 +1,116 @@ +# 0081 - global aggregate null literal rejected as non-constant + +> **RESOLVED.** +> **Root cause:** `Lowering.constExprValue` (`src/ir/lower.zig`) — the +> constant-aggregate serializer for global initializers — had no +> `.null_literal` arm. A `null` in a pointer (or optional-pointer) field +> therefore returned no constant, which propagated up through +> `constStructLiteral` / `constArrayLiteral` and made the whole aggregate look +> non-constant, so `globalInitValue` rejected it with "must be initialized by a +> compile-time constant". A `null` is a compile-time constant (the zero +> pointer) and a top-level scalar pointer global (`p : *s64 = null;`) already +> serialized fine — only the nested-aggregate path was wrong. +> **Fix:** add `.null_literal => .null_val` to `constExprValue` so a null leaf +> serializes to a constant zero pointer. Made the LLVM constant emitters +> exhaustive while at it: `emitConstAggregate` and the top-level `init_val` +> switch in `src/ir/emit_llvm.zig` previously ended in a silent +> `else => LLVMConstNull(...)` catch-all (the precise silent-arm class CLAUDE.md +> mandates rooting out); they now handle every `ConstantValue` tag explicitly +> (`.null_val`/`.zeroinit` → all-zero constant, `.undef` → `LLVMGetUndef`, +> `.func_ref` resolved, nested `.vtable` is a hard `@panic` tripwire since +> vtables are top-level-only). The reject-loud path for genuinely non-constant +> fields (a runtime call, etc.) is preserved. +> **Regression:** `examples/0138-types-global-aggregate-null-pointer-field.sx` +> (array-of-struct with null pointer fields, global array of all-null pointers, +> nested struct-in-struct null pointer — asserts null reads + correct neighbors) +> and the negative `examples/1126-diagnostics-global-aggregate-non-const-field-rejected.sx` +> (a null pointer field beside a non-constant field still errors loudly). +> Verified fail-before (pre-fix rejects 0138) / pass-after. + +## Symptom + +A module-global aggregate initializer rejects a `null` literal in a pointer +field as "not a compile-time constant"; expected the null pointer to serialize +as a constant zero pointer the same way a top-level pointer global does. + +## Reproduction + +```sx +#import "modules/std.sx"; + +Box :: struct { + p: *s64; + marker: s64; +} + +boxes : [2]Box = .[ + .{ p = null, marker = 11 }, + .{ p = null, marker = 22 }, +]; + +main :: () -> s32 { + print("ptrs={} {} markers={} {}\n", + boxes[0].p == null, + boxes[1].p == null, + boxes[0].marker, + boxes[1].marker); + if boxes[0].p == null and boxes[1].p == null and boxes[0].marker == 11 and boxes[1].marker == 22 { + return 0; + } + return 1; +} +``` + +Observed: + +```text +error: global 'boxes' must be initialized by a compile-time constant +``` + +Expected: + +```text +ptrs=true true markers=11 22 +``` + +## Investigation prompt + +Fix issue 0081: module-global aggregate initializers reject `null` literals in +pointer fields even though `null` is a compile-time constant pointer value. + +Suspected area: +- `src/ir/lower.zig`, `Lowering.constExprValue` — the switch has no + `.null_literal` arm, so `constStructLiteral` treats a pointer field initialized + with `null` as non-constant and `globalInitValue` reports the whole aggregate. +- `src/ir/emit_llvm.zig`, top-level `global.init_val` emission and + `LLVMEmitter.emitConstAggregate` — both currently rely on catch-all + `else => LLVMConstNull(...)` for several `ConstantValue` tags. If `.null_val` + is threaded through aggregate constants, add explicit `.null_val` handling + there (and explicit `.zeroinit` / `.undef` handling as appropriate) rather + than depending on the catch-all. + +Likely fix: +- Add `.null_literal => .null_val` to `constExprValue` for constant aggregate + serialization. +- Ensure LLVM constant emission handles `.null_val` explicitly for both + top-level constants and nested aggregate leaves. +- Keep unsupported aggregate expressions loud: non-constant calls/field-accesses + should still diagnose instead of zero-initializing. + +Verification: +- Run the repro above and expect: + +```text +ptrs=true true markers=11 22 +``` + +- Add a pinned regression in the `01xx` types block covering a global + array-of-struct with pointer-null fields (and, if straightforward, optional + null fields too). +- Run: + +```sh +zig build +zig build test +bash tests/run_examples.sh +``` 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 e246e59..508ef13 100644 --- a/src/ir/emit_llvm.zig +++ b/src/ir/emit_llvm.zig @@ -106,8 +106,11 @@ pub const LLVMEmitter = struct { // IR Module being emitted ir_mod: *const Module, - // Set when a comptime `#run` raised an unhandled error (E5.2). The driver - // (core.generateCode) aborts with a non-zero exit after emit() when set. + // Set when a comptime `#run` raised an unhandled error (E5.2), or when a + // global initializer could not be serialized to a valid static constant. + // The driver (core.generateCode) aborts with a non-zero exit after emit() + // when set, so an invalid/placeholder initializer never reaches the object + // file or the JIT — the emit-time diagnostic is the surfaced error. comptime_failed: bool = false, // Allocator for temporary bookkeeping @@ -875,6 +878,7 @@ pub const LLVMEmitter = struct { const sep: []const u8 = if (detail.len > 0) ": " else ""; const gname = self.ir_mod.types.getString(global.name); std.debug.print("error: comptime init of '{s}' failed: {s} (op={s}{s}{s})\n", .{ gname, @errorName(err), op, sep, detail }); + self.comptime_failed = true; break :blk .void_val; }; // A bare failable `NAME :: #run f();`: the comptime function @@ -900,9 +904,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 - else => c.LLVMConstNull(llvm_ty), + // 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_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 { @@ -929,7 +940,14 @@ pub const LLVMEmitter = struct { defer field_vals.deinit(self.alloc); for (func_ids) |fid| { const llvm_func = self.func_map.get(fid.index()) orelse { + std.debug.print( + "error: vtable global '{s}' references function '{s}' which has no declaration\n", + .{ self.ir_mod.types.getString(global.name), self.ir_mod.types.getString(self.ir_mod.getFunction(fid).name) }, + ); + // Keep the struct shape so module construction can + // finish; comptime_failed halts before it ships. field_vals.append(self.alloc, c.LLVMConstNull(self.cached_ptr)) catch unreachable; + self.comptime_failed = true; continue; }; field_vals.append(self.alloc, llvm_func) catch unreachable; @@ -941,10 +959,23 @@ 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 '{s}' which has no declaration\n", + .{ self.ir_mod.types.getString(global.name), self.ir_mod.types.getString(self.ir_mod.getFunction(fid).name) }, + ); + self.comptime_failed = true; + continue; + }; + c.LLVMSetInitializer(llvm_global, llvm_func); + }, else => continue, } } @@ -962,6 +993,17 @@ pub const LLVMEmitter = struct { return ptr[0..len]; } + /// Record that a global initializer could not be serialized to a valid + /// static constant: set the halt flag (the driver aborts with a non-zero + /// exit after `emit()`) and return an `undef` placeholder so in-process + /// LLVM module construction can finish without tripping over an invalid + /// value before the halt is observed. The placeholder is never shipped — + /// `comptime_failed` guarantees we stop before object emission / JIT. + fn failGlobalInit(self: *LLVMEmitter, llvm_ty: c.LLVMTypeRef) c.LLVMValueRef { + self.comptime_failed = true; + return c.LLVMGetUndef(llvm_ty); + } + /// Serialize an interp `Value` to an LLVM constant for use as a static /// global initializer. `ty` is the IR-level type of the destination; /// the LLVM type is derived from it. `interp` gives access to the @@ -969,8 +1011,10 @@ pub const LLVMEmitter = struct { /// is included in any diagnostic the path produces so the user can /// locate the offending `#run` site. /// - /// Returns `LLVMGetUndef` on bail — the build continues so adjacent - /// constants can still emit, but the diagnostic makes the problem clear. + /// On bail, prints the diagnostic and routes through `failGlobalInit` + /// (sets `comptime_failed`, returns `undef`): the in-process module + /// finishes constructing, but the driver halts with a non-zero exit + /// before object emission / JIT, so the placeholder never ships. fn valueToLLVMConst( self: *LLVMEmitter, val: Value, @@ -996,7 +1040,7 @@ pub const LLVMEmitter = struct { "error: comptime init of '{s}' produced a raw integer for a pointer field — needs IR-typed heap-walk serialization (Phase 1.4a heap-walk follow-up)\n", .{global_name}, ); - break :blk c.LLVMGetUndef(llvm_ty); + break :blk self.failGlobalInit(llvm_ty); } break :blk c.LLVMConstInt(llvm_ty, @bitCast(v), 1); }, @@ -1004,7 +1048,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 '{s}', which cannot be serialized as a static constant (function declarations are not available at global-init time)\n", + .{ global_name, self.ir_mod.types.getString(self.ir_mod.getFunction(fid).name) }, + ); + break :blk self.failGlobalInit(llvm_ty); + }, .string => |s| self.emitConstStringGlobal(s), .aggregate => |fields| self.serializeAggregateValue(fields, ty, interp, global_name), // The remaining Value variants cannot become static binary @@ -1017,7 +1071,7 @@ pub const LLVMEmitter = struct { "error: comptime init of '{s}' produced a {s} value, which cannot be serialized as a static constant\n", .{ global_name, @tagName(val) }, ); - break :blk c.LLVMGetUndef(llvm_ty); + break :blk self.failGlobalInit(llvm_ty); }, }; } @@ -1054,7 +1108,7 @@ pub const LLVMEmitter = struct { "error: comptime init of '{s}' produced a fat-pointer aggregate whose len field is not an integer\n", .{global_name}, ); - return c.LLVMGetUndef(llvm_ty); + return self.failGlobalInit(llvm_ty); }; const len: usize = @intCast(len_i); @@ -1078,7 +1132,7 @@ pub const LLVMEmitter = struct { "error: comptime init of '{s}' produced a fat-pointer aggregate whose data field ({s}) cannot be resolved to {} bytes — needs Phase 1.4a heap-walk for this shape\n", .{ global_name, @tagName(data), len }, ); - return c.LLVMGetUndef(llvm_ty); + return self.failGlobalInit(llvm_ty); }; return self.emitConstStringGlobal(bytes); @@ -1094,7 +1148,7 @@ pub const LLVMEmitter = struct { "error: comptime init of '{s}' produced aggregate with {} fields but struct '{s}' expects {}\n", .{ global_name, fields.len, self.ir_mod.types.getString(info.@"struct".name), ir_fields.len }, ); - return c.LLVMGetUndef(llvm_ty); + return self.failGlobalInit(llvm_ty); } var field_vals = std.ArrayList(c.LLVMValueRef).empty; defer field_vals.deinit(self.alloc); @@ -1119,7 +1173,7 @@ pub const LLVMEmitter = struct { "error: comptime init of '{s}' produced an aggregate but the destination type ({s}) is neither struct, array, string, nor slice\n", .{ global_name, self.ir_mod.types.typeName(ty) }, ); - return c.LLVMGetUndef(llvm_ty); + return self.failGlobalInit(llvm_ty); } // ── Function declaration ──────────────────────────────────────── @@ -2415,7 +2469,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); @@ -2431,9 +2491,26 @@ 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), - else => 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 '{s}' which has no declaration\n", + .{self.ir_mod.types.getString(self.ir_mod.getFunction(fid).name)}, + ); + break :blk self.failGlobalInit(elem_ty); + } + // Pass 0 placeholder: func_map is empty until Pass 1, so the + // whole aggregate is re-emitted with require_resolved=true. + 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), + .undef => c.LLVMGetUndef(elem_ty), + // Vtable constants are only ever produced for top-level protocol + // vtable globals (lower.zig), never as a nested aggregate leaf. + .vtable => @panic("nested vtable constant in aggregate is unsupported — vtables are top-level globals only"), }; } if (is_struct) { diff --git a/src/ir/lower.zig b/src/ir/lower.zig index 6bd5d98..6877407 100644 --- a/src/ir/lower.zig +++ b/src/ir/lower.zig @@ -926,25 +926,26 @@ pub const Lowering = struct { .bool_literal => |bl| .{ .boolean = bl.value }, .float_literal => |fl| .{ .float = fl.value }, .string_literal => |sl| .{ .string = self.module.types.internString(sl.raw) }, - .array_literal => |al| self.constArrayLiteral(al.elements), - .struct_literal => |sl| self.constStructLiteral(&sl, var_ty), + .array_literal => |al| self.constArrayLiteral(al.elements, var_ty) orelse self.diagnoseNonConstGlobal(vd, v), + .struct_literal => |sl| self.constStructLiteral(&sl, var_ty) orelse self.diagnoseNonConstGlobal(vd, v), .identifier => |id| blk: { // A global initialized from a module constant copies the // constant's recorded value (typed module consts land in // `module_const_map` via `registerTypedModuleConst`, run in the // same pass-2 before this). if (self.program_index.module_const_map.get(id.name)) |ci| { - if (self.constExprValue(ci.value)) |cv| break :blk cv; + if (self.constExprValue(ci.value, var_ty)) |cv| break :blk cv; } if (self.diagnostics) |d| 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 @@ -957,6 +958,16 @@ pub const Lowering = struct { }; } + /// A global aggregate initializer (array/struct literal) that does not fully + /// reduce to a compile-time constant is rejected loudly. Without this the + /// `null` payload would fall through to a zero-initialized global, silently + /// dropping the declared fields (issues 0071/0072/0080). + fn diagnoseNonConstGlobal(self: *Lowering, vd: *const ast.VarDecl, v: *const Node) ?inst_mod.ConstantValue { + if (self.diagnostics) |d| + d.addFmt(.err, v.span, "global '{s}' must be initialized by a compile-time constant", .{vd.name}); + return null; + } + /// Resolve identifier-RHS type aliases whose target is declared LATER in the /// file. The forward scan above only registers an alias (`A :: B`) when `B` /// is already in `type_alias_map` / the `TypeTable`; a forward target isn't @@ -993,25 +1004,40 @@ pub const Lowering = struct { } } - /// Try to convert an array literal's elements into a compile-time ConstantValue.aggregate. - /// Returns null if any element is not a compile-time constant. - fn constArrayLiteral(self: *Lowering, elements: []const *const Node) ?inst_mod.ConstantValue { + /// Try to convert an array literal's elements into a compile-time + /// ConstantValue.aggregate. `array_ty` is the array's resolved TypeId; its + /// element type drives type-aware serialization of struct-literal and + /// nested-array elements. Returns null if `array_ty` is not an array type or + /// any element is not a compile-time constant. + fn constArrayLiteral(self: *Lowering, elements: []const *const Node, array_ty: TypeId) ?inst_mod.ConstantValue { + if (array_ty.isBuiltin()) return null; + const elem_ty: TypeId = switch (self.module.types.get(array_ty)) { + .array => |a| a.element, + else => return null, + }; const vals = self.alloc.alloc(inst_mod.ConstantValue, elements.len) catch return null; for (elements, 0..) |elem, i| { - vals[i] = self.constExprValue(elem) orelse return null; + vals[i] = self.constExprValue(elem, elem_ty) orelse return null; } return .{ .aggregate = vals }; } /// Try to convert a single AST expression into a compile-time ConstantValue. - /// Returns null if the expression is not constant-foldable here. - fn constExprValue(self: *Lowering, expr: *const Node) ?inst_mod.ConstantValue { + /// `expected_ty` is the destination element/field type — it lets aggregate + /// leaves (struct literals, nested arrays) serialize with the correct shape + /// rather than collapsing to null (issue 0080). Returns null if the + /// expression is not constant-foldable here. + fn constExprValue(self: *Lowering, expr: *const Node, expected_ty: TypeId) ?inst_mod.ConstantValue { return switch (expr.data) { .int_literal => |il| .{ .int = il.value }, .bool_literal => |bl| .{ .boolean = bl.value }, .float_literal => |fl| .{ .float = fl.value }, .string_literal => |sl| .{ .string = self.module.types.internString(sl.raw) }, .undef_literal => .zeroinit, + // A `null` in a pointer (or optional-pointer) field is a + // compile-time constant: the zero pointer. Without this arm the + // aggregate is wrongly rejected as non-constant (issue 0081). + .null_literal => .null_val, .unary_op => |uo| switch (uo.op) { .negate => switch (uo.operand.data) { .int_literal => |il| .{ .int = -il.value }, @@ -1020,11 +1046,46 @@ pub const Lowering = struct { }, else => null, }, - .array_literal => |al| self.constArrayLiteral(al.elements), + .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. @@ -1055,7 +1116,7 @@ pub const Lowering = struct { break :blk null; }; if (init_expr) |e| { - vals[fi] = self.constExprValue(e) orelse return null; + vals[fi] = self.constExprValue(e, sf.ty) orelse return null; } else { vals[fi] = .zeroinit; } @@ -2324,20 +2385,29 @@ pub const Lowering = struct { fn lowerExprAsPtr(self: *Lowering, node: *const Node) Ref { switch (node.data) { .identifier => |id| { - if (self.scope) |scope| { - if (scope.lookup(id.name)) |binding| { - if (binding.is_alloca) { - // If the variable IS a pointer (e.g., p: *Vec2), load it - // to get the actual pointer value for GEP/store operations - if (!binding.ty.isBuiltin()) { - const info = self.module.types.get(binding.ty); - if (info == .pointer) { - return self.builder.load(binding.ref, binding.ty); - } + const local = if (self.scope) |scope| scope.lookup(id.name) else null; + if (local) |binding| { + if (binding.is_alloca) { + // If the variable IS a pointer (e.g., p: *Vec2), load it + // to get the actual pointer value for GEP/store operations + if (!binding.ty.isBuiltin()) { + const info = self.module.types.get(binding.ty); + if (info == .pointer) { + return self.builder.load(binding.ref, binding.ty); } - return binding.ref; } + return binding.ref; } + } else if (self.program_index.global_names.get(id.name)) |gi| { + // Module-global lvalue: address into the global's live storage + // so a downstream GEP/store targets the global itself, not a + // loaded copy. A pointer-typed global is loaded first to get + // the pointer value to GEP through (mirrors the local pointer + // case above); any other global yields its storage address. + if (!gi.ty.isBuiltin() and self.module.types.get(gi.ty) == .pointer) { + return self.builder.emit(.{ .global_get = gi.id }, gi.ty); + } + return self.builder.emit(.{ .global_addr = gi.id }, self.module.types.ptrTo(gi.ty)); } }, .field_access => |fa| { @@ -2697,9 +2767,11 @@ pub const Lowering = struct { const obj_ty = self.inferExprType(ie.object); const elem_ty = self.getElementType(obj_ty); const ptr_ty = self.module.types.ptrTo(elem_ty); - // For array targets, use the alloca directly so the pointer is persistent + // For array targets, use the storage pointer (alloca for a + // local, global_addr for a module global) so the resulting + // pointer is into live storage, not a loaded copy. const is_array = !obj_ty.isBuiltin() and self.module.types.get(obj_ty) == .array; - const base = if (is_array) (self.getExprAlloca(ie.object) orelse self.lowerExpr(ie.object)) else self.lowerExpr(ie.object); + const base = if (is_array) (self.getExprAlloca(ie.object) orelse self.lowerExprAsPtr(ie.object)) else self.lowerExpr(ie.object); break :blk self.builder.emit(.{ .index_gep = .{ .lhs = base, .rhs = idx } }, ptr_ty); } // address_of(field_access) → use lowerExprAsPtr for GEP chain