`lowerComptimeCall` now scans the body for `return` statements
via `fnBodyHasReturn`. When found, it allocates a stack slot
typed to the fn's return type and installs it as
`self.inline_return_target` before lowering the body.
`lowerReturn` checks `inline_return_target` first:
- If set, it stores the coerced return value into the slot,
drains pending defers, sets `block_terminated = true`, and
returns without emitting a `ret` into the caller's basic
block.
- Otherwise it emits the standard `ret` as before.
After the body lowers, the inliner either returns the
tail-expression value (existing fast path — bodies with no
`return` skip the slot entirely) or loads the slot when
`block_terminated` is set.
Why the bug was invisible until now: `format`/`print` and
every other stdlib comptime fn use arrow form (`=> expr`) or
`#insert`-only bodies — no `return` statement, no path through
`lowerReturn`. Step 1.b of the pack feature made `..$args`
parseable; the natural smoke test
`foo :: (..$args) -> s64 { return 42; }` was the first
comptime-fn body to take the `return`-with-trailing-statements
path, surfacing the LLVM verifier crash.
`examples/issue-0045.sx` flips from the lock-in failure to
`42`. 194/194 example tests + `zig build test` green.
129 lines
4.7 KiB
Markdown
129 lines
4.7 KiB
Markdown
**FIXED.** `lowerComptimeCall` now allocates a result slot when
|
|
the body contains a `return` statement and reroutes
|
|
`lowerReturn` to store into it instead of emitting `ret` into the
|
|
caller's basic block. Regression test:
|
|
[examples/issue-0045.sx](../examples/issue-0045.sx).
|
|
|
|
# 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.
|