| /* |
| * 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. Also avoid optimizing unreachable calls (e.g. |
| // calls with unreachable operands), since their callee return type may not |
| // match the caller return type. |
| if (call->isReturn || !isTail || call->type == Type::unreachable || |
| !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 |