macovedj opened PR #14349 from macovedj:winch-tailcall-callee-clean to bytecodealliance:main:
Adds support for
return_callandreturn_call_indirectto Winch on x86-64 and AArch64.
The implementation changes Winch’s internal calling convention so callees reclaim their aligned incoming stack-argument area before returning. This applies to ordinary calls as well as tail calls. Tail calls can then replace the current frame and resize the argument area without requiring the original caller to recover SP afterward. Cranelift-generated trampolines use the existingcallee_pop_sizemachinery to follow the same convention.
Includes two performance optimizations:
- Reclaim alignment padding and consumed operand spills together when no stack-return area needs to be preserved.
- On x86-64, relocate the return address and perform explicit cleanup before a plain ret, using a compact SP-relative sequence for small frames and an FP-relative fallback for larger frames.
macovedj requested cfallin for a review on PR #14349.
macovedj requested wasmtime-compiler-reviewers for a review on PR #14349.
macovedj requested wasmtime-core-reviewers for a review on PR #14349.
github-actions[bot] added the label cranelift on PR #14349.
github-actions[bot] added the label cranelift:area:machinst on PR #14349.
github-actions[bot] added the label cranelift:area:aarch64 on PR #14349.
github-actions[bot] added the label cranelift:area:x64 on PR #14349.
github-actions[bot] added the label wasmtime:api on PR #14349.
github-actions[bot] added the label wasmtime:config on PR #14349.
github-actions[bot] commented on PR #14349:
Label Messager: wasmtime:config
It looks like you are changing Wasmtime's configuration options. Make sure to
complete this check list:
[ ] If you added a new
Configmethod, you wrote extensive documentation for
it.<details>
Our documentation should be of the following form:
```text
Short, simple summary sentence.More details. These details can be multiple paragraphs. There should be
information about not just the method, but its parameters and results as
well.Is this method fallible? If so, when can it return an error?
Can this method panic? If so, when does it panic?
Example
Optional example here.
```</details>
[ ] If you added a new
Configmethod, or modified an existing one, you
ensured that this configuration is exercised by the fuzz targets.<details>
For example, if you expose a new strategy for allocating the next instance
slot inside the pooling allocator, you should ensure that at least one of our
fuzz targets exercises that new strategy.Often, all that is required of you is to ensure that there is a knob for this
configuration option in [wasmtime_fuzzing::Config][fuzzing-config] (or one
of its nestedstructs).Rarely, this may require authoring a new fuzz target to specifically test this
configuration. See [our docs on fuzzing][fuzzing-docs] for more details.</details>
[ ] If you are enabling a configuration option by default, make sure that it
has been fuzzed for at least two weeks before turning it on by default.[fuzzing-config]: https://github.com/bytecodealliance/wasmtime/blob/ca0e8d0a1d8cefc0496dba2f77a670571d8fdcab/crates/fuzzing/src/generators.rs#L182-L194
[fuzzing-docs]: https://docs.wasmtime.dev/contributing-fuzzing.html
<details>
To modify this label's message, edit the <code>.github/label-messager/wasmtime-config.md</code> file.
To add new label messages or remove existing label messages, edit the
<code>.github/label-messager.json</code> configuration file.</details>
cfallin commented on PR #14349:
Thanks for this -- I will be able to review next week (currently on a work trip with limited availability).
:memo: saulecabrera submitted PR review:
Did a first pass; thanks for all the changes here. FWIW, @macovedj and myself discussed some of the trade-offs here between a caller and callee pop approach.
The following programs fail to compile:
(module (type $t (func (result i32))) (table 1 funcref) (func (export "f") (param i32) (result i32) (if (local.get 0) (then (return_call_indirect (type $t) (i32.const 0)))) (if (local.get 0) (then (return_call_indirect (type $t) (i32.const 0)))) (if (local.get 0) (then (return_call_indirect (type $t) (i32.const 0)))) (if (local.get 0) (then (return_call_indirect (type $t) (i32.const 0)))) (if (local.get 0) (then (return_call_indirect (type $t) (i32.const 0)))) (i32.const 0) ) )When invoked via
target/release/wasmtime -C compiler=winch -W tail-call issue_a.watIt fails with
Winch internal error: Expected register to be available.I think we are failing to free the register in
emit_return?(module (func $f (return_call $f) (loop) ) )When invoked using epoch or using fuel interruption, like:
wasmtime compile -C compiler=winch -W tail-call,fuel=1000 <wasm>wasmtime compile -C compiler=winch -W tail-call,epoch-interruption=y <wasm>It fails with:
Winch internal error: Invalid local offsetIs it possible that we are not handling SP correctly at
emit_return?Aside from the issues above, I think we'd also want to:
- Enable tail calls in the fuzzer configuration
- Update the doc comment in
Config::wasm_tail_call
:memo: saulecabrera submitted PR review:
Did a first pass; thanks for all the changes here. FWIW, @macovedj and myself discussed some of the trade-offs here between a caller and callee pop approach.
The following programs fail to compile:
(module (type $t (func (result i32))) (table 1 funcref) (func (export "f") (param i32) (result i32) (if (local.get 0) (then (return_call_indirect (type $t) (i32.const 0)))) (if (local.get 0) (then (return_call_indirect (type $t) (i32.const 0)))) (if (local.get 0) (then (return_call_indirect (type $t) (i32.const 0)))) (if (local.get 0) (then (return_call_indirect (type $t) (i32.const 0)))) (if (local.get 0) (then (return_call_indirect (type $t) (i32.const 0)))) (i32.const 0) ) )When invoked via
target/release/wasmtime -C compiler=winch -W tail-call <wasm>It fails with
Winch internal error: Expected register to be available.I think we are failing to free the register in
emit_return?(module (func $f (return_call $f) (loop) ) )When invoked using epoch or using fuel interruption, like:
wasmtime compile -C compiler=winch -W tail-call,fuel=1000 <wasm>wasmtime compile -C compiler=winch -W tail-call,epoch-interruption=y <wasm>It fails with:
Winch internal error: Invalid local offsetIs it possible that we are not handling SP correctly at
emit_return?Aside from the issues above, I think we'd also want to:
- Enable tail calls in the fuzzer configuration
- Update the doc comment in
Config::wasm_tail_call
macovedj updated PR #14349.
macovedj requested fitzgen for a review on PR #14349.
macovedj requested wasmtime-fuzz-reviewers for a review on PR #14349.
macovedj commented on PR #14349:
Thanks Saul!
I’ve pushed an update addressing the feedback. It updates the documentation and fuzz configuration, releases the temporary callee registers after tail-call lowering, and skips fuel/epoch checks for unreachable loops while preserving the control-stack bookkeeping. I also added regressions for the register cleanup and unreachable-loop cases.
macovedj edited a comment on PR #14349:
Thanks @saulecabrera!
I’ve pushed an update addressing the feedback. It updates the documentation and fuzz configuration, releases the temporary callee registers after tail-call lowering, and skips fuel/epoch checks for unreachable loops while preserving the control-stack bookkeeping. I also added regressions for the register cleanup and unreachable-loop cases.
github-actions[bot] added the label fuzzing on PR #14349.
github-actions[bot] commented on PR #14349:
Subscribe to Label Action
cc @fitzgen
<details>
This issue or pull request has been labeled: "cranelift", "cranelift:area:aarch64", "cranelift:area:machinst", "cranelift:area:x64", "fuzzing", "wasmtime:api", "wasmtime:config"Thus the following users have been cc'd because of the following labels:
- fitzgen: fuzzing
To subscribe or unsubscribe from this label, edit the <code>.github/subscribe-to-label.json</code> configuration file.
Learn more.
</details>
Last updated: Sep 20 2026 at 18:08 UTC