Merge branch 'flow/sx-foundation/F0.3' into dist-foundation
This commit is contained in:
57
examples/0136-types-global-array-element-store.sx
Normal file
57
examples/0136-types-global-array-element-store.sx
Normal 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");
|
||||||
|
}
|
||||||
|
}
|
||||||
49
examples/0137-types-global-aggregate-literal-init.sx
Normal file
49
examples/0137-types-global-aggregate-literal-init.sx
Normal 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");
|
||||||
|
}
|
||||||
|
}
|
||||||
48
examples/0138-types-global-aggregate-null-pointer-field.sx
Normal file
48
examples/0138-types-global-aggregate-null-pointer-field.sx
Normal 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");
|
||||||
|
}
|
||||||
|
}
|
||||||
51
examples/0139-types-global-enum-literal-init.sx
Normal file
51
examples/0139-types-global-enum-literal-init.sx
Normal 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");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
16
examples/1127-diagnostics-global-enum-literal-bad-variant.sx
Normal file
16
examples/1127-diagnostics-global-enum-literal-bad-variant.sx
Normal 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;
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
|
|
||||||
@OS = internal global i64 0
|
@OS = internal global i64 6
|
||||||
@ARCH = internal global i64 0
|
@ARCH = internal global i64 4
|
||||||
@POINTER_SIZE = internal global i64 8
|
@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_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
|
@str = private unnamed_addr constant [2 x i8] c"0\00", align 1
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
|
|
||||||
@OS = internal global i64 0
|
@OS = internal global i64 6
|
||||||
@ARCH = internal global i64 0
|
@ARCH = internal global i64 4
|
||||||
@POINTER_SIZE = internal global i64 8
|
@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_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
|
@str = private unnamed_addr constant [2 x i8] c"0\00", align 1
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
0
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
|
||||||
@@ -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
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
0
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
|
||||||
@@ -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
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
0
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
|
||||||
@@ -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
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
0
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
chosen=.green
|
||||||
|
palette=.blue,.green,.red
|
||||||
|
pair.a=.blue pair.b=.green
|
||||||
|
PASS
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
|
|
||||||
@OS = internal global i64 0
|
@OS = internal global i64 6
|
||||||
@ARCH = internal global i64 0
|
@ARCH = internal global i64 4
|
||||||
@POINTER_SIZE = internal global i64 8
|
@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_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 }
|
@__Counter__SimpleCounter__vtable = internal constant { ptr, ptr } { ptr @__thunk_SimpleCounter_Counter_inc, ptr @__thunk_SimpleCounter_Counter_get }
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
1
|
||||||
@@ -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() } ];
|
||||||
|
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
1
|
||||||
@@ -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;
|
||||||
|
| ^^^^^^^
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
1
|
||||||
@@ -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)
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
|
|
||||||
@__SxFoo_state_ivar = internal global ptr null
|
@__SxFoo_state_ivar = internal global ptr null
|
||||||
@__SxFoo_class = internal global ptr null
|
@__SxFoo_class = internal global ptr null
|
||||||
@OS = internal global i64 0
|
@OS = internal global i64 6
|
||||||
@ARCH = internal global i64 0
|
@ARCH = internal global i64 4
|
||||||
@POINTER_SIZE = internal global i64 8
|
@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_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"
|
@__sx_objc_cstr_dealloc = internal global [8 x i8] c"dealloc\00"
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
|
|
||||||
@__SxFoo_state_ivar = internal global ptr null
|
@__SxFoo_state_ivar = internal global ptr null
|
||||||
@__SxFoo_class = internal global ptr null
|
@__SxFoo_class = internal global ptr null
|
||||||
@OS = internal global i64 0
|
@OS = internal global i64 6
|
||||||
@ARCH = internal global i64 0
|
@ARCH = internal global i64 4
|
||||||
@POINTER_SIZE = internal global i64 8
|
@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_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"
|
@__sx_objc_cstr_dealloc = internal global [8 x i8] c"dealloc\00"
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
|
|
||||||
@__SxBox_state_ivar = internal global ptr null
|
@__SxBox_state_ivar = internal global ptr null
|
||||||
@__SxBox_class = internal global ptr null
|
@__SxBox_class = internal global ptr null
|
||||||
@OS = internal global i64 0
|
@OS = internal global i64 6
|
||||||
@ARCH = internal global i64 0
|
@ARCH = internal global i64 4
|
||||||
@POINTER_SIZE = internal global i64 8
|
@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_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
|
@OBJC_CLASSLIST_REFERENCES_SxBox = internal global ptr null
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
|
|
||||||
@OS = internal global i64 0
|
@OS = internal global i64 6
|
||||||
@ARCH = internal global i64 0
|
@ARCH = internal global i64 4
|
||||||
@POINTER_SIZE = internal global i64 8
|
@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_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
|
@OBJC_SELECTOR_REFERENCES_init = internal global ptr null
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
|
|
||||||
@OS = internal global i64 0
|
@OS = internal global i64 6
|
||||||
@ARCH = internal global i64 0
|
@ARCH = internal global i64 4
|
||||||
@POINTER_SIZE = internal global i64 8
|
@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_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
|
@OBJC_SELECTOR_REFERENCES_tripleValue = internal global ptr null
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
|
|
||||||
@OS = internal global i64 0
|
@OS = internal global i64 6
|
||||||
@ARCH = internal global i64 0
|
@ARCH = internal global i64 4
|
||||||
@POINTER_SIZE = internal global i64 8
|
@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_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
|
@OBJC_SELECTOR_REFERENCES_length = internal global ptr null
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
|
|
||||||
@OS = internal global i64 0
|
@OS = internal global i64 6
|
||||||
@ARCH = internal global i64 0
|
@ARCH = internal global i64 4
|
||||||
@POINTER_SIZE = internal global i64 8
|
@POINTER_SIZE = internal global i64 8
|
||||||
@g_held_view = internal global ptr null
|
@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 }
|
@__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 }
|
||||||
|
|||||||
102
issues/0079-global-array-element-store-dropped.md
Normal file
102
issues/0079-global-array-element-store-dropped.md
Normal 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.
|
||||||
121
issues/0080-global-array-struct-literal-initializer-zero.md
Normal file
121
issues/0080-global-array-struct-literal-initializer-zero.md
Normal 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
|
||||||
|
```
|
||||||
116
issues/0081-global-aggregate-null-literal-rejected.md
Normal file
116
issues/0081-global-aggregate-null-literal-rejected.md
Normal 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
|
||||||
|
```
|
||||||
110
issues/0082-global-enum-literal-initializer-zeroes.md
Normal file
110
issues/0082-global-enum-literal-initializer-zeroes.md
Normal 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
|
||||||
|
```
|
||||||
@@ -106,8 +106,11 @@ pub const LLVMEmitter = struct {
|
|||||||
// IR Module being emitted
|
// IR Module being emitted
|
||||||
ir_mod: *const Module,
|
ir_mod: *const Module,
|
||||||
|
|
||||||
// Set when a comptime `#run` raised an unhandled error (E5.2). The driver
|
// Set when a comptime `#run` raised an unhandled error (E5.2), or when a
|
||||||
// (core.generateCode) aborts with a non-zero exit after emit() when set.
|
// 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,
|
comptime_failed: bool = false,
|
||||||
|
|
||||||
// Allocator for temporary bookkeeping
|
// Allocator for temporary bookkeeping
|
||||||
@@ -875,6 +878,7 @@ pub const LLVMEmitter = struct {
|
|||||||
const sep: []const u8 = if (detail.len > 0) ": " else "";
|
const sep: []const u8 = if (detail.len > 0) ": " else "";
|
||||||
const gname = self.ir_mod.types.getString(global.name);
|
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 });
|
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;
|
break :blk .void_val;
|
||||||
};
|
};
|
||||||
// A bare failable `NAME :: #run f();`: the comptime function
|
// A bare failable `NAME :: #run f();`: the comptime function
|
||||||
@@ -900,9 +904,16 @@ pub const LLVMEmitter = struct {
|
|||||||
.float => |v| c.LLVMConstReal(llvm_ty, v),
|
.float => |v| c.LLVMConstReal(llvm_ty, v),
|
||||||
.boolean => |v| c.LLVMConstInt(llvm_ty, @intFromBool(v), 0),
|
.boolean => |v| c.LLVMConstInt(llvm_ty, @intFromBool(v), 0),
|
||||||
.string => |sid| self.emitConstStringGlobal(self.ir_mod.types.getString(sid)),
|
.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
|
.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);
|
c.LLVMSetInitializer(llvm_global, init_val);
|
||||||
} else {
|
} else {
|
||||||
@@ -929,7 +940,14 @@ pub const LLVMEmitter = struct {
|
|||||||
defer field_vals.deinit(self.alloc);
|
defer field_vals.deinit(self.alloc);
|
||||||
for (func_ids) |fid| {
|
for (func_ids) |fid| {
|
||||||
const llvm_func = self.func_map.get(fid.index()) orelse {
|
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;
|
field_vals.append(self.alloc, c.LLVMConstNull(self.cached_ptr)) catch unreachable;
|
||||||
|
self.comptime_failed = true;
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
field_vals.append(self.alloc, llvm_func) catch unreachable;
|
field_vals.append(self.alloc, llvm_func) catch unreachable;
|
||||||
@@ -941,10 +959,23 @@ pub const LLVMEmitter = struct {
|
|||||||
.aggregate => |agg| {
|
.aggregate => |agg| {
|
||||||
// Re-emit. The first pass in `emitGlobals` already ran,
|
// Re-emit. The first pass in `emitGlobals` already ran,
|
||||||
// but func_ref leaves resolved to null then (func_map
|
// but func_ref leaves resolved to null then (func_map
|
||||||
// wasn't populated yet). Now they resolve properly.
|
// wasn't populated yet). Now they must resolve — a still-
|
||||||
const init_val = self.emitConstAggregate(agg, llvm_ty);
|
// 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);
|
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,
|
else => continue,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -962,6 +993,17 @@ pub const LLVMEmitter = struct {
|
|||||||
return ptr[0..len];
|
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
|
/// Serialize an interp `Value` to an LLVM constant for use as a static
|
||||||
/// global initializer. `ty` is the IR-level type of the destination;
|
/// global initializer. `ty` is the IR-level type of the destination;
|
||||||
/// the LLVM type is derived from it. `interp` gives access to the
|
/// 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
|
/// is included in any diagnostic the path produces so the user can
|
||||||
/// locate the offending `#run` site.
|
/// locate the offending `#run` site.
|
||||||
///
|
///
|
||||||
/// Returns `LLVMGetUndef` on bail — the build continues so adjacent
|
/// On bail, prints the diagnostic and routes through `failGlobalInit`
|
||||||
/// constants can still emit, but the diagnostic makes the problem clear.
|
/// (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(
|
fn valueToLLVMConst(
|
||||||
self: *LLVMEmitter,
|
self: *LLVMEmitter,
|
||||||
val: Value,
|
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",
|
"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},
|
.{global_name},
|
||||||
);
|
);
|
||||||
break :blk c.LLVMGetUndef(llvm_ty);
|
break :blk self.failGlobalInit(llvm_ty);
|
||||||
}
|
}
|
||||||
break :blk c.LLVMConstInt(llvm_ty, @bitCast(v), 1);
|
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),
|
.boolean => |v| c.LLVMConstInt(llvm_ty, @intFromBool(v), 0),
|
||||||
.null_val => c.LLVMConstNull(llvm_ty),
|
.null_val => c.LLVMConstNull(llvm_ty),
|
||||||
.void_val, .undef => c.LLVMGetUndef(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),
|
.string => |s| self.emitConstStringGlobal(s),
|
||||||
.aggregate => |fields| self.serializeAggregateValue(fields, ty, interp, global_name),
|
.aggregate => |fields| self.serializeAggregateValue(fields, ty, interp, global_name),
|
||||||
// The remaining Value variants cannot become static binary
|
// 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",
|
"error: comptime init of '{s}' produced a {s} value, which cannot be serialized as a static constant\n",
|
||||||
.{ global_name, @tagName(val) },
|
.{ 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",
|
"error: comptime init of '{s}' produced a fat-pointer aggregate whose len field is not an integer\n",
|
||||||
.{global_name},
|
.{global_name},
|
||||||
);
|
);
|
||||||
return c.LLVMGetUndef(llvm_ty);
|
return self.failGlobalInit(llvm_ty);
|
||||||
};
|
};
|
||||||
const len: usize = @intCast(len_i);
|
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",
|
"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 },
|
.{ global_name, @tagName(data), len },
|
||||||
);
|
);
|
||||||
return c.LLVMGetUndef(llvm_ty);
|
return self.failGlobalInit(llvm_ty);
|
||||||
};
|
};
|
||||||
|
|
||||||
return self.emitConstStringGlobal(bytes);
|
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",
|
"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 },
|
.{ 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;
|
var field_vals = std.ArrayList(c.LLVMValueRef).empty;
|
||||||
defer field_vals.deinit(self.alloc);
|
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",
|
"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) },
|
.{ global_name, self.ir_mod.types.typeName(ty) },
|
||||||
);
|
);
|
||||||
return c.LLVMGetUndef(llvm_ty);
|
return self.failGlobalInit(llvm_ty);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Function declaration ────────────────────────────────────────
|
// ── Function declaration ────────────────────────────────────────
|
||||||
@@ -2415,7 +2469,13 @@ pub const LLVMEmitter = struct {
|
|||||||
return c.LLVMConstStructInContext(self.context, &fields, 2, 0);
|
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 kind = c.LLVMGetTypeKind(llvm_ty);
|
||||||
const is_struct = kind == c.LLVMStructTypeKind;
|
const is_struct = kind == c.LLVMStructTypeKind;
|
||||||
const n: c_uint = @intCast(agg.len);
|
const n: c_uint = @intCast(agg.len);
|
||||||
@@ -2431,9 +2491,26 @@ pub const LLVMEmitter = struct {
|
|||||||
.float => |v| c.LLVMConstReal(elem_ty, v),
|
.float => |v| c.LLVMConstReal(elem_ty, v),
|
||||||
.boolean => |v| c.LLVMConstInt(elem_ty, @intFromBool(v), 0),
|
.boolean => |v| c.LLVMConstInt(elem_ty, @intFromBool(v), 0),
|
||||||
.string => |sid| self.emitConstStringGlobal(self.ir_mod.types.getString(sid)),
|
.string => |sid| self.emitConstStringGlobal(self.ir_mod.types.getString(sid)),
|
||||||
.aggregate => |inner| self.emitConstAggregate(inner, elem_ty),
|
.aggregate => |inner| self.emitConstAggregate(inner, elem_ty, require_resolved),
|
||||||
.func_ref => |fid| self.func_map.get(fid.index()) orelse c.LLVMConstNull(elem_ty),
|
.func_ref => |fid| self.func_map.get(fid.index()) orelse blk: {
|
||||||
else => c.LLVMConstNull(elem_ty),
|
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) {
|
if (is_struct) {
|
||||||
|
|||||||
130
src/ir/lower.zig
130
src/ir/lower.zig
@@ -926,25 +926,26 @@ pub const Lowering = struct {
|
|||||||
.bool_literal => |bl| .{ .boolean = bl.value },
|
.bool_literal => |bl| .{ .boolean = bl.value },
|
||||||
.float_literal => |fl| .{ .float = fl.value },
|
.float_literal => |fl| .{ .float = fl.value },
|
||||||
.string_literal => |sl| .{ .string = self.module.types.internString(sl.raw) },
|
.string_literal => |sl| .{ .string = self.module.types.internString(sl.raw) },
|
||||||
.array_literal => |al| self.constArrayLiteral(al.elements),
|
.array_literal => |al| self.constArrayLiteral(al.elements, var_ty) orelse self.diagnoseNonConstGlobal(vd, v),
|
||||||
.struct_literal => |sl| self.constStructLiteral(&sl, var_ty),
|
.struct_literal => |sl| self.constStructLiteral(&sl, var_ty) orelse self.diagnoseNonConstGlobal(vd, v),
|
||||||
.identifier => |id| blk: {
|
.identifier => |id| blk: {
|
||||||
// A global initialized from a module constant copies the
|
// A global initialized from a module constant copies the
|
||||||
// constant's recorded value (typed module consts land in
|
// constant's recorded value (typed module consts land in
|
||||||
// `module_const_map` via `registerTypedModuleConst`, run in the
|
// `module_const_map` via `registerTypedModuleConst`, run in the
|
||||||
// same pass-2 before this).
|
// same pass-2 before this).
|
||||||
if (self.program_index.module_const_map.get(id.name)) |ci| {
|
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|
|
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 });
|
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;
|
break :blk null;
|
||||||
},
|
},
|
||||||
// Enum-literal shorthand globals (`OS : OperatingSystem = .unknown;`)
|
// An enum-literal global (`chosen : Color = .green;`) serializes to
|
||||||
// keep their established zero-init: it is load-bearing for
|
// the variant's tag value against the destination enum type (issue
|
||||||
// compile-time `inline if OS == .X` in the stdlib (issue 0071 scope
|
// 0082). The compiler-injected `OS`/`ARCH` globals flow through here
|
||||||
// note). Carved out explicitly — not folded into a silent fallthrough.
|
// too; their runtime reads resolve via `comptime_constants`, so the
|
||||||
.enum_literal => null,
|
// 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
|
// Any other initializer shape (`.field_access` on a const, a call, an
|
||||||
// arithmetic expression, …) is not a static constant the compiler can
|
// arithmetic expression, …) is not a static constant the compiler can
|
||||||
// evaluate here. Diagnose loudly rather than emit a null payload that
|
// 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
|
/// 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`
|
/// 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
|
/// 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.
|
/// Try to convert an array literal's elements into a compile-time
|
||||||
/// Returns null if any element is not a compile-time constant.
|
/// ConstantValue.aggregate. `array_ty` is the array's resolved TypeId; its
|
||||||
fn constArrayLiteral(self: *Lowering, elements: []const *const Node) ?inst_mod.ConstantValue {
|
/// 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;
|
const vals = self.alloc.alloc(inst_mod.ConstantValue, elements.len) catch return null;
|
||||||
for (elements, 0..) |elem, i| {
|
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 };
|
return .{ .aggregate = vals };
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Try to convert a single AST expression into a compile-time ConstantValue.
|
/// Try to convert a single AST expression into a compile-time ConstantValue.
|
||||||
/// Returns null if the expression is not constant-foldable here.
|
/// `expected_ty` is the destination element/field type — it lets aggregate
|
||||||
fn constExprValue(self: *Lowering, expr: *const Node) ?inst_mod.ConstantValue {
|
/// 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) {
|
return switch (expr.data) {
|
||||||
.int_literal => |il| .{ .int = il.value },
|
.int_literal => |il| .{ .int = il.value },
|
||||||
.bool_literal => |bl| .{ .boolean = bl.value },
|
.bool_literal => |bl| .{ .boolean = bl.value },
|
||||||
.float_literal => |fl| .{ .float = fl.value },
|
.float_literal => |fl| .{ .float = fl.value },
|
||||||
.string_literal => |sl| .{ .string = self.module.types.internString(sl.raw) },
|
.string_literal => |sl| .{ .string = self.module.types.internString(sl.raw) },
|
||||||
.undef_literal => .zeroinit,
|
.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) {
|
.unary_op => |uo| switch (uo.op) {
|
||||||
.negate => switch (uo.operand.data) {
|
.negate => switch (uo.operand.data) {
|
||||||
.int_literal => |il| .{ .int = -il.value },
|
.int_literal => |il| .{ .int = -il.value },
|
||||||
@@ -1020,11 +1046,46 @@ pub const Lowering = struct {
|
|||||||
},
|
},
|
||||||
else => null,
|
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,
|
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
|
/// 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
|
/// struct's fields in declaration order, filling missing fields from the struct's
|
||||||
/// field defaults. Returns null if any value is not constant-foldable.
|
/// field defaults. Returns null if any value is not constant-foldable.
|
||||||
@@ -1055,7 +1116,7 @@ pub const Lowering = struct {
|
|||||||
break :blk null;
|
break :blk null;
|
||||||
};
|
};
|
||||||
if (init_expr) |e| {
|
if (init_expr) |e| {
|
||||||
vals[fi] = self.constExprValue(e) orelse return null;
|
vals[fi] = self.constExprValue(e, sf.ty) orelse return null;
|
||||||
} else {
|
} else {
|
||||||
vals[fi] = .zeroinit;
|
vals[fi] = .zeroinit;
|
||||||
}
|
}
|
||||||
@@ -2324,20 +2385,29 @@ pub const Lowering = struct {
|
|||||||
fn lowerExprAsPtr(self: *Lowering, node: *const Node) Ref {
|
fn lowerExprAsPtr(self: *Lowering, node: *const Node) Ref {
|
||||||
switch (node.data) {
|
switch (node.data) {
|
||||||
.identifier => |id| {
|
.identifier => |id| {
|
||||||
if (self.scope) |scope| {
|
const local = if (self.scope) |scope| scope.lookup(id.name) else null;
|
||||||
if (scope.lookup(id.name)) |binding| {
|
if (local) |binding| {
|
||||||
if (binding.is_alloca) {
|
if (binding.is_alloca) {
|
||||||
// If the variable IS a pointer (e.g., p: *Vec2), load it
|
// If the variable IS a pointer (e.g., p: *Vec2), load it
|
||||||
// to get the actual pointer value for GEP/store operations
|
// to get the actual pointer value for GEP/store operations
|
||||||
if (!binding.ty.isBuiltin()) {
|
if (!binding.ty.isBuiltin()) {
|
||||||
const info = self.module.types.get(binding.ty);
|
const info = self.module.types.get(binding.ty);
|
||||||
if (info == .pointer) {
|
if (info == .pointer) {
|
||||||
return self.builder.load(binding.ref, binding.ty);
|
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| {
|
.field_access => |fa| {
|
||||||
@@ -2697,9 +2767,11 @@ pub const Lowering = struct {
|
|||||||
const obj_ty = self.inferExprType(ie.object);
|
const obj_ty = self.inferExprType(ie.object);
|
||||||
const elem_ty = self.getElementType(obj_ty);
|
const elem_ty = self.getElementType(obj_ty);
|
||||||
const ptr_ty = self.module.types.ptrTo(elem_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 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);
|
break :blk self.builder.emit(.{ .index_gep = .{ .lhs = base, .rhs = idx } }, ptr_ty);
|
||||||
}
|
}
|
||||||
// address_of(field_access) → use lowerExprAsPtr for GEP chain
|
// address_of(field_access) → use lowerExprAsPtr for GEP chain
|
||||||
|
|||||||
Reference in New Issue
Block a user