ffi issue-0045: pack-fn block-body call — lock in LLVM verifier crash
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.
This commit is contained in:
25
examples/issue-0045.sx
Normal file
25
examples/issue-0045.sx
Normal file
@@ -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;
|
||||
}
|
||||
122
issues/0045-pack-fn-call-llvm-verifier-failure.md
Normal file
122
issues/0045-pack-fn-call-llvm-verifier-failure.md
Normal file
@@ -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.
|
||||
1
tests/expected/issue-0045.exit
Normal file
1
tests/expected/issue-0045.exit
Normal file
@@ -0,0 +1 @@
|
||||
1
|
||||
3
tests/expected/issue-0045.txt
Normal file
3
tests/expected/issue-0045.txt
Normal file
@@ -0,0 +1,3 @@
|
||||
LLVM verification failed: Terminator found in the middle of a basic block!
|
||||
label %entry
|
||||
|
||||
Reference in New Issue
Block a user