Skip to content

Bridge f32/f64 across closure calls on the stack backend - #43

Merged
wtholliday merged 2 commits into
mainfrom
fix-stack-closure-float-return
Aug 26, 2026
Merged

Bridge f32/f64 across closure calls on the stack backend#43
wtholliday merged 2 commits into
mainfrom
fix-stack-closure-float-return

Conversation

@wtholliday

@wtholliday wtholliday commented Aug 26, 2026

Copy link
Copy Markdown
Collaborator

Fixes #40.

Problem

On the stack backend, closure calls mishandled f32/f64 in both directions. jit, vm, and asm were correct.

The interpreter passes floats through the int window across a call boundary: op_call / op_call_closure copy arguments out of the int TOS window, and return values come back through t0. The direct-call path bridges both ends (FToBitsF/DToBitsD going in, BitsToFF/BitsToDD coming out). Both CallClosure sites did neither — they only pushed a placeholder for the Void case.

Returns (the reported bug in #40) — the next float-window op ran on an empty window, which the compiler's own balance check flagged as apply: f-window underflow at op 3 (FToBitsF): in=0 delta=-1:

apply(f: i32 -> f32) -> f32 { f(0) }
main { var x = 1.5; assert(apply((|i| x + 1.0)) == 2.5) }
# jit/vm/asm: assert(true)   stack: assert(false) -> trap

Arguments (found in review, second commit) — the mirror image. A non-literal float argument reached the callee as garbage; a literal happened to work, which is why it hid:

main { var g = (|a: f32| a + 1.0); var v = 2.0; assert(g(v) == 3.0) }
# jit/vm/asm: assert(true)   stack: assert(false) -> trap

Fix

Two helpers now own the CallClosure ABI, used by both call sites:

  • push_closure_args — bridges f32/f64 args into the int window before the call.
  • bridge_call_result — pushes the Void placeholder, or bridges an f32/f64 return back into the float/double window.

Test

tests/cases/lambdas/lambda_float_return.lyte covers f32 and f64 returns from a closure passed to a higher-order function, a call through a local variable holding a closure, and f32/f64 arguments passed non-literally. Golden tests run once per backend, so all of this is checked on jit/vm/asm/stack.

cargo test --workspace is green (358 lib + 4 golden + 21 lsp).

Out of scope

Two adjacent pre-existing bugs found during review, filed separately rather than expanded into this PR:

🤖 Generated with Claude Code

wtholliday and others added 2 commits August 26, 2026 07:16
f32/f64 return values come back through t0 (the int window) and must be
moved into the float/double window. The direct-call path did this with
BitsToFF/BitsToDD, but both CallClosure sites only handled the Void
placeholder case, so the next float-window op ran on an empty window and
produced a garbage result (the f-window balance check flagged it too).

Factor the fixup into bridge_call_result and use it at both sites.

Fixes #40.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The mirror image of the return-value bug: op_call_closure copies args out
of the int TOS window, so f32/f64 arguments that rode through the float
window need FToBitsF/DToBitsD first. The direct-call path did this; both
CallClosure sites pushed args with a bare translate_expr, so a non-literal
float argument reached the callee as garbage.

Factor arg pushing into push_closure_args and extend the golden test.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@wtholliday wtholliday changed the title Bridge float returns from closure calls on the stack backend Bridge f32/f64 across closure calls on the stack backend Aug 26, 2026
@wtholliday
wtholliday merged commit b2a2c3f into main Aug 26, 2026
8 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Stack backend: closure returning f32/f64 gives a wrong answer

1 participant