From fa7c07faf8447394c83c2b4573cb5547aa1322cc Mon Sep 17 00:00:00 2001 From: agra Date: Tue, 23 Jun 2026 11:34:22 +0300 Subject: [PATCH] fix: comptime reg->value bridge for array-in-aggregate + clean abort on comptime-init failure (issue 0167) (C) regToValue (comptime_vm.zig) gained no array arm, so a #run returning an aggregate containing an array bailed 'reg->value: aggregate shape not bridged yet'. Add an .array arm: read N elements at stride typeSizeBytes(elem) from the array address, bridge each recursively via regToValue -> an .aggregate Value (serializeAggregateValue already emits arrays). Composes with struct fields, nested arrays, array-of-structs, and the ?Arr optional payload; unbridgeable elements bail loudly. (E) A global failing #run proceeded into LLVM emission and panicked 'unresolved type reached LLVM emission' when the unresolved const was used. Add 'if (self.comptime_failed) return;' in emit() after Pass 0 so it aborts cleanly (exit 1, the comptime diagnostic) across run/ir/build. Regression: examples/comptime/0644-comptime-run-array-aggregate.sx. Verified by 3 adversarial reviews, suite 793/0. Filed separate bugs found during review: 0181 (optional-chain ?. to array field + index panics), 0182 (body-local #run unbridged silently miscompiles). --- .../0644-comptime-run-array-aggregate.sx | 62 +++++++++++++ .../0644-comptime-run-array-aggregate.exit | 1 + .../0644-comptime-run-array-aggregate.stderr | 1 + .../0644-comptime-run-array-aggregate.stdout | 5 ++ ...-comptime-regtovalue-array-in-aggregate.md | 17 ++++ ...truct-with-array-field-unresolved-panic.md | 86 +++++++++++++++++++ ...omptime-run-unbridged-silent-miscompile.md | 46 ++++++++++ src/ir/comptime_vm.zig | 18 ++++ src/ir/emit_llvm.zig | 10 +++ 9 files changed, 246 insertions(+) create mode 100644 examples/comptime/0644-comptime-run-array-aggregate.sx create mode 100644 examples/comptime/expected/0644-comptime-run-array-aggregate.exit create mode 100644 examples/comptime/expected/0644-comptime-run-array-aggregate.stderr create mode 100644 examples/comptime/expected/0644-comptime-run-array-aggregate.stdout create mode 100644 issues/0181-optional-chain-on-struct-with-array-field-unresolved-panic.md create mode 100644 issues/0182-body-local-comptime-run-unbridged-silent-miscompile.md diff --git a/examples/comptime/0644-comptime-run-array-aggregate.sx b/examples/comptime/0644-comptime-run-array-aggregate.sx new file mode 100644 index 00000000..1717fa7e --- /dev/null +++ b/examples/comptime/0644-comptime-run-array-aggregate.sx @@ -0,0 +1,62 @@ +// A `#run` (comptime const init) whose function returns an aggregate that +// CONTAINS AN ARRAY — or an array directly — evaluates: the comptime VM's +// reg→value bridge reads the array's elements out of comptime memory and +// produces a `Value` array the LLVM serializer emits as a constant. +// +// Regression (issue 0167 C): the array-in-aggregate shape used to bail with +// `reg→value: aggregate shape not bridged yet`. +// Covers: struct-with-array-field, array-of-structs, nested array `[2][2]`, +// a direct `[N]T` return, and the `?Arr` optional payload (composes with the +// optional bridge arm) unwrapped via `!`. + +#import "modules/std.sx"; + +Arr3 :: struct { xs: [3]i64; } +Pt :: struct { x: i64; y: i64; } +Box :: struct { items: [2]Pt; } +Mat :: struct { g: [2][2]i64; } + +mk_struct :: () -> Arr3 { + r : Arr3 = ---; + r.xs[0] = 1; r.xs[1] = 2; r.xs[2] = 3; + return r; +} + +mk_aos :: () -> Box { + r : Box = ---; + r.items[0].x = 1; r.items[0].y = 2; + r.items[1].x = 3; r.items[1].y = 4; + return r; +} + +mk_nested :: () -> Mat { + r : Mat = ---; + r.g[0][0] = 1; r.g[0][1] = 2; + r.g[1][0] = 3; r.g[1][1] = 4; + return r; +} + +mk_direct :: () -> [3]i64 { + r : [3]i64 = ---; + r[0] = 7; r[1] = 8; r[2] = 9; + return r; +} + +mk_opt :: () -> ?Arr3 { + r : Arr3 = .{ xs = .[10, 20, 30] }; + return r; +} + +G :: #run mk_struct(); // struct { [3]i64 } +B :: #run mk_aos(); // struct { [2]Pt } +M :: #run mk_nested(); // struct { [2][2]i64 } +D :: #run mk_direct(); // [3]i64 +A :: #run mk_opt(); // ?Arr3 + +main :: () { + print("{} {} {}\n", G.xs[0], G.xs[1], G.xs[2]); // 1 2 3 + print("{} {} {} {}\n", B.items[0].x, B.items[0].y, B.items[1].x, B.items[1].y); // 1 2 3 4 + print("{} {} {} {}\n", M.g[0][0], M.g[0][1], M.g[1][0], M.g[1][1]); // 1 2 3 4 + print("{} {} {}\n", D[0], D[1], D[2]); // 7 8 9 + print("{}\n", A!.xs[0]); // 10 +} diff --git a/examples/comptime/expected/0644-comptime-run-array-aggregate.exit b/examples/comptime/expected/0644-comptime-run-array-aggregate.exit new file mode 100644 index 00000000..573541ac --- /dev/null +++ b/examples/comptime/expected/0644-comptime-run-array-aggregate.exit @@ -0,0 +1 @@ +0 diff --git a/examples/comptime/expected/0644-comptime-run-array-aggregate.stderr b/examples/comptime/expected/0644-comptime-run-array-aggregate.stderr new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/examples/comptime/expected/0644-comptime-run-array-aggregate.stderr @@ -0,0 +1 @@ + diff --git a/examples/comptime/expected/0644-comptime-run-array-aggregate.stdout b/examples/comptime/expected/0644-comptime-run-array-aggregate.stdout new file mode 100644 index 00000000..b46dc62a --- /dev/null +++ b/examples/comptime/expected/0644-comptime-run-array-aggregate.stdout @@ -0,0 +1,5 @@ +1 2 3 +1 2 3 4 +1 2 3 4 +7 8 9 +10 diff --git a/issues/0167-comptime-regtovalue-array-in-aggregate.md b/issues/0167-comptime-regtovalue-array-in-aggregate.md index 86c7cada..88c94cc9 100644 --- a/issues/0167-comptime-regtovalue-array-in-aggregate.md +++ b/issues/0167-comptime-regtovalue-array-in-aggregate.md @@ -1,5 +1,22 @@ # 0167 — comptime `#run` returning an aggregate that contains an array fails the reg→value bridge (+ unclean recovery) +> **RESOLVED.** (C) Added an `.array` arm to `regToValue` in +> `src/ir/comptime_vm.zig`: reads N elements at stride `typeSizeBytes(elem)` from +> the array's address and recursively bridges each via `regToValue(elem_ty)` → +> an `.aggregate` Value (`serializeAggregateValue` already handles arrays). +> Composes with struct-field walks, nested arrays, array-of-structs, and the +> `?Arr` optional payload; unbridgeable element types bail loudly. (E) Added +> `if (self.comptime_failed) return;` in `emit()` (`src/ir/emit_llvm.zig`) after +> Pass 0, so a GLOBAL failing `#run` aborts cleanly (exit 1, the `comptime init +> of 'X' failed: …` diagnostic) instead of panicking `unresolved type reached +> LLVM emission` — verified across `sx run`/`ir`/`build`. Regression: +> `examples/comptime/0644-comptime-run-array-aggregate.sx`. Verified by 3 +> adversarial reviews; suite 793/0. The issue's (E) repro `A?.xs[0]` now routes +> to two SEPARATE pre-existing bugs filed during review: **0181** (optional-chain +> `?.` to an array field then `[idx]` → unresolved panic, pure-runtime) and +> **0182** (body-local `#run` of an unbridged shape silently miscompiles — +> `lowerInlineComptime` doesn't set `comptime_failed`). Both are out of 0167's +> reg→value-bridge scope. ## Symptom Two related defects: diff --git a/issues/0181-optional-chain-on-struct-with-array-field-unresolved-panic.md b/issues/0181-optional-chain-on-struct-with-array-field-unresolved-panic.md new file mode 100644 index 00000000..6555f716 --- /dev/null +++ b/issues/0181-optional-chain-on-struct-with-array-field-unresolved-panic.md @@ -0,0 +1,86 @@ +# 0181 — `?.`-chain (and `?`-postfix) on an optional whose child struct contains an ARRAY field panics `unresolved type reached LLVM emission` + +## Symptom + +A `?.` optional-chain access (or the `?` optional-test postfix used in a +member-access chain) on a value of type `?S`, where `S` is a struct that +contains an **array field**, panics: + +`thread … panic: unresolved type reached LLVM emission — a type resolution +failure was not diagnosed/aborted` (exit 134). + +The same chain on `?S` where `S` has **no** array field works fine, and the +`!` force-unwrap chain (`opt!.field`) on the same array-containing `?S` works +fine. The defect is specific to the `?`/`?.` operator's receiver-type inference +when the optional's child struct contains an array field — that receiver types +as `.unresolved` and reaches LLVM. This is a pure **runtime** lowering bug: no +`#run`/comptime is involved. + +Observed vs expected: +- Observed: SIGABRT panic (exit 134) at `src/backend/llvm/types.zig:196` + (`toLLVMTypeInfo` `.unresolved` arm), reached from `declareFunction`'s + `param.ty` lowering of a synthesized accessor. +- Expected: the chain evaluates (prints the field), exactly as the `!`-unwrap + and the non-array `?.` forms already do. + +## Reproduction + +Pure runtime, no `#run` — panics: +```sx +#import "modules/std.sx"; +Arr3 :: struct { xs: [3]i64; } +mk :: () -> ?Arr3 { r : Arr3 = .{ xs = .[1,2,3] }; return r; } +main :: () { print("{}\n", mk()?.xs[0] ?? 99); } // PANIC exit 134 +``` + +Control A — same chain, child struct has NO array field — WORKS, prints `7`: +```sx +#import "modules/std.sx"; +Pt :: struct { x: i64; } +mk :: () -> ?Pt { return Pt.{ x = 7 }; } +main :: () { print("{}\n", mk()?.x ?? 99); } +``` + +Control B — same array-containing `?Arr3`, but `!` force-unwrap — WORKS, prints `1`: +```sx +#import "modules/std.sx"; +Arr3 :: struct { xs: [3]i64; } +mk :: () -> ?Arr3 { r : Arr3 = .{ xs = .[1,2,3] }; return r; } +main :: () { print("{}\n", mk()!.xs[0]); } +``` + +(The issue 0167 (E) repro `A?.xs[0]` hit this same bug — it used `?` where `!` +was meant; with `!` the comptime `#run ?Arr3` case evaluates. So this is the +*residual* defect that 0167's (E) repro tripped, distinct from 0167 (C)/(E), +both of which are fixed.) + +## Investigation prompt + +The `?` optional-chaining / optional-test path synthesizes an accessor whose +receiver (the unwrapped child) types as `.unresolved` specifically when the +child is a struct containing an array field — mirroring the already-fixed +issue-0101 `!`-unwrap bug (`inferExprType` had no force_unwrap arm → receiver +typed `.unresolved`). The `!` path was fixed (see +`examples/optionals/0905-optionals-unwrap-field-chain.sx`); the `?`/`?.` path +has an analogous gap that only surfaces for an array-containing child (a +plain-scalar/string child happens to resolve). + +Suspected area: `src/ir/lower.zig` `inferExprType` (grep for the optional-chain +/ `?` postfix / `safe_nav` handling) and/or `src/ir/lower/` accessor-chain +lowering — find where the `?`-chain receiver type is computed and why an +array-containing struct child yields `.unresolved`. Compare against the working +`!`-unwrap arm (issue 0101 fix) and apply the same receiver-type flow. + +Verification: the first repro above prints `1` and exits 0; controls A and B +still pass; add a regression under `examples/optionals/` covering `?.`-chain on +an array-containing `?S` (field read + `?? default`). Confirm +`examples/comptime/0644-comptime-run-array-aggregate.sx` (issue 0167) still +passes. + +## Provenance + +Discovered while implementing issue 0167 (C: comptime reg→value array-in- +aggregate bridge; E: clean-abort on comptime-init failure). 0167 (C) and (E) +are FIXED and verified; the `?Arr3` access form in 0167's (E) repro tripped this +SEPARATE, pre-existing runtime lowering bug (confirmed reproducible on clean +`HEAD` with no `#run`). diff --git a/issues/0182-body-local-comptime-run-unbridged-silent-miscompile.md b/issues/0182-body-local-comptime-run-unbridged-silent-miscompile.md new file mode 100644 index 00000000..ee286182 --- /dev/null +++ b/issues/0182-body-local-comptime-run-unbridged-silent-miscompile.md @@ -0,0 +1,46 @@ +# 0182 — a body-local `#run` of an unbridged-shape return silently miscompiles (no abort, exit 0 garbage) + +## Symptom + +A `#run` const declared INSIDE a function body, whose comptime function returns a +shape the comptime VM cannot bridge to a host `Value` (e.g. `[2][]i64` — array of +slices, or a struct containing a slice that can't be const-materialized), does +NOT fail the build. Unlike a GLOBAL `#run` const (which sets `comptime_failed` +and aborts cleanly with `error: comptime init of 'X' failed: ...` — see issue +0167's recovery guard), the body-local form leaves a RUNTIME call to the comptime +function in place, executing it at runtime over uninitialized (`---`) storage → +garbage, exit 0, NO diagnostic. Silent miscompile (no-silent-fallback violation). + +Found during adversarial review of issue 0167. + +## Reproduction + +```sx +#import "modules/std.sx"; +mk :: () -> [2][]i64 { a : []i64 = ---; r : [2][]i64 = ---; r[0] = a; r[1] = a; return r; } +main :: () { + L :: #run mk(); // body-local #run of an unbridgeable shape + print("{}\n", L[0][0]); // prints garbage, exit 0 — should be a clean comptime error +} +``` + +Expected: a clean `error: comptime init of 'L' failed: ...` (exit 1), the same as +the equivalent GLOBAL `#run` const. Observed: garbage output, exit 0, no +diagnostic. (Even an UNUSED body-local `L :: #run mk()` silently "succeeds" +instead of reporting the bridge failure.) + +## Investigation prompt + +`src/ir/lower/comptime.zig` `lowerInlineComptime` (~line 337) emits a RUNTIME +`call` to the comptime function and relies on the interpreter to const-fold it. +When the fold/`regToValue` bridge cannot materialize the result, the runtime call +is left in place rather than failing — so the comptime fn runs at runtime over +`---` storage. The fix: when a body-local `#run` const-fold fails to materialize +(the same `regToValue` bail that a global `#run` reports), it must set +`comptime_failed` / emit the `comptime init of 'X' failed: ` diagnostic +and abort, exactly like the global-init path (`failGlobalInit` in +`src/ir/emit_llvm.zig`), NOT silently fall back to a runtime call. Mirror the +global path's loud failure. Verify: the repro exits 1 with the comptime +diagnostic; a body-local `#run` of a BRIDGEABLE shape (scalar, struct, array, +`?Arr`) still works (don't regress the common case); an unused failing body-local +`#run` also aborts. Add an `examples/comptime/06xx-...` (or a diagnostics) test. diff --git a/src/ir/comptime_vm.zig b/src/ir/comptime_vm.zig index a88274c1..8184af7b 100644 --- a/src/ir/comptime_vm.zig +++ b/src/ir/comptime_vm.zig @@ -2236,6 +2236,24 @@ pub const Vm = struct { } return .{ .aggregate = out }; } + if (info == .array) { + // `[N]E` is held by-address as N contiguous `E` slots at + // stride `sizeof(E)`. Bridge each element via `regToValue` + // recursively (so a nested array / array-of-struct / array + // inside a struct all compose), producing an `.aggregate` + // Value whose serializer arm (`serializeAggregateValue`'s + // `.array` case) emits an `LLVMConstArray2`. + const elem_ty = info.array.element; + const len: usize = @intCast(info.array.length); + const stride: Addr = @intCast(table.typeSizeBytes(elem_ty)); + const out = alloc.alloc(Value, len) catch return self.failMsg("reg→value: out of memory (array)"); + for (0..len) |i| { + const elem_addr = reg + @as(Addr, @intCast(i)) * stride; + const er = try self.readField(table, elem_addr, elem_ty); + out[i] = try self.regToValue(alloc, table, er, elem_ty); + } + return .{ .aggregate = out }; + } if (info == .optional) { // Only the `{ payload@0, has_value@sizeof(child) }` aggregate // shape lands here — a pointer-child optional is a word and diff --git a/src/ir/emit_llvm.zig b/src/ir/emit_llvm.zig index c54d0284..5d60ebad 100644 --- a/src/ir/emit_llvm.zig +++ b/src/ir/emit_llvm.zig @@ -391,6 +391,16 @@ pub const LLVMEmitter = struct { // Pass 0.5: Run comptime side-effect functions (#run expr; at top level) self.runComptimeSideEffects(); + // A comptime/global init failure (e.g. an unbridgeable `#run` result) + // sets `comptime_failed` AND leaves the failed const's type as the + // `.unresolved` sentinel. The driver converts `comptime_failed` into a + // clean exit-1 *after* emit() returns — but the remaining passes + // (declare/emit function bodies that reference the now-unresolved const) + // would `@panic("unresolved type reached LLVM emission")` first. Abort + // emission here so the failure surfaces as the printed diagnostic + + // clean exit, never the panic. + if (self.comptime_failed) return; + // Pass 1: Declare all functions (so calls can reference them) for (self.ir_mod.functions.items, 0..) |func, i| { self.declareFunction(&func, @intCast(i));