ffi issue-0046: file nested-comptime-call + return latent bug

Comptime fn body containing BOTH a nested comptime call
(`print(...)`) AND a `return X;` fails in one of two shapes
depending on the comptime-param flavour: a `storeAtRawPtr`
panic in the interp (plain `$x: s32` comptime) or "unresolved
'result'" at compile time (pack-fn `..$args`).

Same root: my issue-0045 fix's `inline_return_target` slot
setup interacts badly with the recursive comptime-call path
that invokes `#insert build_format(fmt)` → interpreter →
parse-and-lower of `result := ...` statements.

Pre-issue-0045-fix the pattern crashed at the LLVM verifier
("Terminator found in the middle of a basic block") so the
recursive path never ran. The fix exposed the deeper bug; it
didn't create it.

Not blocking the next pack-feature slices:
- Step 2a tests use arrow-form bodies with no nested print.
- Steps 2b/3 don't inherently require nested comptime calls —
  builders run inside `#insert` contexts, not inside public
  pack-fn bodies.
- Will bite when step 5 refactors stdlib's `print`/`format` to
  `..$args` or when user code writes a pack-fn with both
  `print` debug output and an early `return`.

Investigation prompt in the issue file points at
`createComptimeFunction`'s saved/restored state list (missing
`inline_return_target`, `pack_arg_nodes`,
`comptime_param_nodes`) as the most likely angle.
This commit is contained in:
agra
2026-05-27 14:09:20 +03:00
parent 4d9a7eda5a
commit 83c2c9d176

View File

@@ -0,0 +1,140 @@
# Symptom
A comptime fn body containing BOTH a nested comptime call
(e.g. `print(...)`) AND a `return X;` statement fails in one of
two shapes depending on the comptime-param flavour:
| Outer fn shape | Failure |
|---|---|
| `helper :: ($x: s32) -> s64 { print("inside\n"); return 42; }` (plain comptime) | Panic: `cast causes pointer to be null` at `src/ir/interp.zig:207 storeAtRawPtr`. |
| `dump :: (..$args) -> s64 { n := args[0]; print("got {}\n", n); return n; }` (pack-fn) | Compile error: `unresolved 'result'` at fake span `1:5` (inside the inserted code). |
Both vanish if you remove either the nested `print(...)` OR the
`return X;` statement:
- Arrow-form bodies (`=> expr`) work.
- Bodies with `return` but no nested comptime call work.
- Bodies with a nested comptime call but no `return` work.
Both faces share one root: my fix for issue-0045 (commit
`9e78790`) added an `inline_return_target` slot + alloca in
`lowerComptimeCall`, which is now active when the outer call's
body recursively invokes another comptime fn that itself runs
the `#insert build_format(fmt)` → interpreter → parse-and-lower
pipeline.
Pre-fix this pattern crashed too — at the LLVM verifier with
"Terminator found in the middle of a basic block" — because the
outer `return` emitted `ret X` into the caller's basic block
mid-flight. My fix routed `return` into the slot so the outer
body now fully completes, which means the recursive comptime
call runs to completion too. The interpreter / #insert scope
chain then has to be correct in this newly-reachable context,
and it isn't.
# Reproduction
```sx
#import "modules/std.sx";
// Face 1 — interp panic:
helper :: ($x: s32) -> s64 {
print("inside\n");
return 42;
}
// Face 2 — "unresolved 'result'":
dump :: (..$args) -> s64 {
n : s64 = args[0];
print("got {}\n", n);
return n;
}
main :: () -> s32 {
n := helper(7); // ← panic in interp
print("{}\n", dump(7)); // ← "unresolved 'result'"
return 0;
}
```
Each face reproduces independently — they don't need to coexist
in the same program.
# What's NOT happening
- Not a regression introduced by issue-0045's fix per se: the
same pattern hit a different fatal stage (LLVM verifier) before
the fix. The fix exposed it; it didn't create it.
- Not caused by step 2a (pack typed indexing, commit `cd36784`):
Face 1 reproduces with a plain `($x: s32)` comptime fn, no
pack involved.
- Not exercised by any test in the suite today. `format`/`print`
use arrow form or `#insert`-only bodies — no `return` in a
block. User code historically followed the same pattern.
# Why this didn't block step 2a
Step 2a only tests `args[$i]` in arrow-form pack bodies and
arithmetic chains. No nested comptime call in any test body.
Step 2b (per-mono mangling) and step 3 (type-reflection
intrinsics, `$args[$i]` in type positions) don't inherently
require nested comptime calls either — builder fns run inside
`#insert` contexts, not inside the public pack-fn body, so
they have a different lowering path.
The pattern WILL bite when:
- Step 5 of the pack plan refactors stdlib's `print`/`format` to
use `..$args` — print's body itself becomes the outer comptime
fn that nests comptime calls.
- User code writes a pack-fn that wants both `print` for debug
output AND `return X;` for early exit.
# Investigation prompt
For a fresh session picking this up:
The interaction is between (a) my issue-0045 `inline_return_target`
slot + alloca setup in `lowerComptimeCall` and (b) the recursive
comptime path that invokes `#insert build_format(fmt)`
`evalComptimeString``createComptimeFunction``interp.call`
on a wrapper fn, then parses the returned source string and
lowers each parsed statement into the current scope.
Three angles worth probing:
1. **Saved/restored state in `createComptimeFunction`** at
`src/ir/lower.zig:8851+`. It saves `builder.func`,
`builder.current_block`, `builder.inst_counter`, `self.scope`,
`current_ctx_ref`. It does NOT save/restore
`inline_return_target`, `pack_arg_nodes`, `comptime_param_nodes`.
The first two were added by my recent commits (`9e78790`,
`cd36784`). One of these leaking into the wrapper-fn lowering
is the most likely cause of Face 1.
2. **Ref numbering** — the alloca I added for `ret_slot` shifts
subsequent Ref values in the outer fn (main). The interp
shouldn't see those refs (it executes the wrapper fn's IR,
not main's), but check whether the wrapper fn carries a stale
Ref handle from the outer build context.
3. **Scope chain visible to parsed `#insert` statements**. For
Face 2 the inserted code declares `result := ""` then references
`result` in the next stmt. The lookup fails. Maybe the
`lowerBlockValue` exit defer fires the parent scope deinit
before the next stmt lowers — or `block_terminated` from the
inline-return slot setup is interfering with the inserted-stmt
loop in `lowerInsertExprValue`
(`src/ir/lower.zig:7065+`).
A reasonable starting place: add the missing save/restore for
`inline_return_target` (and `pack_arg_nodes`) in
`createComptimeFunction`, then re-run both repros. If Face 1
disappears, that confirms angle 1.
# Verification
```sh
./zig-out/bin/sx run /tmp/issue-0046-face1.sx # expect "n=42"
./zig-out/bin/sx run /tmp/issue-0046-face2.sx # expect "got 7\n7"
```
Full suite + zig test must still pass after the fix.