Files
sx/issues/0046-comptime-fn-nested-print-with-return.md
agra 83c2c9d176 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.
2026-05-27 14:09:20 +03:00

5.3 KiB

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

#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)evalComptimeStringcreateComptimeFunctioninterp.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

./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.