Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions scripts/fuzz_opt.py
Original file line number Diff line number Diff line change
Expand Up @@ -2336,6 +2336,10 @@ def do_run(vm, js, wasm):
#
# Ignore it, as details of traces differ based on optimizations.
continue
elif not line:
# V8 may print blank lines before stack traces when the top
# frame has no script location (e.g. after a return_call to JS).
continue
cleaned.append(line)
cleaned = '\n'.join(cleaned)

Expand Down Expand Up @@ -2765,6 +2769,7 @@ def write_commands(commands, filename):
("--simplify-locals-notee",),
("--simplify-locals-notee-nostructure",),
("--ssa",),
("--tail-call",),
("--tuple-optimization",),
("--type-finalizing",),
("--type-refining",),
Expand Down
1 change: 1 addition & 0 deletions src/passes/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,7 @@ set(passes_SOURCES
StackCheck.cpp
StripEH.cpp
SSAify.cpp
TailCall.cpp
TupleOptimization.cpp
TranslateEH.cpp
TypeFinalizing.cpp
Expand Down
339 changes: 339 additions & 0 deletions src/passes/TailCall.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,339 @@
/*
* 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.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 {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The PreWalker can be extracted from the current pass file and used as a shared helper class.

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 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 Pass, public PreWalker<TailCall> {
bool isFunctionParallel() override { return true; }

std::unique_ptr<Pass> create() override {
return std::make_unique<TailCall>();
}

Module* module = nullptr;
Function* func = nullptr;

// 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(), *module, 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;
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rather than a custom ::scan(), can we use ExpressionStackWalker? When we see a call we can walk up the stack of parents and see that they are just fallthroughs.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We would still need to duplicate a lot of the logic here. For example, the part where we look for side effects in br_table and br_if conditions or the part where we check whether the next instruction in a block is a return or a br to another tail-position block. And we wouldn't be able to use any generic helpers relating to fallthrough expressions because many fallthrough expressions like casts are not relevant here.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe I'm not understanding what the br_if logic does. What is it actually handling?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you have (br_if $out (call $tail) (cond-with-side-effect)), then you can't optimize even if the br_if itself is in tail position because the side effect is executed between the call and the end of the function.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, yes, but that is done inside getImmediateFallthrough?

In my mind we just need to get the expression stack, then go from the call up to parents, checking falling-through works in each step?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another complication is that for e.g. br_table, we would need to essentially do a DAG traversal of potentially many parent labels to make sure they all end up being in tail position. In contrast, the current approach can easily check this because it maintains a set of tail-position labels.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah yes, for br_table you do need a more complex analysis. But br_table is rare, br_table with a value even rarer, with a call rarer still, and to be in tail call position... well it's not impossible 😄 But if that is the only case, I think the simplification of this pass would be the better option, it should be much shorter.


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, func->getResults())) {
return;
}
if (ehDepth > 0 &&
ShallowEffectAnalyzer(getPassOptions(), *module, 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 (func->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 run(Module* module) override {
assert(getPassRunner());
auto options = getPassOptions();
options.optimizeLevel = std::min(options.optimizeLevel, 1);
options.shrinkLevel = std::min(options.shrinkLevel, 1);
PassRunner runner(module, options);
runner.setIsNested(true);
runner.add(create());
runner.run();
}

void runOnFunction(Module* module, Function* func) override {
if (!module->features.hasTailCall() || func->imported()) {
return;
}
this->module = module;
this->func = func;
walk(func->body);
if (changed) {
ReFinalize().walkFunctionInModule(func, module);
PassRunner runner(module, getPassOptions());
runner.setIsNested(true);
runner.add("dce");
runner.runOnFunction(func);
}
}
};

} // anonymous namespace

Pass* createTailCallPass() { return new TailCall(); }

} // namespace wasm
3 changes: 3 additions & 0 deletions src/passes/pass.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -555,6 +555,9 @@ void PassRegistry::registerPasses() {
registerPass("stack-check",
"enforce limits on llvm's __stack_pointer global",
createStackCheckPass);
registerPass("tail-call",
"convert calls in tail position to return calls",
createTailCallPass);
registerPass("strip-debug",
"strip debug info (including the names section)",
createStripDebugPass);
Expand Down
1 change: 1 addition & 0 deletions src/passes/passes.h
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,7 @@ Pass* createStubUnsupportedJSOpsPass();
Pass* createSSAifyPass();
Pass* createSSAifyNoMergePass();
Pass* createTable64LoweringPass();
Pass* createTailCallPass();
Pass* createTranslateToExnrefPass();
Pass* createTupleOptimizationPass();
Pass* createTypeGeneralizingPass();
Expand Down
3 changes: 3 additions & 0 deletions test/lit/help/wasm-metadce.test
Original file line number Diff line number Diff line change
Expand Up @@ -548,6 +548,9 @@
;; CHECK-EMPTY:
;; CHECK-NEXT: --table64-lowering alias for memory64-lowering
;; CHECK-EMPTY:
;; CHECK-NEXT: --tail-call convert calls in tail position
;; CHECK-NEXT: to return calls
;; CHECK-EMPTY:
;; CHECK-NEXT: --trace-calls instrument the build with code
;; CHECK-NEXT: to intercept specific function
;; CHECK-NEXT: calls
Expand Down
Loading
Loading