diff --git a/scripts/fuzz_opt.py b/scripts/fuzz_opt.py index 40a75bda261..67b00e7b94d 100755 --- a/scripts/fuzz_opt.py +++ b/scripts/fuzz_opt.py @@ -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) @@ -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",), diff --git a/src/passes/CMakeLists.txt b/src/passes/CMakeLists.txt index 5924e3b7e76..ad32083d78e 100644 --- a/src/passes/CMakeLists.txt +++ b/src/passes/CMakeLists.txt @@ -137,6 +137,7 @@ set(passes_SOURCES StackCheck.cpp StripEH.cpp SSAify.cpp + TailCall.cpp TupleOptimization.cpp TranslateEH.cpp TypeFinalizing.cpp diff --git a/src/passes/TailCall.cpp b/src/passes/TailCall.cpp new file mode 100644 index 00000000000..9f7a8581ace --- /dev/null +++ b/src/passes/TailCall.cpp @@ -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 + +#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 +struct PreWalker : public Walker> { + 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 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(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(); +#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(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(this)->visit##CLASS_TO_VISIT( \ + static_cast(curr), isTail); + +#include "wasm-delegations.def" + + default: + WASM_UNREACHABLE("unexpected expression type"); + } + } +}; + +struct TailCall : public WalkerPass> { + bool isFunctionParallel() override { return true; } + + std::unique_ptr create() override { + return std::make_unique(); + } + + // Names of blocks whose exit flows directly out of the function. + std::unordered_set 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 true; + } + if (auto* br = curr->dynCast()) { + return isTailTransfer(br, isTail); + } + if (auto* sw = curr->dynCast()) { + return isTailTransfer(sw); + } + return false; + } + + template 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 diff --git a/src/passes/pass.cpp b/src/passes/pass.cpp index d93bb0876cb..13f23c29bd2 100644 --- a/src/passes/pass.cpp +++ b/src/passes/pass.cpp @@ -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); diff --git a/src/passes/passes.h b/src/passes/passes.h index b6242cf5259..659c6ca7ee9 100644 --- a/src/passes/passes.h +++ b/src/passes/passes.h @@ -190,6 +190,7 @@ Pass* createStubUnsupportedJSOpsPass(); Pass* createSSAifyPass(); Pass* createSSAifyNoMergePass(); Pass* createTable64LoweringPass(); +Pass* createTailCallPass(); Pass* createTranslateToExnrefPass(); Pass* createTupleOptimizationPass(); Pass* createTypeGeneralizingPass(); diff --git a/test/lit/help/wasm-metadce.test b/test/lit/help/wasm-metadce.test index d6b80d5bf89..74b81fd9872 100644 --- a/test/lit/help/wasm-metadce.test +++ b/test/lit/help/wasm-metadce.test @@ -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 diff --git a/test/lit/help/wasm-opt.test b/test/lit/help/wasm-opt.test index ff37a52d159..73f66e05193 100644 --- a/test/lit/help/wasm-opt.test +++ b/test/lit/help/wasm-opt.test @@ -584,6 +584,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 diff --git a/test/lit/help/wasm2js.test b/test/lit/help/wasm2js.test index 33c963860e1..7bb19f146f6 100644 --- a/test/lit/help/wasm2js.test +++ b/test/lit/help/wasm2js.test @@ -512,6 +512,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 diff --git a/test/lit/passes/tail-call-eh.wast b/test/lit/passes/tail-call-eh.wast new file mode 100644 index 00000000000..1478084a901 --- /dev/null +++ b/test/lit/passes/tail-call-eh.wast @@ -0,0 +1,377 @@ +;; NOTE: Assertions have been generated by update_lit_checks.py and should not be edited. +;; Test tail-call optimization interaction with exception handling and GlobalEffects. + +;; RUN: wasm-opt %s --tail-call --enable-tail-call --enable-exception-handling --enable-reference-types -S -o - | filecheck %s --check-prefix=NO-EFFECTS +;; RUN: wasm-opt %s --generate-global-effects --tail-call --enable-tail-call --enable-exception-handling --enable-reference-types -S -o - | filecheck %s --check-prefix=WITH-EFFECTS + +(module + ;; NO-EFFECTS: (tag $e1 (type $0)) + ;; WITH-EFFECTS: (tag $e1 (type $0)) + (tag $e1) + ;; NO-EFFECTS: (tag $e2 (type $0)) + ;; WITH-EFFECTS: (tag $e2 (type $0)) + (tag $e2) + + ;; NO-EFFECTS: (func $throwing-callee + ;; NO-EFFECTS-NEXT: (throw $e1) + ;; NO-EFFECTS-NEXT: ) + ;; WITH-EFFECTS: (func $throwing-callee + ;; WITH-EFFECTS-NEXT: (throw $e1) + ;; WITH-EFFECTS-NEXT: ) + (func $throwing-callee + (throw $e1) + ) + + ;; NO-EFFECTS: (func $throwing-i32-callee (result i32) + ;; NO-EFFECTS-NEXT: (throw $e1) + ;; NO-EFFECTS-NEXT: ) + ;; WITH-EFFECTS: (func $throwing-i32-callee (result i32) + ;; WITH-EFFECTS-NEXT: (throw $e1) + ;; WITH-EFFECTS-NEXT: ) + (func $throwing-i32-callee (result i32) + (throw $e1) + ) + + ;; NO-EFFECTS: (func $non-throwing-callee (result i32) + ;; NO-EFFECTS-NEXT: (i32.const 42) + ;; NO-EFFECTS-NEXT: ) + ;; WITH-EFFECTS: (func $non-throwing-callee (result i32) + ;; WITH-EFFECTS-NEXT: (i32.const 42) + ;; WITH-EFFECTS-NEXT: ) + (func $non-throwing-callee (result i32) + (i32.const 42) + ) + + ;; NO-EFFECTS: (func $non-throwing-param-callee (param $x i32) (result i32) + ;; NO-EFFECTS-NEXT: (local.get $x) + ;; NO-EFFECTS-NEXT: ) + ;; WITH-EFFECTS: (func $non-throwing-param-callee (param $x i32) (result i32) + ;; WITH-EFFECTS-NEXT: (local.get $x) + ;; WITH-EFFECTS-NEXT: ) + (func $non-throwing-param-callee (param $x i32) (result i32) + (local.get $x) + ) + + ;; NO-EFFECTS: (func $caller-throws + ;; NO-EFFECTS-NEXT: (block $catch + ;; NO-EFFECTS-NEXT: (try_table (catch_all $catch) + ;; NO-EFFECTS-NEXT: (call $throwing-callee) + ;; NO-EFFECTS-NEXT: ) + ;; NO-EFFECTS-NEXT: ) + ;; NO-EFFECTS-NEXT: ) + ;; WITH-EFFECTS: (func $caller-throws + ;; WITH-EFFECTS-NEXT: (block $catch + ;; WITH-EFFECTS-NEXT: (try_table (catch_all $catch) + ;; WITH-EFFECTS-NEXT: (call $throwing-callee) + ;; WITH-EFFECTS-NEXT: ) + ;; WITH-EFFECTS-NEXT: ) + ;; WITH-EFFECTS-NEXT: ) + (func $caller-throws + ;; Inside try_table, a call that might throw cannot be converted to return_call. + (block $catch + (try_table (catch_all $catch) + (call $throwing-callee) + ) + ) + ) + + ;; NO-EFFECTS: (func $caller-non-throwing-in-try (result i32) + ;; NO-EFFECTS-NEXT: (block $catch + ;; NO-EFFECTS-NEXT: (return + ;; NO-EFFECTS-NEXT: (try_table (result i32) (catch_all $catch) + ;; NO-EFFECTS-NEXT: (call $non-throwing-callee) + ;; NO-EFFECTS-NEXT: ) + ;; NO-EFFECTS-NEXT: ) + ;; NO-EFFECTS-NEXT: ) + ;; NO-EFFECTS-NEXT: (i32.const 0) + ;; NO-EFFECTS-NEXT: ) + ;; WITH-EFFECTS: (func $caller-non-throwing-in-try (result i32) + ;; WITH-EFFECTS-NEXT: (block $catch + ;; WITH-EFFECTS-NEXT: (return + ;; WITH-EFFECTS-NEXT: (try_table (catch_all $catch) + ;; WITH-EFFECTS-NEXT: (return_call $non-throwing-callee) + ;; WITH-EFFECTS-NEXT: ) + ;; WITH-EFFECTS-NEXT: ) + ;; WITH-EFFECTS-NEXT: ) + ;; WITH-EFFECTS-NEXT: (i32.const 0) + ;; WITH-EFFECTS-NEXT: ) + (func $caller-non-throwing-in-try (result i32) + ;; Inside try_table, a call to a non-throwing function CAN be converted when + ;; GlobalEffects is available. + (block $catch + (return + (try_table (result i32) (catch_all $catch) + (call $non-throwing-callee) + ) + ) + ) + (i32.const 0) + ) + + ;; NO-EFFECTS: (func $caller-throwing-arg-in-try (result i32) + ;; NO-EFFECTS-NEXT: (block $catch + ;; NO-EFFECTS-NEXT: (return + ;; NO-EFFECTS-NEXT: (try_table (result i32) (catch_all $catch) + ;; NO-EFFECTS-NEXT: (call $non-throwing-param-callee + ;; NO-EFFECTS-NEXT: (call $throwing-i32-callee) + ;; NO-EFFECTS-NEXT: ) + ;; NO-EFFECTS-NEXT: ) + ;; NO-EFFECTS-NEXT: ) + ;; NO-EFFECTS-NEXT: ) + ;; NO-EFFECTS-NEXT: (i32.const 0) + ;; NO-EFFECTS-NEXT: ) + ;; WITH-EFFECTS: (func $caller-throwing-arg-in-try (result i32) + ;; WITH-EFFECTS-NEXT: (block $catch + ;; WITH-EFFECTS-NEXT: (return + ;; WITH-EFFECTS-NEXT: (try_table (catch_all $catch) + ;; WITH-EFFECTS-NEXT: (return_call $non-throwing-param-callee + ;; WITH-EFFECTS-NEXT: (call $throwing-i32-callee) + ;; WITH-EFFECTS-NEXT: ) + ;; WITH-EFFECTS-NEXT: ) + ;; WITH-EFFECTS-NEXT: ) + ;; WITH-EFFECTS-NEXT: ) + ;; WITH-EFFECTS-NEXT: (i32.const 0) + ;; WITH-EFFECTS-NEXT: ) + (func $caller-throwing-arg-in-try (result i32) + ;; Even if an argument expression may throw, ShallowEffectAnalyzer only + ;; checks whether the call instruction itself can throw, allowing the outer + ;; non-throwing call to be converted when GlobalEffects is available. + (block $catch + (return + (try_table (result i32) (catch_all $catch) + (call $non-throwing-param-callee + (call $throwing-i32-callee) + ) + ) + ) + ) + (i32.const 0) + ) + + ;; NO-EFFECTS: (func $caller-outside-try + ;; NO-EFFECTS-NEXT: (return_call $throwing-callee) + ;; NO-EFFECTS-NEXT: ) + ;; WITH-EFFECTS: (func $caller-outside-try + ;; WITH-EFFECTS-NEXT: (return_call $throwing-callee) + ;; WITH-EFFECTS-NEXT: ) + (func $caller-outside-try + ;; Outside any active try/try_table handler, calls in tail position are + ;; converted even when EH is enabled on the module. + (call $throwing-callee) + ) + + ;; NO-EFFECTS: (func $legacy-try-catch + ;; NO-EFFECTS-NEXT: (try + ;; NO-EFFECTS-NEXT: (do + ;; NO-EFFECTS-NEXT: (call $throwing-callee) + ;; NO-EFFECTS-NEXT: ) + ;; NO-EFFECTS-NEXT: (catch_all + ;; NO-EFFECTS-NEXT: (return_call $throwing-callee) + ;; NO-EFFECTS-NEXT: ) + ;; NO-EFFECTS-NEXT: ) + ;; NO-EFFECTS-NEXT: ) + ;; WITH-EFFECTS: (func $legacy-try-catch + ;; WITH-EFFECTS-NEXT: (try + ;; WITH-EFFECTS-NEXT: (do + ;; WITH-EFFECTS-NEXT: (call $throwing-callee) + ;; WITH-EFFECTS-NEXT: ) + ;; WITH-EFFECTS-NEXT: (catch_all + ;; WITH-EFFECTS-NEXT: (return_call $throwing-callee) + ;; WITH-EFFECTS-NEXT: ) + ;; WITH-EFFECTS-NEXT: ) + ;; WITH-EFFECTS-NEXT: ) + (func $legacy-try-catch + ;; Legacy try-catch: try body is protected by catch handler (ehDepth > 0), + ;; while catch body is outside the handler (ehDepth == 0). + (try + (do + (call $throwing-callee) + ) + (catch_all + (call $throwing-callee) + ) + ) + ) + + ;; NO-EFFECTS: (func $legacy-try-multiple-catches + ;; NO-EFFECTS-NEXT: (try + ;; NO-EFFECTS-NEXT: (do + ;; NO-EFFECTS-NEXT: (call $throwing-callee) + ;; NO-EFFECTS-NEXT: ) + ;; NO-EFFECTS-NEXT: (catch $e1 + ;; NO-EFFECTS-NEXT: (return_call $throwing-callee) + ;; NO-EFFECTS-NEXT: ) + ;; NO-EFFECTS-NEXT: (catch $e2 + ;; NO-EFFECTS-NEXT: (return_call $throwing-callee) + ;; NO-EFFECTS-NEXT: ) + ;; NO-EFFECTS-NEXT: (catch_all + ;; NO-EFFECTS-NEXT: (return_call $throwing-callee) + ;; NO-EFFECTS-NEXT: ) + ;; NO-EFFECTS-NEXT: ) + ;; NO-EFFECTS-NEXT: ) + ;; WITH-EFFECTS: (func $legacy-try-multiple-catches + ;; WITH-EFFECTS-NEXT: (try + ;; WITH-EFFECTS-NEXT: (do + ;; WITH-EFFECTS-NEXT: (call $throwing-callee) + ;; WITH-EFFECTS-NEXT: ) + ;; WITH-EFFECTS-NEXT: (catch $e1 + ;; WITH-EFFECTS-NEXT: (return_call $throwing-callee) + ;; WITH-EFFECTS-NEXT: ) + ;; WITH-EFFECTS-NEXT: (catch $e2 + ;; WITH-EFFECTS-NEXT: (return_call $throwing-callee) + ;; WITH-EFFECTS-NEXT: ) + ;; WITH-EFFECTS-NEXT: (catch_all + ;; WITH-EFFECTS-NEXT: (return_call $throwing-callee) + ;; WITH-EFFECTS-NEXT: ) + ;; WITH-EFFECTS-NEXT: ) + ;; WITH-EFFECTS-NEXT: ) + (func $legacy-try-multiple-catches + ;; Legacy try-catch with multiple catch bodies: all catch bodies are outside + ;; the try handler (ehDepth == 0) and should convert their tail calls. + (try + (do + (call $throwing-callee) + ) + (catch $e1 + (call $throwing-callee) + ) + (catch $e2 + (call $throwing-callee) + ) + (catch_all + (call $throwing-callee) + ) + ) + ) + + ;; NO-EFFECTS: (func $legacy-try-delegate-caller + ;; NO-EFFECTS-NEXT: (try + ;; NO-EFFECTS-NEXT: (do + ;; NO-EFFECTS-NEXT: (return_call $throwing-callee) + ;; NO-EFFECTS-NEXT: ) + ;; NO-EFFECTS-NEXT: (delegate 0) + ;; NO-EFFECTS-NEXT: ) + ;; NO-EFFECTS-NEXT: ) + ;; WITH-EFFECTS: (func $legacy-try-delegate-caller + ;; WITH-EFFECTS-NEXT: (try + ;; WITH-EFFECTS-NEXT: (do + ;; WITH-EFFECTS-NEXT: (return_call $throwing-callee) + ;; WITH-EFFECTS-NEXT: ) + ;; WITH-EFFECTS-NEXT: (delegate 0) + ;; WITH-EFFECTS-NEXT: ) + ;; WITH-EFFECTS-NEXT: ) + (func $legacy-try-delegate-caller + ;; A try block that delegates directly to the caller (0) does not catch any + ;; exceptions locally, so tail calls inside its body can be converted. + (try + (do + (call $throwing-callee) + ) + (delegate 0) + ) + ) + + ;; NO-EFFECTS: (func $legacy-try-delegate-outer + ;; NO-EFFECTS-NEXT: (try $outer + ;; NO-EFFECTS-NEXT: (do + ;; NO-EFFECTS-NEXT: (try + ;; NO-EFFECTS-NEXT: (do + ;; NO-EFFECTS-NEXT: (call $throwing-callee) + ;; NO-EFFECTS-NEXT: ) + ;; NO-EFFECTS-NEXT: (delegate $outer) + ;; NO-EFFECTS-NEXT: ) + ;; NO-EFFECTS-NEXT: ) + ;; NO-EFFECTS-NEXT: (catch_all + ;; NO-EFFECTS-NEXT: (nop) + ;; NO-EFFECTS-NEXT: ) + ;; NO-EFFECTS-NEXT: ) + ;; NO-EFFECTS-NEXT: ) + ;; WITH-EFFECTS: (func $legacy-try-delegate-outer + ;; WITH-EFFECTS-NEXT: (try $outer + ;; WITH-EFFECTS-NEXT: (do + ;; WITH-EFFECTS-NEXT: (try + ;; WITH-EFFECTS-NEXT: (do + ;; WITH-EFFECTS-NEXT: (call $throwing-callee) + ;; WITH-EFFECTS-NEXT: ) + ;; WITH-EFFECTS-NEXT: (delegate $outer) + ;; WITH-EFFECTS-NEXT: ) + ;; WITH-EFFECTS-NEXT: ) + ;; WITH-EFFECTS-NEXT: (catch_all + ;; WITH-EFFECTS-NEXT: (nop) + ;; WITH-EFFECTS-NEXT: ) + ;; WITH-EFFECTS-NEXT: ) + ;; WITH-EFFECTS-NEXT: ) + (func $legacy-try-delegate-outer + ;; A try block that delegates to an outer try with a local catch handler has + ;; ehDepth > 0, so throwing tail calls inside its body must not be converted. + ;; TODO: Optimize the case where a delegate targets an outer block that has + ;; no catch handlers outside of it. + (try $outer + (do + (try + (do + (call $throwing-callee) + ) + (delegate $outer) + ) + ) + (catch_all + (nop) + ) + ) + ) + + ;; NO-EFFECTS: (func $nested-try-tables-reset-count (result i32) + ;; NO-EFFECTS-NEXT: (block $catch1 + ;; NO-EFFECTS-NEXT: (try_table (catch_all $catch1) + ;; NO-EFFECTS-NEXT: (block $catch2 + ;; NO-EFFECTS-NEXT: (try_table (catch_all $catch2) + ;; NO-EFFECTS-NEXT: (return + ;; NO-EFFECTS-NEXT: (call $throwing-i32-callee) + ;; NO-EFFECTS-NEXT: ) + ;; NO-EFFECTS-NEXT: ) + ;; NO-EFFECTS-NEXT: ) + ;; NO-EFFECTS-NEXT: (return + ;; NO-EFFECTS-NEXT: (call $throwing-i32-callee) + ;; NO-EFFECTS-NEXT: ) + ;; NO-EFFECTS-NEXT: ) + ;; NO-EFFECTS-NEXT: ) + ;; NO-EFFECTS-NEXT: (return_call $throwing-i32-callee) + ;; NO-EFFECTS-NEXT: ) + ;; WITH-EFFECTS: (func $nested-try-tables-reset-count (result i32) + ;; WITH-EFFECTS-NEXT: (block $catch1 + ;; WITH-EFFECTS-NEXT: (try_table (catch_all $catch1) + ;; WITH-EFFECTS-NEXT: (block $catch2 + ;; WITH-EFFECTS-NEXT: (try_table (catch_all $catch2) + ;; WITH-EFFECTS-NEXT: (return + ;; WITH-EFFECTS-NEXT: (call $throwing-i32-callee) + ;; WITH-EFFECTS-NEXT: ) + ;; WITH-EFFECTS-NEXT: ) + ;; WITH-EFFECTS-NEXT: ) + ;; WITH-EFFECTS-NEXT: (return + ;; WITH-EFFECTS-NEXT: (call $throwing-i32-callee) + ;; WITH-EFFECTS-NEXT: ) + ;; WITH-EFFECTS-NEXT: ) + ;; WITH-EFFECTS-NEXT: ) + ;; WITH-EFFECTS-NEXT: (return_call $throwing-i32-callee) + ;; WITH-EFFECTS-NEXT: ) + (func $nested-try-tables-reset-count (result i32) + ;; After exiting nested try_table blocks, ehDepth is decremented back to 0, + ;; allowing a subsequent throwing call in tail position to be converted. + (block $catch1 + (try_table (catch_all $catch1) + (block $catch2 + (try_table (catch_all $catch2) + (return + (call $throwing-i32-callee) + ) + ) + ) + (return + (call $throwing-i32-callee) + ) + ) + ) + (call $throwing-i32-callee) + ) +) diff --git a/test/lit/passes/tail-call-tnh.wast b/test/lit/passes/tail-call-tnh.wast new file mode 100644 index 00000000000..191970ce075 --- /dev/null +++ b/test/lit/passes/tail-call-tnh.wast @@ -0,0 +1,144 @@ +;; NOTE: Assertions have been generated by update_lit_checks.py and should not be edited. +;; Test tail-call optimization with --traps-never-happen (-tnh). + +;; RUN: wasm-opt %s --tail-call --enable-tail-call -S -o - | filecheck %s --check-prefix=DEFAULT +;; RUN: wasm-opt %s --tail-call -tnh --enable-tail-call -S -o - | filecheck %s --check-prefix=TNH + +(module + (memory 1 1) + ;; DEFAULT: (global $g (mut i32) (i32.const 0)) + ;; TNH: (global $g (mut i32) (i32.const 0)) + (global $g (mut i32) (i32.const 0)) + + ;; DEFAULT: (func $value-callee (result i32) + ;; DEFAULT-NEXT: (i32.const 1) + ;; DEFAULT-NEXT: ) + ;; TNH: (func $value-callee (result i32) + ;; TNH-NEXT: (i32.const 1) + ;; TNH-NEXT: ) + (func $value-callee (result i32) + (i32.const 1) + ) + + ;; DEFAULT: (func $br-if-trapping-condition (param $x i32) (result i32) + ;; DEFAULT-NEXT: (block $out (result i32) + ;; DEFAULT-NEXT: (br_if $out + ;; DEFAULT-NEXT: (call $value-callee) + ;; DEFAULT-NEXT: (i32.load + ;; DEFAULT-NEXT: (local.get $x) + ;; DEFAULT-NEXT: ) + ;; DEFAULT-NEXT: ) + ;; DEFAULT-NEXT: ) + ;; DEFAULT-NEXT: ) + ;; TNH: (func $br-if-trapping-condition (param $x i32) (result i32) + ;; TNH-NEXT: (block $out + ;; TNH-NEXT: (block + ;; TNH-NEXT: (return_call $value-callee) + ;; TNH-NEXT: (drop + ;; TNH-NEXT: (i32.load + ;; TNH-NEXT: (local.get $x) + ;; TNH-NEXT: ) + ;; TNH-NEXT: ) + ;; TNH-NEXT: ) + ;; TNH-NEXT: ) + ;; TNH-NEXT: ) + (func $br-if-trapping-condition (param $x i32) (result i32) + ;; Without -tnh, the load in the condition might trap, so converting the + ;; call to a return_call (which skips the condition) is disallowed. With + ;; -tnh, the load's trap is ignored and the call becomes a return_call. + (block $out (result i32) + (br_if $out + (call $value-callee) + (i32.load + (local.get $x) + ) + ) + ) + ) + + ;; DEFAULT: (func $br-table-trapping-condition (param $x i32) (result i32) + ;; DEFAULT-NEXT: (block $out1 (result i32) + ;; DEFAULT-NEXT: (block $out2 (result i32) + ;; DEFAULT-NEXT: (br_table $out1 $out2 + ;; DEFAULT-NEXT: (call $value-callee) + ;; DEFAULT-NEXT: (i32.div_s + ;; DEFAULT-NEXT: (i32.const 10) + ;; DEFAULT-NEXT: (local.get $x) + ;; DEFAULT-NEXT: ) + ;; DEFAULT-NEXT: ) + ;; DEFAULT-NEXT: ) + ;; DEFAULT-NEXT: ) + ;; DEFAULT-NEXT: ) + ;; TNH: (func $br-table-trapping-condition (param $x i32) (result i32) + ;; TNH-NEXT: (block $out1 + ;; TNH-NEXT: (block $out2 + ;; TNH-NEXT: (block + ;; TNH-NEXT: (return_call $value-callee) + ;; TNH-NEXT: (drop + ;; TNH-NEXT: (i32.div_s + ;; TNH-NEXT: (i32.const 10) + ;; TNH-NEXT: (local.get $x) + ;; TNH-NEXT: ) + ;; TNH-NEXT: ) + ;; TNH-NEXT: ) + ;; TNH-NEXT: ) + ;; TNH-NEXT: ) + ;; TNH-NEXT: ) + (func $br-table-trapping-condition (param $x i32) (result i32) + ;; Similarly, a potentially trapping division in a br_table condition + ;; prevents tail-call conversion unless -tnh is enabled. + (block $out1 (result i32) + (block $out2 (result i32) + (br_table $out1 $out2 + (call $value-callee) + (i32.div_s + (i32.const 10) + (local.get $x) + ) + ) + ) + ) + ) + + ;; DEFAULT: (func $br-if-non-trap-side-effect (result i32) + ;; DEFAULT-NEXT: (block $out (result i32) + ;; DEFAULT-NEXT: (br_if $out + ;; DEFAULT-NEXT: (call $value-callee) + ;; DEFAULT-NEXT: (block (result i32) + ;; DEFAULT-NEXT: (global.set $g + ;; DEFAULT-NEXT: (i32.const 1) + ;; DEFAULT-NEXT: ) + ;; DEFAULT-NEXT: (i32.const 0) + ;; DEFAULT-NEXT: ) + ;; DEFAULT-NEXT: ) + ;; DEFAULT-NEXT: ) + ;; DEFAULT-NEXT: ) + ;; TNH: (func $br-if-non-trap-side-effect (result i32) + ;; TNH-NEXT: (block $out (result i32) + ;; TNH-NEXT: (br_if $out + ;; TNH-NEXT: (call $value-callee) + ;; TNH-NEXT: (block (result i32) + ;; TNH-NEXT: (global.set $g + ;; TNH-NEXT: (i32.const 1) + ;; TNH-NEXT: ) + ;; TNH-NEXT: (i32.const 0) + ;; TNH-NEXT: ) + ;; TNH-NEXT: ) + ;; TNH-NEXT: ) + ;; TNH-NEXT: ) + (func $br-if-non-trap-side-effect (result i32) + ;; Non-trap side effects (like global.set) in a condition still inhibit + ;; optimization even when -tnh is enabled. + (block $out (result i32) + (br_if $out + (call $value-callee) + (block (result i32) + (global.set $g + (i32.const 1) + ) + (i32.const 0) + ) + ) + ) + ) +) diff --git a/test/lit/passes/tail-call.wast b/test/lit/passes/tail-call.wast new file mode 100644 index 00000000000..67ea18728f0 --- /dev/null +++ b/test/lit/passes/tail-call.wast @@ -0,0 +1,582 @@ +;; NOTE: Assertions have been generated by update_lit_checks.py and should not be edited. +;; Test converting calls in tail position to return calls. + +;; RUN: wasm-opt %s --tail-call --enable-tail-call --enable-gc --enable-reference-types --enable-multivalue -S -o - | filecheck %s + +(module + ;; CHECK: (type $none-to-i32 (func (result i32))) + + ;; CHECK: (type $none-to-none (func)) + (type $none-to-none (func)) + (type $none-to-i32 (func (result i32))) + ;; CHECK: (type $struct (sub (struct))) + (type $struct (sub (struct))) + ;; CHECK: (global $g (mut i32) (i32.const 0)) + (global $g (mut i32) (i32.const 0)) + ;; CHECK: (table $table 1 funcref) + (table $table 1 funcref) + + ;; CHECK: (func $void-callee (type $none-to-none) + ;; CHECK-NEXT: ) + (func $void-callee) + ;; CHECK: (func $value-callee (type $none-to-i32) (result i32) + ;; CHECK-NEXT: (i32.const 1) + ;; CHECK-NEXT: ) + (func $value-callee (result i32) + (i32.const 1) + ) + ;; CHECK: (func $ref-callee (type $none-to-i32) (result i32) + ;; CHECK-NEXT: (i32.const 2) + ;; CHECK-NEXT: ) + (func $ref-callee (result i32) + (i32.const 2) + ) + ;; CHECK: (func $param-callee (type $6) (param $x i32) (param $y i32) (result i32) + ;; CHECK-NEXT: (i32.add + ;; CHECK-NEXT: (local.get $x) + ;; CHECK-NEXT: (local.get $y) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $param-callee (param $x i32) (param $y i32) (result i32) + (i32.add + (local.get $x) + (local.get $y) + ) + ) + ;; CHECK: (func $subtype-callee (type $7) (result (ref $struct)) + ;; CHECK-NEXT: (struct.new_default $struct) + ;; CHECK-NEXT: ) + (func $subtype-callee (result (ref $struct)) + (struct.new_default $struct) + ) + ;; CHECK: (func $multivalue-callee (type $5) (result i32 i64) + ;; CHECK-NEXT: (tuple.make 2 + ;; CHECK-NEXT: (i32.const 1) + ;; CHECK-NEXT: (i64.const 2) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $multivalue-callee (result i32 i64) + (tuple.make 2 + (i32.const 1) + (i64.const 2) + ) + ) + (elem (i32.const 0) $value-callee) + + ;; CHECK: (func $direct-void (type $none-to-none) + ;; CHECK-NEXT: (return_call $void-callee) + ;; CHECK-NEXT: ) + (func $direct-void + ;; A direct void call at the end of a function becomes a return_call. + (call $void-callee) + ) + + ;; CHECK: (func $direct-void-ret (type $none-to-none) + ;; CHECK-NEXT: (return_call $void-callee) + ;; CHECK-NEXT: (return) + ;; CHECK-NEXT: ) + (func $direct-void-ret + ;; A void call immediately preceding a return becomes a return_call. + (call $void-callee) + (return) + ) + + ;; CHECK: (func $direct-value (type $none-to-i32) (result i32) + ;; CHECK-NEXT: (return + ;; CHECK-NEXT: (return_call $value-callee) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $direct-value (result i32) + ;; A return wrapping a call becomes a return_call. + (return + (call $value-callee) + ) + ) + + ;; CHECK: (func $call-with-params (type $1) (param $x i32) (result i32) + ;; CHECK-NEXT: (return + ;; CHECK-NEXT: (return_call $param-callee + ;; CHECK-NEXT: (local.get $x) + ;; CHECK-NEXT: (block (result i32) + ;; CHECK-NEXT: (global.set $g + ;; CHECK-NEXT: (i32.const 1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (call $value-callee) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $call-with-params (param $x i32) (result i32) + ;; Calls with parameters, including side-effecting parameter expressions, + ;; are still converted to return_call because operands are evaluated before + ;; the call transfers control. + (return + (call $param-callee + (local.get $x) + (block (result i32) + (global.set $g + (i32.const 1) + ) + (call $value-callee) + ) + ) + ) + ) + + ;; CHECK: (func $subtype-return (type $8) (result anyref) + ;; CHECK-NEXT: (return_call $subtype-callee) + ;; CHECK-NEXT: ) + (func $subtype-return (result anyref) + ;; A call returning a subtype of the enclosing function's result type can be + ;; converted to return_call. + (call $subtype-callee) + ) + + ;; CHECK: (func $multivalue (type $5) (result i32 i64) + ;; CHECK-NEXT: (return_call $multivalue-callee) + ;; CHECK-NEXT: ) + (func $multivalue (result i32 i64) + ;; Multivalue return calls are supported. + (call $multivalue-callee) + ) + + ;; CHECK: (func $conditional (type $1) (param $condition i32) (result i32) + ;; CHECK-NEXT: (if + ;; CHECK-NEXT: (local.get $condition) + ;; CHECK-NEXT: (then + ;; CHECK-NEXT: (return_call $value-callee) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (else + ;; CHECK-NEXT: (return_call $ref-callee) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $conditional (param $condition i32) (result i32) + ;; Both arms of an if in tail position are converted to return_call. + (if (result i32) + (local.get $condition) + (then + (call $value-callee) + ) + (else + (call $ref-callee) + ) + ) + ) + + ;; CHECK: (func $one-armed-if (type $3) (param $condition i32) + ;; CHECK-NEXT: (if + ;; CHECK-NEXT: (local.get $condition) + ;; CHECK-NEXT: (then + ;; CHECK-NEXT: (return_call $void-callee) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $one-armed-if (param $condition i32) + ;; In a void function, the arm of a one-armed if in tail position is + ;; converted to return_call. + (if + (local.get $condition) + (then + (call $void-callee) + ) + ) + ) + + ;; CHECK: (func $indirect (type $none-to-i32) (result i32) + ;; CHECK-NEXT: (return_call_indirect $table (type $none-to-i32) + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $indirect (result i32) + ;; An indirect call in tail position becomes a return_call_indirect. + (call_indirect $table (type $none-to-i32) + (i32.const 0) + ) + ) + + ;; CHECK: (func $ref (type $none-to-i32) (result i32) + ;; CHECK-NEXT: (return_call_ref $none-to-i32 + ;; CHECK-NEXT: (ref.func $value-callee) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $ref (result i32) + ;; A call_ref in tail position becomes a return_call_ref. + (call_ref $none-to-i32 + (ref.func $value-callee) + ) + ) + + ;; CHECK: (func $break (type $1) (param $condition i32) (result i32) + ;; CHECK-NEXT: (block $out + ;; CHECK-NEXT: (if + ;; CHECK-NEXT: (local.get $condition) + ;; CHECK-NEXT: (then + ;; CHECK-NEXT: (return_call $value-callee) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (return_call $ref-callee) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $break (param $condition i32) (result i32) + ;; An unconditional branch to a tail block and the fallthrough of that block + ;; are both in tail position. + (block $out (result i32) + (if + (local.get $condition) + (then + (br $out + (call $value-callee) + ) + ) + ) + (call $ref-callee) + ) + ) + + ;; CHECK: (func $return-break (type $1) (param $condition i32) (result i32) + ;; CHECK-NEXT: (return + ;; CHECK-NEXT: (block $out + ;; CHECK-NEXT: (if + ;; CHECK-NEXT: (local.get $condition) + ;; CHECK-NEXT: (then + ;; CHECK-NEXT: (return_call $value-callee) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (return_call $ref-callee) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $return-break (param $condition i32) (result i32) + ;; A block wrapped in an explicit return is in tail position, so branches to + ;; it are also in tail position. + (return + (block $out (result i32) + (if + (local.get $condition) + (then + (br $out + (call $value-callee) + ) + ) + ) + (call $ref-callee) + ) + ) + ) + + ;; CHECK: (func $br-if-tail (type $1) (param $condition i32) (result i32) + ;; CHECK-NEXT: (block $out + ;; CHECK-NEXT: (block + ;; CHECK-NEXT: (return_call $value-callee) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (local.get $condition) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $br-if-tail (param $condition i32) (result i32) + ;; A conditional branch in tail position to a tail block is converted + ;; because both taking the branch and falling through exit the function with + ;; the call's result. + (block $out (result i32) + (br_if $out + (call $value-callee) + (local.get $condition) + ) + ) + ) + + ;; CHECK: (func $br-if-not-tail (type $1) (param $condition i32) (result i32) + ;; CHECK-NEXT: (block $out (result i32) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (br_if $out + ;; CHECK-NEXT: (call $value-callee) + ;; CHECK-NEXT: (local.get $condition) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (i32.const 42) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $br-if-not-tail (param $condition i32) (result i32) + ;; A conditional branch NOT in tail position must not be converted. + (block $out (result i32) + (drop + (br_if $out + (call $value-callee) + (local.get $condition) + ) + ) + (i32.const 42) + ) + ) + + ;; CHECK: (func $br-if-side-effect (type $none-to-i32) (result i32) + ;; CHECK-NEXT: (block $out (result i32) + ;; CHECK-NEXT: (br_if $out + ;; CHECK-NEXT: (call $value-callee) + ;; CHECK-NEXT: (block (result i32) + ;; CHECK-NEXT: (global.set $g + ;; CHECK-NEXT: (i32.const 1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $br-if-side-effect (result i32) + ;; A conditional branch in tail position whose condition has side effects + ;; must not be converted because converting would skip the side effect. + (block $out (result i32) + (br_if $out + (call $value-callee) + (block (result i32) + (global.set $g + (i32.const 1) + ) + (i32.const 0) + ) + ) + ) + ) + + ;; CHECK: (func $br-table-all-tail (type $1) (param $idx i32) (result i32) + ;; CHECK-NEXT: (block $out1 + ;; CHECK-NEXT: (block $out2 + ;; CHECK-NEXT: (block + ;; CHECK-NEXT: (return_call $value-callee) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (local.get $idx) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $br-table-all-tail (param $idx i32) (result i32) + ;; A br_table where all targets exit the function can be converted. + (block $out1 (result i32) + (block $out2 (result i32) + (br_table $out1 $out2 + (call $value-callee) + (local.get $idx) + ) + ) + ) + ) + + ;; CHECK: (func $br-table-not-all-tail (type $1) (param $idx i32) (result i32) + ;; CHECK-NEXT: (block $exit (result i32) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (block $not-exit (result i32) + ;; CHECK-NEXT: (br_table $not-exit $exit + ;; CHECK-NEXT: (call $value-callee) + ;; CHECK-NEXT: (local.get $idx) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (i32.const 42) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $br-table-not-all-tail (param $idx i32) (result i32) + ;; A br_table where not all targets exit the function must not be converted. + (block $exit (result i32) + (drop + (block $not-exit (result i32) + (br_table $not-exit $exit + (call $value-callee) + (local.get $idx) + ) + ) + ) + (i32.const 42) + ) + ) + + ;; CHECK: (func $void-break (type $3) (param $condition i32) + ;; CHECK-NEXT: (block $out + ;; CHECK-NEXT: (if + ;; CHECK-NEXT: (local.get $condition) + ;; CHECK-NEXT: (then + ;; CHECK-NEXT: (return_call $void-callee) + ;; CHECK-NEXT: (br $out) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (return_call $void-callee) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $void-break (param $condition i32) + ;; In a void function, statements immediately preceding an unconditional + ;; branch to a tail block or falling through a tail block are converted. + (block $out + (if + (local.get $condition) + (then + (call $void-callee) + (br $out) + ) + ) + (call $void-callee) + ) + ) + + ;; CHECK: (func $void-br-if-tail (type $3) (param $condition i32) + ;; CHECK-NEXT: (block $out + ;; CHECK-NEXT: (return_call $void-callee) + ;; CHECK-NEXT: (br_if $out + ;; CHECK-NEXT: (local.get $condition) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $void-br-if-tail (param $condition i32) + ;; In a void function, a call preceding a conditional branch in tail + ;; position targeting a tail block is converted. + (block $out + (call $void-callee) + (br_if $out + (local.get $condition) + ) + ) + ) + + ;; CHECK: (func $void-br-if-not-tail (type $3) (param $condition i32) + ;; CHECK-NEXT: (block $out + ;; CHECK-NEXT: (call $void-callee) + ;; CHECK-NEXT: (br_if $out + ;; CHECK-NEXT: (local.get $condition) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (nop) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $void-br-if-not-tail (param $condition i32) + ;; In a void function, a call preceding a conditional branch that is not in + ;; tail position must not be converted. + (block $out + (call $void-callee) + (br_if $out + (local.get $condition) + ) + (nop) + ) + ) + + ;; CHECK: (func $void-br-table-all-tail (type $3) (param $idx i32) + ;; CHECK-NEXT: (block $out1 + ;; CHECK-NEXT: (block $out2 + ;; CHECK-NEXT: (return_call $void-callee) + ;; CHECK-NEXT: (br_table $out1 $out2 + ;; CHECK-NEXT: (local.get $idx) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $void-br-table-all-tail (param $idx i32) + ;; In a void function, a call preceding a br_table whose targets are all tail + ;; blocks is converted. + (block $out1 + (block $out2 + (call $void-callee) + (br_table $out1 $out2 + (local.get $idx) + ) + ) + ) + ) + + ;; CHECK: (func $void-br-table-not-all-tail (type $3) (param $idx i32) + ;; CHECK-NEXT: (block $exit + ;; CHECK-NEXT: (block $not-exit + ;; CHECK-NEXT: (call $void-callee) + ;; CHECK-NEXT: (br_table $not-exit $exit + ;; CHECK-NEXT: (local.get $idx) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (nop) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $void-br-table-not-all-tail (param $idx i32) + ;; In a void function, a call preceding a br_table where not all targets are + ;; tail blocks must not be converted. + (block $exit + (block $not-exit + (call $void-callee) + (br_table $not-exit $exit + (local.get $idx) + ) + ) + (nop) + ) + ) + + ;; CHECK: (func $loop-tail (type $1) (param $condition i32) (result i32) + ;; CHECK-NEXT: (loop $l + ;; CHECK-NEXT: (br_if $l + ;; CHECK-NEXT: (local.get $condition) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (return_call $value-callee) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $loop-tail (param $condition i32) (result i32) + ;; A loop whose fallthrough is in tail position converts its fallthrough call + ;; to a return_call. + (loop $l (result i32) + (br_if $l + (local.get $condition) + ) + (call $value-callee) + ) + ) + + ;; CHECK: (func $loop-backedge-not-tail (type $none-to-none) + ;; CHECK-NEXT: (loop $l + ;; CHECK-NEXT: (call $void-callee) + ;; CHECK-NEXT: (br $l) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $loop-backedge-not-tail + ;; A branch to a loop header is a backedge rather than an exit, so a call + ;; preceding a loop branch must not be converted. + (loop $l + (call $void-callee) + (br $l) + ) + ) + + ;; CHECK: (func $not-tail (type $none-to-none) + ;; CHECK-NEXT: (call $void-callee) + ;; CHECK-NEXT: (nop) + ;; CHECK-NEXT: ) + (func $not-tail + ;; A call followed by another instruction is not in tail position. + (call $void-callee) + (nop) + ) + + ;; CHECK: (func $mismatched-result (type $none-to-none) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (call $value-callee) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $mismatched-result + ;; A dropped call is not in tail position. + (drop + (call $value-callee) + ) + ) + + ;; CHECK: (func $dead-after-return (type $none-to-i32) (result i32) + ;; CHECK-NEXT: (return + ;; CHECK-NEXT: (i32.const 1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (call $void-callee) + ;; CHECK-NEXT: ) + (func $dead-after-return (result i32) + ;; A dead void call at the end of a block after an unreachable instruction + ;; has a mismatched return type and is safely ignored. + (block + (return + (i32.const 1) + ) + (call $void-callee) + ) + ) +)