-
Notifications
You must be signed in to change notification settings - Fork 885
Add TailCall optimization pass (--tail-call) #9103
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
10 commits
Select commit
Hold shift + click to select a range
0491d0c
Add TailCall optimization pass (--tail-call)
tlively 6c0a4e3
fuzzer
tlively 2338ce6
simplified ExpressionStackWalker approach
tlively 54bb39f
Revert "simplified ExpressionStackWalker approach"
tlively 6143e8d
prewalker
tlively 572c963
Merge remote-tracking branch 'origin/main' into return-call-opt
tlively ba1c611
Merge branch 'main' into return-call-opt
tlively dc09d9f
do not automatically run dce
tlively fa7e76a
use WalkerPass
tlively 5e96f48
refinalize
tlively File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,324 @@ | ||
| /* | ||
| * Copyright 2026 WebAssembly Community Group participants | ||
| * | ||
| * Licensed under the Apache License, Version 2.0 (the "License"); | ||
| * you may not use this file except in compliance with the License. | ||
| * You may obtain a copy of the License at | ||
| * | ||
| * http://www.apache.org/licenses/LICENSE-2.0 | ||
| * | ||
| * Unless required by applicable law or agreed to in writing, software | ||
| * distributed under the License is distributed on an "AS IS" BASIS, | ||
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| * See the License for the specific language governing permissions and | ||
| * limitations under the License. | ||
| */ | ||
|
|
||
| // | ||
| // Convert calls in tail position to return calls (tail calls). | ||
| // | ||
|
|
||
| #include <unordered_set> | ||
|
|
||
| #include "ir/effects.h" | ||
| #include "ir/properties.h" | ||
| #include "ir/utils.h" | ||
| #include "pass.h" | ||
| #include "wasm-traversal.h" | ||
| #include "wasm.h" | ||
|
|
||
| namespace wasm { | ||
|
|
||
| namespace { | ||
|
|
||
| // We are doing a pre-order traversal (i.e. parents before children) rather | ||
| // than the normal post-order traversal because whether an expression is in | ||
| // tail position is propagated down from parents to children. Define our own | ||
| // pre-order traversal task stack, and take the opportunity to pass `isTail` | ||
| // as an extra parameter to each task rather than storing it in a side table. | ||
| template<typename SubType> | ||
| struct PreWalker : public Walker<SubType, Visitor<SubType>> { | ||
| using TaskFunc = void (*)(SubType*, Expression**, bool); | ||
|
|
||
| struct Task { | ||
| TaskFunc func; | ||
| Expression** currp; | ||
| bool isTail; | ||
| Task() = default; | ||
| Task(TaskFunc func, Expression** currp, bool isTail) | ||
| : func(func), currp(currp), isTail(isTail) {} | ||
| }; | ||
|
|
||
| SmallVector<Task, 10> stack; | ||
|
|
||
| void push(Expression** currp, bool isTail) { | ||
| assert(*currp); | ||
| stack.emplace_back(doVisit, currp, isTail); | ||
| } | ||
|
|
||
| void maybePush(Expression** currp, bool isTail) { | ||
| if (*currp) { | ||
| stack.emplace_back(doVisit, currp, isTail); | ||
| } | ||
| } | ||
|
|
||
| Task popTask() { | ||
| auto ret = stack.back(); | ||
| stack.pop_back(); | ||
| return ret; | ||
| } | ||
|
|
||
| static void doVisit(SubType* self, Expression** currp, bool isTail) { | ||
| self->visit(*currp, isTail); | ||
| } | ||
|
|
||
| void walk(Expression*& root) { | ||
| assert(stack.empty()); | ||
| push(&root, true); | ||
| while (!stack.empty()) { | ||
| auto task = popTask(); | ||
| task.func(static_cast<SubType*>(this), task.currp, task.isTail); | ||
| } | ||
| } | ||
|
|
||
| void doWalkFunction(Function* func) { walk(func->body); } | ||
|
|
||
| void visitExpression(Expression* curr, bool isTail) { | ||
| assert(!Properties::isControlFlowStructure(curr) && | ||
| "unexpected control flow structure"); | ||
|
|
||
| #define DELEGATE_ID curr->_id | ||
| #define DELEGATE_START(id) [[maybe_unused]] auto* cast = curr->cast<id>(); | ||
| #define DELEGATE_END(id) | ||
| #define DELEGATE_GET_FIELD(id, field) cast->field | ||
| #define DELEGATE_FIELD_CHILD(id, field) push(&cast->field, false); | ||
| #define DELEGATE_FIELD_OPTIONAL_CHILD(id, field) maybePush(&cast->field, false); | ||
| #define DELEGATE_FIELD_INT(id, field) | ||
| #define DELEGATE_FIELD_LITERAL(id, field) | ||
| #define DELEGATE_FIELD_NAME(id, field) | ||
| #define DELEGATE_FIELD_SCOPE_NAME_DEF(id, field) | ||
| #define DELEGATE_FIELD_SCOPE_NAME_USE(id, field) | ||
| #define DELEGATE_FIELD_TYPE(id, field) | ||
| #define DELEGATE_FIELD_HEAPTYPE(id, field) | ||
| #define DELEGATE_FIELD_ADDRESS(id, field) | ||
|
|
||
| #include "wasm-delegations-fields.def" | ||
| } | ||
|
|
||
| #define DELEGATE(CLASS_TO_VISIT) \ | ||
| void visit##CLASS_TO_VISIT(CLASS_TO_VISIT* curr, bool isTail) { \ | ||
| static_cast<SubType*>(this)->visitExpression(curr, isTail); \ | ||
| } | ||
|
|
||
| #include "wasm-delegations.def" | ||
|
|
||
| void visit(Expression* curr, bool isTail) { | ||
| assert(curr); | ||
| switch (curr->_id) { | ||
| #define DELEGATE(CLASS_TO_VISIT) \ | ||
| case Expression::Id::CLASS_TO_VISIT##Id: \ | ||
| return static_cast<SubType*>(this)->visit##CLASS_TO_VISIT( \ | ||
| static_cast<CLASS_TO_VISIT*>(curr), isTail); | ||
|
|
||
| #include "wasm-delegations.def" | ||
|
|
||
| default: | ||
| WASM_UNREACHABLE("unexpected expression type"); | ||
| } | ||
| } | ||
| }; | ||
|
|
||
| struct TailCall : public WalkerPass<PreWalker<TailCall>> { | ||
| bool isFunctionParallel() override { return true; } | ||
|
|
||
| std::unique_ptr<Pass> create() override { | ||
| return std::make_unique<TailCall>(); | ||
| } | ||
|
|
||
| // Names of blocks whose exit flows directly out of the function. | ||
| std::unordered_set<Name> tailBlocks; | ||
| // Nesting depth of active exception handlers that catch or redirect | ||
| // exceptions within the current function. | ||
| size_t ehDepth = 0; | ||
| // Whether any call in the current function was converted to a return call. | ||
| bool changed = false; | ||
|
|
||
| void pushEnterTry() { stack.emplace_back(doEnterTryBody, nullptr, false); } | ||
|
|
||
| void pushLeaveTry() { stack.emplace_back(doLeaveTryBody, nullptr, false); } | ||
|
|
||
| static void doEnterTryBody(TailCall* self, Expression**, bool) { | ||
| ++self->ehDepth; | ||
| } | ||
|
|
||
| static void doLeaveTryBody(TailCall* self, Expression**, bool) { | ||
| assert(self->ehDepth > 0); | ||
| --self->ehDepth; | ||
| } | ||
|
|
||
| bool hasUnremovableSideEffects(Expression* expr) { | ||
| return EffectAnalyzer(getPassOptions(), *getModule(), expr) | ||
| .hasUnremovableSideEffects(); | ||
| } | ||
|
|
||
| bool allTargetsInTailBlocks(Switch* curr) { | ||
| if (!tailBlocks.contains(curr->default_)) { | ||
| return false; | ||
| } | ||
| for (auto target : curr->targets) { | ||
| if (!tailBlocks.contains(target)) { | ||
| return false; | ||
| } | ||
| } | ||
| return true; | ||
| } | ||
|
|
||
| bool isTailTransfer(Break* curr, bool isTail) { | ||
| if (!tailBlocks.contains(curr->name)) { | ||
| return false; | ||
| } | ||
| // Converting a call in a conditional branch or br_table to a return_call | ||
| // skips evaluating the condition, so the condition must not have side | ||
| // effects. | ||
| return !curr->condition || | ||
| (isTail && !hasUnremovableSideEffects(curr->condition)); | ||
| } | ||
|
|
||
| bool isTailTransfer(Switch* curr) { | ||
| return allTargetsInTailBlocks(curr) && | ||
| !hasUnremovableSideEffects(curr->condition); | ||
| } | ||
|
|
||
| bool isTailTransfer(Expression* curr, bool isTail) { | ||
| if (curr->is<Return>()) { | ||
| return true; | ||
| } | ||
| if (auto* br = curr->dynCast<Break>()) { | ||
| return isTailTransfer(br, isTail); | ||
| } | ||
| if (auto* sw = curr->dynCast<Switch>()) { | ||
| return isTailTransfer(sw); | ||
| } | ||
| return false; | ||
| } | ||
|
|
||
| template<typename CallType> void handleCall(CallType* call, bool isTail) { | ||
| // A call in tail position can have a type incompatible with the function's | ||
| // return type if it is dead code at the end of a block following an earlier | ||
| // unreachable instruction. | ||
| if (call->isReturn || !isTail || | ||
| !Type::isSubType(call->type, getFunction()->getResults())) { | ||
| return; | ||
| } | ||
| if (ehDepth > 0 && | ||
| ShallowEffectAnalyzer(getPassOptions(), *getModule(), call).throws()) { | ||
| return; | ||
| } | ||
| call->isReturn = true; | ||
| call->finalize(); | ||
| changed = true; | ||
| } | ||
|
|
||
| void visitBlock(Block* curr, bool isTail) { | ||
| if (isTail && curr->name.is()) { | ||
| tailBlocks.insert(curr->name); | ||
| } | ||
| bool nextIsTail = isTail; | ||
| for (int i = int(curr->list.size()) - 1; i >= 0; --i) { | ||
| bool itemIsTail = false; | ||
| if (i == int(curr->list.size()) - 1) { | ||
| itemIsTail = isTail; | ||
| } else if (getFunction()->getResults() == Type::none && | ||
| isTailTransfer(curr->list[i + 1], nextIsTail)) { | ||
| itemIsTail = true; | ||
| } | ||
| nextIsTail = itemIsTail; | ||
| push(&curr->list[i], itemIsTail); | ||
| } | ||
| } | ||
|
|
||
| void visitIf(If* curr, bool isTail) { | ||
| maybePush(&curr->ifFalse, isTail); | ||
| push(&curr->ifTrue, isTail); | ||
| push(&curr->condition, false); | ||
| } | ||
|
|
||
| void visitLoop(Loop* curr, bool isTail) { push(&curr->body, isTail); } | ||
|
|
||
| void visitBreak(Break* curr, bool isTail) { | ||
| bool valueIsTail = curr->value && isTailTransfer(curr, isTail); | ||
| maybePush(&curr->condition, false); | ||
| maybePush(&curr->value, valueIsTail); | ||
| } | ||
|
|
||
| void visitSwitch(Switch* curr, bool isTail) { | ||
| bool valueIsTail = curr->value && isTailTransfer(curr); | ||
| push(&curr->condition, false); | ||
| maybePush(&curr->value, valueIsTail); | ||
| } | ||
|
|
||
| void visitReturn(Return* curr, bool isTail) { maybePush(&curr->value, true); } | ||
|
|
||
| void visitTry(Try* curr, bool isTail) { | ||
| for (int i = int(curr->catchBodies.size()) - 1; i >= 0; --i) { | ||
| push(&curr->catchBodies[i], isTail); | ||
| } | ||
| // A try block that delegates directly to the caller does not catch any | ||
| // exceptions in this function; exceptions thrown in its body already unwind | ||
| // the frame to the caller just like a return_call would. All other try | ||
| // blocks (catch/catch_all or delegating to an outer try) establish a local | ||
| // handler that would be bypassed by return_call. | ||
| bool hasLocalHandler = | ||
| !curr->isDelegate() || curr->delegateTarget != DELEGATE_CALLER_TARGET; | ||
| if (hasLocalHandler) { | ||
| pushLeaveTry(); | ||
| } | ||
| push(&curr->body, isTail); | ||
| if (hasLocalHandler) { | ||
| pushEnterTry(); | ||
| } | ||
| } | ||
|
|
||
| void visitTryTable(TryTable* curr, bool isTail) { | ||
| bool hasLocalHandler = !curr->catchTags.empty(); | ||
| if (hasLocalHandler) { | ||
| pushLeaveTry(); | ||
| } | ||
| push(&curr->body, isTail); | ||
| if (hasLocalHandler) { | ||
| pushEnterTry(); | ||
| } | ||
| } | ||
|
|
||
| void visitCall(Call* curr, bool isTail) { | ||
| handleCall(curr, isTail); | ||
| visitExpression(curr, false); | ||
| } | ||
|
|
||
| void visitCallIndirect(CallIndirect* curr, bool isTail) { | ||
| handleCall(curr, isTail); | ||
| visitExpression(curr, false); | ||
| } | ||
|
|
||
| void visitCallRef(CallRef* curr, bool isTail) { | ||
| handleCall(curr, isTail); | ||
| visitExpression(curr, false); | ||
| } | ||
|
|
||
| void doWalkFunction(Function* func) { | ||
| if (!getModule()->features.hasTailCall() || func->imported()) { | ||
| return; | ||
| } | ||
| tailBlocks.clear(); | ||
| walk(func->body); | ||
| if (changed) { | ||
| ReFinalize().walkFunctionInModule(func, getModule()); | ||
| } | ||
| } | ||
| }; | ||
|
|
||
| } // anonymous namespace | ||
|
|
||
| Pass* createTailCallPass() { return new TailCall(); } | ||
|
|
||
| } // namespace wasm | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.