Merge branch 'flow/sx-foundation/F0.3' into dist-foundation

This commit is contained in:
agra
2026-06-04 05:38:18 +03:00
44 changed files with 982 additions and 68 deletions

View File

@@ -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");
}
}

View File

@@ -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");
}
}

View File

@@ -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");
}
}

View File

@@ -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");
}
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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

View File

@@ -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

View File

@@ -0,0 +1 @@
0

View File

@@ -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

View File

@@ -0,0 +1 @@
0

View File

@@ -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

View File

@@ -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

View File

@@ -0,0 +1 @@
0

View File

@@ -0,0 +1 @@

View File

@@ -0,0 +1,4 @@
chosen=.green
palette=.blue,.green,.red
pair.a=.blue pair.b=.green
PASS

View File

@@ -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 }

View File

@@ -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() } ];
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

View File

@@ -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;
| ^^^^^^^

View File

@@ -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)

View File

@@ -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"

View File

@@ -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"

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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 }

View File

@@ -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 <file>` → 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.

View File

@@ -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 <file>` 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
```

View File

@@ -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
```

View File

@@ -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
```

View File

@@ -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) {

View File

@@ -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