diff --git a/examples/issue-0045.sx b/examples/issue-0045.sx new file mode 100644 index 0000000..4ac4771 --- /dev/null +++ b/examples/issue-0045.sx @@ -0,0 +1,25 @@ +// issue-0045 — calling a comptime fn whose body is a block +// containing an explicit `return X;` trips LLVM's "Terminator found +// in the middle of a basic block" verifier. +// +// Surfaced by the variadic heterogeneous type packs feature (step +// 1 made `..$args` parseable, so the simplest pack-fn smoke test +// exercised the bug). The root cause is broader than packs: ANY +// comptime fn with `is_comptime` params, a non-void return, and a +// block body with `return X;` had the same crash. `format`/`print` +// use arrow form (`=> expr`) or `#insert`-only bodies, so the bug +// was invisible until pack-fn bodies surfaced it. +// +// Once fixed, calling foo() reaches the body's `return 42;`, the +// inliner stores 42 into a result slot, the caller loads it as the +// inline value, and main prints "42". + +#import "modules/std.sx"; + +foo :: (..$args) -> s64 { return 42; } + +main :: () -> s32 { + n : s64 = foo(1, "hello"); + print("{}\n", n); + return 0; +} diff --git a/issues/0045-pack-fn-call-llvm-verifier-failure.md b/issues/0045-pack-fn-call-llvm-verifier-failure.md new file mode 100644 index 0000000..6bb920f --- /dev/null +++ b/issues/0045-pack-fn-call-llvm-verifier-failure.md @@ -0,0 +1,122 @@ +# Symptom + +Calling a fn declared with `..$args` (variadic heterogeneous type +pack, parser-accepted as of commit `a51fe26`) — even with zero +positional arguments — emits LLVM IR that fails verification: + +``` +LLVM verification failed: Terminator found in the middle of a basic block! +label %entry +``` + +No IR is printed by `sx ir`; the `sx run` JIT exits 1 immediately. + +Expected: at minimum, the empty-pack call site should compile and +execute the fn body. Plan step 2 ("Runtime indexing + mono expansion") +specifies per-mono mangling and `..$args` expansion to N positional +IR params; until that lands, calling such a fn should at minimum +emit a clear "pack-fn calls not yet implemented" diagnostic rather +than corrupt IR. + +# Reproduction + +```sx +foo :: (..$args) -> s64 { return 42; } + +main :: () -> s32 { + n : s64 = foo(); + return 0; +} +``` + +``` +$ ./zig-out/bin/sx run repro.sx +LLVM verification failed: Terminator found in the middle of a basic block! +label %entry +``` + +`foo()` with zero args, one arg (`foo(1)`), or multiple args +(`foo(1, "hello")`) all produce the same crash. + +# Background + +After M5.A.next.1b (commit `a51fe26`), `parseParams` accepts +`..$args` as a parameter declaration. The Param is recorded with +`is_variadic = true`, `is_comptime = true`, `type_expr = inferred_type`. +`parseFnDecl`'s `collectTypeParams` then registers `args` as a +type-param (because `is_comptime = true`), so `fd.type_params.len > 0`. + +This routes the fn through the existing generic-fn path: +`lowerFnDecl` skips eager lowering, expecting calls to monomorphise +at first use. But the existing monomorphisation machinery binds a +single TypeId per `$T` name — it has no notion of a *pack* (a +variable-length list of TypeIds bound positionally). When the +call site tries to monomorphise with the call's args, the body's +`args` parameter gets resolved to a single (probably default `.s64`) +TypeId, but the call-site arg-packing path (`packVariadicCallArgs`) +treats it as a regular `..T` slice — the two views disagree and +the emitted IR is malformed. + +The bug isn't in step 1's code itself; it's the gap between +"step 1 made the syntax parseable" and "step 2 hasn't made the +calls executable yet." + +# Investigation prompt + +For a fresh session picking this up: + +Plan step 2 ("Runtime indexing + mono expansion") in +`~/.claude/plans/lets-see-options-for-merry-dijkstra.md` is the +intended fix: +1. Detect pack-fns at declaration: the fn has a trailing param + with `is_variadic && is_comptime` (no concrete type annotation + distinguishes it from a regular `args: ..T` variadic). +2. Per-call monomorphisation: bind `$args := [T1, ..., Tn]` + from the call site's concrete arg types. Each unique + `(arg-type-tuple, $ret)` combination gets its own mono. +3. Expand the pack into N positional IR params in the mono's + signature; mangling encodes the pack shape so distinct + monos get distinct symbols. +4. Body `args[$i]` at comptime-known `$i` lowers to the i-th + expanded param load (return type from `$args[$i]`). + +Key files: +- `src/ir/lower.zig`: + - `lowerFnDecl` (around line 949 — generic skip) needs to keep + skipping pack-fns. + - `monomorphizeFunction` (line 7834) needs a pack-aware path + that binds `pack_bindings` (the field added in commit + `08feb60` for impl matching) instead of just `type_bindings`. + - `packVariadicCallArgs` (line 7275) should NOT run for pack + fns — args stay positional, not slice-packed. + - Index-expression lowering needs an `args[$i]` arm that reads + the i-th positional param. +- `src/ir/types.zig`: `FunctionInfo`/`ClosureInfo` have + `pack_start` already (added in commit `6582449`); the mono's + expanded signature should NOT carry `pack_start` (it's a + concrete shape). + +Verification: the repro above compiles and prints "42" when run +as `./zig-out/bin/sx run repro.sx`. A new +`examples/156-pack-fn-mono.sx` (number depends on next free slot) +should be added per the FFI cadence rule (xfail-lock-in then +green). + +Alternative interim option: if step 2 is too large to land in +one session, gate `parseFnDecl` to reject pack params with an +explicit "pack-fn body lowering not yet implemented; only impl +target types accept `..$args` today" diagnostic. Lets the +parser accept the syntax in impl headers (step 1's payoff) while +preventing the LLVM verifier crash. The diagnostic disappears +when step 2 lands. + +# Verification + +Once the fix is in: + +```sh +./zig-out/bin/sx run examples/156-pack-fn-mono.sx +# Expected: prints "42" +``` + +Full suite + zig test must still pass. diff --git a/tests/expected/issue-0045.exit b/tests/expected/issue-0045.exit new file mode 100644 index 0000000..d00491f --- /dev/null +++ b/tests/expected/issue-0045.exit @@ -0,0 +1 @@ +1 diff --git a/tests/expected/issue-0045.txt b/tests/expected/issue-0045.txt new file mode 100644 index 0000000..2556f1e --- /dev/null +++ b/tests/expected/issue-0045.txt @@ -0,0 +1,3 @@ +LLVM verification failed: Terminator found in the middle of a basic block! +label %entry +