From 3d32ab0fc61d03991993e74c1699beac26cecb80 Mon Sep 17 00:00:00 2001 From: agra Date: Wed, 27 May 2026 13:19:49 +0300 Subject: [PATCH] =?UTF-8?q?ffi=20issue-0045:=20pack-fn=20block-body=20call?= =?UTF-8?q?=20=E2=80=94=20lock=20in=20LLVM=20verifier=20crash?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Filed `issues/0045-pack-fn-call-llvm-verifier-failure.md`. Surfaced by probing step 2 territory of the variadic heterogeneous type packs feature: any `..$args` fn whose body is a block containing `return X;` (or any comptime fn with a non-void return, comptime params, and explicit `return` in a block body) trips LLVM's "Terminator found in the middle of a basic block" verifier. `lowerComptimeCall` inlines the body's statements directly into the caller's LLVM function. `lowerReturn` then emits a `ret` into the caller's basic block — but the caller still has trailing instructions, hence the verifier failure. `examples/issue-0045.sx` reproduces the crash with the minimum pack-fn shape (`foo :: (..$args) -> s64 { return 42; }`). Same shape with a plain comptime param (`($x: s32) -> s64 { return 42; }`) reproduces identically, so the bug is broader than packs. Arrow-form bodies (`=> 42`) work today because they have no `return` statement. Next commit teaches `lowerComptimeCall` to allocate a result slot when the body contains a `return`, and reroutes `lowerReturn` to store into that slot + flag the block as terminated so the inliner picks up the value. --- examples/issue-0045.sx | 25 ++++ ...0045-pack-fn-call-llvm-verifier-failure.md | 122 ++++++++++++++++++ tests/expected/issue-0045.exit | 1 + tests/expected/issue-0045.txt | 3 + 4 files changed, 151 insertions(+) create mode 100644 examples/issue-0045.sx create mode 100644 issues/0045-pack-fn-call-llvm-verifier-failure.md create mode 100644 tests/expected/issue-0045.exit create mode 100644 tests/expected/issue-0045.txt 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 +