current commit
260917.1 · [TailCall] Fix intrinsic and unreachable calls (#9111)
/*
 * 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