diff --git a/src/passes/BranchHintAnalysis.cpp b/src/passes/BranchHintAnalysis.cpp new file mode 100644 index 00000000000..1a77505d522 --- /dev/null +++ b/src/passes/BranchHintAnalysis.cpp @@ -0,0 +1,324 @@ +/* + * Copyright 2025 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. + */ + +// +// Infer values for Branch Hints and emit the custom section with those +// annotations. +// +// Inspired by LLVM's BranchProbabilityInfo: +// https://github.com/llvm/llvm-project/blob/main/llvm/lib/Analysis/BranchProbabilityInfo.cpp +// + +#include "cfg/cfg-traversal.h" +#include "ir/module-utils.h" +#include "pass.h" +#include "support/unique_deferring_queue.h" +#include "wasm-builder.h" +#include "wasm.h" + +namespace wasm { + +namespace { + +// An abstract chance (probability, but in less letters) of code being +// reached, in the range 0 - 100. +using Chance = uint8_t; +static constexpr Chance MinChance = 0; +static constexpr Chance TinyChance = 1; +static constexpr Chance MaxChance = 100; + +// Info we store in a basic block. +struct Info { + // In each basic block we will store instructions that either branch, or that + // provide hints as to branching. + std::vector actions; + + // The chance of the block being reached. We assume it is likely to be reached + // until we see a signal otherwise. + Chance chance = MaxChance; + + void dump(Function* func) { + std::cerr << " info\n"; + if (!actions.empty()) { + std::cerr << " with last: " << getExpressionName(actions.back()) + << '\n'; + } + std::cerr << " with chance: " << int(chance) << '\n'; + } +}; + +// Analyze Branch Hints in a function, using a CFG. +struct BranchHintCFGAnalysis + : public CFGWalker, + Info> { + + using Super = CFGWalker, + Info>; + + // We only look at things that branch twice, which is all branching + // instructions but without br (without condition, which is an unconditional + // branch we don't need to hint about) and not switch (which Branch Hints do + // not support). + bool isBranching(Expression* curr) { + if (auto* br = curr->dynCast()) { + return !!br->condition; + } + return curr->is() || curr->is(); + } + + bool isCall(Expression* curr) { + // TODO: we could infer something for indirect calls too. + return curr->is(); + } + + // Returns the chance that an instruction is reached, if something about + // it suggests it is likely or not. + std::optional getChance(Expression* curr) { + // Unreachable is assumed to never happen. + if (curr->is()) { + return MinChance; + } + + // Throw is assumed to almost never happen. + if (curr->is() || curr->is()) { + return TinyChance; + } + + // Otherwise, we don't know. + // TODO: cold calls, noreturn calls, etc. + return {}; + } + + void visitExpression(Expression* curr) { + // Ignore unreachable code. + if (!currBasicBlock) { + return; + } + + // Add all things that branch or call. + if (isBranching(curr) || isCall(curr)) { + currBasicBlock->contents.actions.push_back(curr); + } + + // Apply all signals: if something tells us the block is unlikely, mark it + // so. + if (auto chance = getChance(curr)) { + currBasicBlock->contents.chance = + std::min(currBasicBlock->contents.chance, *chance); + } + } + + // Override cfg-analysis's handling of If start. Ifs are control flow + // structures, so they do not appear in basic blocks (an If spans several), + // but we do need to know where the If begins, specifically, where the + // condition can branch + static void doStartIfTrue(BranchHintCFGAnalysis* self, Expression** currp) { + // Right before the Super creates a basic block for the ifTrue, note the + // basic block the condition is in. + if (self->currBasicBlock) { + self->currBasicBlock->contents.actions.push_back(*currp); + } + Super::doStartIfTrue(self, currp); + } + + void visitFunction(Function* curr) { flow(); } + + // Flow chances in a function, to find the chances of all blocks inside it. + void flow() { + // We consider the chance of a block to be no higher than the things it + // targets, that is, chance(block) := max(chance(target) for target). Flow + // chances to sources of blocks to achieve that, starting from the indexes + // of all blocks. + UniqueDeferredQueue work; + for (auto& block : basicBlocks) { + // Blocks with no successors have nothing new to compute in the loop + // below. + if (!block->out.empty()) { + work.push(block.get()); + } + } + while (!work.empty()) { + auto* block = work.pop(); + + // We should not get here if there is no work. + assert(!block->out.empty()); + + // Compute this block from its successors. The naive chance we already + // computed may decrease if all successors have lower probability. + auto maxOut = MinChance; + for (auto* out : block->out) { + maxOut = std::max(maxOut, out->contents.chance); + } + + auto& chance = block->contents.chance; + if (maxOut < chance) { + chance = maxOut; + for (auto* in : block->in) { + work.push(in); + } + } + } + } + + // Checks if a branch from a branching instruction to two targets is likely or + // not (or unknown). + std::optional + getLikelihood(Expression* brancher, Chance first, Chance second) { + if (first == second) { + // No data to suggest likelihood either way. + return {}; + } + + // |first, second| are the chances of the basic blocks after the brancher, + // in order. For Br*, the block right after them is reached if we do *not* + // branch (the condition is false), and the later block if we do, while for + // If, it is reversed: the block right after us is reached if the condition + // is true. + if (brancher->is()) { + std::swap(first, second); + } + return first < second; + } +}; + +// A BranchHintCFGAnalysis with the additional info that +// CallGraphPropertyAnalysis::FunctionInfo needs, which we will store in the map +// generated by CallGraphPropertyAnalysis. This structure provides all the info +// for one function. +// TODO: we don't actually use CallGraphPropertyAnalysis?! +struct FunctionAnalysis + : public BranchHintCFGAnalysis, + ModuleUtils::CallGraphPropertyAnalysis::FunctionInfo {}; + +struct BranchHintAnalysis : public Pass { + // Locals are not modified here. + bool requiresNonNullableLocalFixups() override { return false; } + + void run(Module* module) override { + // Analyze each function, computing chances inside it. + ModuleUtils::CallGraphPropertyAnalysis analyzer( + *module, [&](Function* func, FunctionAnalysis& analysis) { + if (func->imported()) { + return; + } + + analysis.walkFunctionInModule(func, module); + }); + + using BasicBlock = FunctionAnalysis::BasicBlock; + + // A block plus the context of which function it is in. + struct BlockContext { + BasicBlock* block; + FunctionAnalysis* analysis; + }; + + // Whenever a function's entry block has low chance, that means callers are + // low chance as well. Build a mapping to connect each entry function to the + // callers, so we can update them later down. + // + // How much this cross-function analysis matters varies a lot by codebase, + // anywhere from 3%, 7%, 20%, to 50%. (The 50% is on code that uses partial + // inlining heavily, leaving many outlined throws, which can then be marked + // unlikely.) + std::unordered_map> + entryToCallersMap; + for (auto& [_, analysis] : analyzer.map) { + for (auto& callerBlock : analysis.basicBlocks) { + // Calls only appear at the end of blocks. + if (callerBlock->contents.actions.empty()) { + continue; + } + auto* last = callerBlock->contents.actions.back(); + if (auto* call = last->dynCast()) { + auto* target = module->getFunction(call->target); + auto* targetEntryBlock = analyzer.map[target].entry; + auto context = BlockContext{callerBlock.get(), &analysis}; + entryToCallersMap[targetEntryBlock].push_back(context); + } + } + } + + // Flow back from entries to callers. We start from all entries with low + // chance and put them in a work queue. + UniqueDeferredQueue work; + for (auto& [entry, _] : entryToCallersMap) { + if (entry && entry->contents.chance < MaxChance) { + work.push(entry); + } + } + while (!work.empty()) { + auto* entry = work.pop(); + auto entryChance = entry->contents.chance; + // Find callers with higher chance: we can infer they have lower, now. + for (auto& callerContext : entryToCallersMap[entry]) { + auto& callerChance = callerContext.block->contents.chance; + if (callerChance > entryChance) { + // This adjustment to a basic block's chance may lead to more + // inferences inside that function: do a flow. TODO we could flow only + // from the caller blocks, and we could do these flows in parallel. + auto* callerAnalysis = callerContext.analysis; + auto* callerEntry = callerAnalysis->entry; + auto oldCallerEntryChance = callerEntry->contents.chance; + callerChance = entryChance; + callerAnalysis->flow(); + + // If our entry decreased in chance, we can propagate that to our + // callers too. + if (oldCallerEntryChance > callerEntry->contents.chance) { + work.push(callerEntry); + } + } + } + } + + // Finally, apply all we've inferred. TODO: parallelize. + + // Apply the final chances: when a branch between two options has a higher + // chance to go one way then the other, mark it as likely or unlikely + // accordingly. TODO: should we not mark when the difference is small? + for (auto& [func, analysis] : analyzer.map) { + for (auto& block : analysis.basicBlocks) { + if (block->contents.actions.empty() || block->out.size() != 2) { + continue; + } + + auto* last = block->contents.actions.back(); + if (!analysis.isBranching(last)) { + continue; + } + + // Compare the probabilities of the two targets and see if we can infer + // likelihood. + if (auto likely = + analysis.getLikelihood(last, + block->out[0]->contents.chance, + block->out[1]->contents.chance)) { + // We have a useful hint! + func->codeAnnotations[last].branchLikely = likely; + } + } + } + } +}; + +} // anonymous namespace + +Pass* createBranchHintAnalysisPass() { return new BranchHintAnalysis(); } + +} // namespace wasm diff --git a/src/passes/CMakeLists.txt b/src/passes/CMakeLists.txt index 6fab09b1bc2..9370060cf50 100644 --- a/src/passes/CMakeLists.txt +++ b/src/passes/CMakeLists.txt @@ -21,6 +21,7 @@ set(passes_SOURCES AlignmentLowering.cpp Asyncify.cpp AvoidReinterprets.cpp + BranchHintAnalysis.cpp CoalesceLocals.cpp CodePushing.cpp CodeFolding.cpp diff --git a/src/passes/pass.cpp b/src/passes/pass.cpp index 2042bc71d3a..e602fc7092d 100644 --- a/src/passes/pass.cpp +++ b/src/passes/pass.cpp @@ -99,6 +99,9 @@ void PassRegistry::registerPasses() { registerPass("avoid-reinterprets", "Tries to avoid reinterpret operations via more loads", createAvoidReinterpretsPass); + registerPass("branch-hint-analysis", + "Infers branch hints and emits branch hinting section", + createBranchHintAnalysisPass); registerPass( "dae", "removes arguments to calls in an lto-like manner", createDAEPass); registerPass("dae-optimizing", diff --git a/src/passes/passes.h b/src/passes/passes.h index e051e466e72..e871be68faf 100644 --- a/src/passes/passes.h +++ b/src/passes/passes.h @@ -26,6 +26,7 @@ Pass* createAbstractTypeRefiningPass(); Pass* createAlignmentLoweringPass(); Pass* createAsyncifyPass(); Pass* createAvoidReinterpretsPass(); +Pass* createBranchHintAnalysisPass(); Pass* createCoalesceLocalsPass(); Pass* createCoalesceLocalsWithLearningPass(); Pass* createCodeFoldingPass(); diff --git a/test/lit/passes/branch-hint-analysis-lto.wast b/test/lit/passes/branch-hint-analysis-lto.wast new file mode 100644 index 00000000000..1f939923037 --- /dev/null +++ b/test/lit/passes/branch-hint-analysis-lto.wast @@ -0,0 +1,322 @@ +;; NOTE: Assertions have been generated by update_lit_checks.py and should not be edited. +;; RUN: wasm-opt %s --branch-hint-analysis -all -S -o - \ +;; RUN: | filecheck %s + +;; Tests "lto"-like behavior of BranchHintAnalysis, that is, inferences between +;; functions. + +(module + ;; CHECK: (import "a" "b" (func $import (type $1))) + (import "a" "b" (func $import)) + + ;; CHECK: (tag $e (type $1)) + (tag $e) + + ;; CHECK: (func $unreachable (type $1) + ;; CHECK-NEXT: (unreachable) + ;; CHECK-NEXT: ) + (func $unreachable + ;; Helper for below. + (unreachable) + ) + + ;; CHECK: (func $throw (type $1) + ;; CHECK-NEXT: (throw $e) + ;; CHECK-NEXT: ) + (func $throw + ;; Helper for below. + (throw $e) + ) + + ;; CHECK: (func $nop (type $1) + ;; CHECK-NEXT: ) + (func $nop + ;; Helper for below. + ) + + ;; CHECK: (func $call-unreachable (type $0) (param $x i32) + ;; CHECK-NEXT: (@metadata.code.branch_hint "\00") + ;; CHECK-NEXT: (if + ;; CHECK-NEXT: (local.get $x) + ;; CHECK-NEXT: (then + ;; CHECK-NEXT: (call $unreachable) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $call-unreachable (param $x i32) + ;; This is unlikely as the call's target is. + (if + (local.get $x) + (then + (call $unreachable) + ) + ) + ) + + ;; CHECK: (func $call-throw (type $0) (param $x i32) + ;; CHECK-NEXT: (@metadata.code.branch_hint "\00") + ;; CHECK-NEXT: (if + ;; CHECK-NEXT: (local.get $x) + ;; CHECK-NEXT: (then + ;; CHECK-NEXT: (call $throw) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $call-throw (param $x i32) + ;; This is unlikely as the call's target is. + (if + (local.get $x) + (then + (call $throw) + ) + ) + ) + + ;; CHECK: (func $call-nop (type $0) (param $x i32) + ;; CHECK-NEXT: (if + ;; CHECK-NEXT: (local.get $x) + ;; CHECK-NEXT: (then + ;; CHECK-NEXT: (call $nop) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $call-nop (param $x i32) + ;; Nothing to infer here. + (if + (local.get $x) + (then + (call $nop) + ) + ) + ) + + ;; CHECK: (func $call-both (type $0) (param $x i32) + ;; CHECK-NEXT: (@metadata.code.branch_hint "\01") + ;; CHECK-NEXT: (if + ;; CHECK-NEXT: (local.get $x) + ;; CHECK-NEXT: (then + ;; CHECK-NEXT: (call $throw) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (else + ;; CHECK-NEXT: (call $unreachable) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $call-both (param $x i32) + ;; Unreachable is less likely than throw. + (if + (local.get $x) + (then + (call $throw) + ) + (else + (call $unreachable) + ) + ) + ) + + ;; CHECK: (func $call-both-flip (type $0) (param $x i32) + ;; CHECK-NEXT: (@metadata.code.branch_hint "\00") + ;; CHECK-NEXT: (if + ;; CHECK-NEXT: (local.get $x) + ;; CHECK-NEXT: (then + ;; CHECK-NEXT: (call $unreachable) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (else + ;; CHECK-NEXT: (call $throw) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $call-both-flip (param $x i32) + ;; As above, but flipped. + (if + (local.get $x) + (then + (call $unreachable) + ) + (else + (call $throw) + ) + ) + ) + + ;; CHECK: (func $call-both-mix (type $0) (param $x i32) + ;; CHECK-NEXT: (if + ;; CHECK-NEXT: (local.get $x) + ;; CHECK-NEXT: (then + ;; CHECK-NEXT: (call $unreachable) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (else + ;; CHECK-NEXT: (call $unreachable) + ;; CHECK-NEXT: (call $throw) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $call-both-mix (param $x i32) + ;; As above, but mix calls in one arm. Nothing to infer. + (if + (local.get $x) + (then + (call $unreachable) + ) + (else + (call $unreachable) + (call $throw) + ) + ) + ) + + ;; CHECK: (func $call-both-mix-flip (type $0) (param $x i32) + ;; CHECK-NEXT: (if + ;; CHECK-NEXT: (local.get $x) + ;; CHECK-NEXT: (then + ;; CHECK-NEXT: (call $unreachable) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (else + ;; CHECK-NEXT: (call $throw) + ;; CHECK-NEXT: (call $unreachable) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $call-both-mix-flip (param $x i32) + ;; As above, but flipped in that arm. Still nothing to infer. + (if + (local.get $x) + (then + (call $unreachable) + ) + (else + (call $throw) + (call $unreachable) + ) + ) + ) + + ;; CHECK: (func $flow-back (type $0) (param $x i32) + ;; CHECK-NEXT: (@metadata.code.branch_hint "\00") + ;; CHECK-NEXT: (if + ;; CHECK-NEXT: (local.get $x) + ;; CHECK-NEXT: (then + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (i32.const 10) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (if + ;; CHECK-NEXT: (local.get $x) + ;; CHECK-NEXT: (then + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (i32.const 20) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (i32.const 30) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (call $unreachable) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $flow-back (param $x i32) + ;; We must flow back what we infer about the call, to previous blocks, in + ;; order to infer a hint on this if. + (if + (local.get $x) + (then + ;; Add some basic blocks and code in the middle. + (drop + (i32.const 10) + ) + (if + (local.get $x) + (then + (drop + (i32.const 20) + ) + ) + ) + (drop + (i32.const 30) + ) + (call $unreachable) + ) + ) + ) + + ;; CHECK: (func $chain-0 (type $1) + ;; CHECK-NEXT: (call $throw) + ;; CHECK-NEXT: ) + (func $chain-0 + ;; The end of a chain of functions that calls something unlikely. Helper for + ;; below. + (call $throw) + ) + + ;; CHECK: (func $chain-1 (type $1) + ;; CHECK-NEXT: (@metadata.code.branch_hint "\01") + ;; CHECK-NEXT: (if + ;; CHECK-NEXT: (i32.const 10) + ;; CHECK-NEXT: (then + ;; CHECK-NEXT: (call $chain-0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (else + ;; CHECK-NEXT: (call $unreachable) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $chain-1 + ;; This callchain segment needs some flow internally. We can also infer this + ;; if is likely (since throw is more likely than unreachable). + (if + (i32.const 10) + (then + (call $chain-0) + ) + (else + (call $unreachable) + ) + ) + ) + + ;; CHECK: (func $chain-2 (type $1) + ;; CHECK-NEXT: (call $chain-1) + ;; CHECK-NEXT: ) + (func $chain-2 + (call $chain-1) + ) + + ;; CHECK: (func $call-chain (type $0) (param $x i32) + ;; CHECK-NEXT: (@metadata.code.branch_hint "\00") + ;; CHECK-NEXT: (if + ;; CHECK-NEXT: (local.get $x) + ;; CHECK-NEXT: (then + ;; CHECK-NEXT: (call $chain-2) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $call-chain (param $x i32) + ;; This is unlikely as the callchain ends that way. + (if + (local.get $x) + (then + (call $chain-2) + ) + ) + ) + + ;; CHECK: (func $call-import (type $0) (param $x i32) + ;; CHECK-NEXT: (if + ;; CHECK-NEXT: (local.get $x) + ;; CHECK-NEXT: (then + ;; CHECK-NEXT: (call $import) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $call-import (param $x i32) + ;; We know nothing about imports. + (if + (local.get $x) + (then + (call $import) + ) + ) + ) +) diff --git a/test/lit/passes/branch-hint-analysis.wast b/test/lit/passes/branch-hint-analysis.wast new file mode 100644 index 00000000000..62b4ba400a4 --- /dev/null +++ b/test/lit/passes/branch-hint-analysis.wast @@ -0,0 +1,812 @@ +;; NOTE: Assertions have been generated by update_lit_checks.py and should not be edited. +;; RUN: wasm-opt %s --branch-hint-analysis -all -S -o - \ +;; RUN: | filecheck %s +;; + +(module + ;; CHECK: (tag $e (type $2)) + (tag $e) + + ;; CHECK: (func $br-unreachable (type $0) (param $x i32) + ;; CHECK-NEXT: (block $block + ;; CHECK-NEXT: (@metadata.code.branch_hint "\00") + ;; CHECK-NEXT: (br_if $block + ;; CHECK-NEXT: (local.get $x) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (return) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (unreachable) + ;; CHECK-NEXT: ) + (func $br-unreachable (param $x i32) + ;; The br_if is unlikely, as it reaches an unreachable. + (block $block + (br_if $block + (local.get $x) + ) + (return) + ) + (unreachable) + ) + + ;; CHECK: (func $br-unreachable-flip (type $0) (param $x i32) + ;; CHECK-NEXT: (block $block + ;; CHECK-NEXT: (@metadata.code.branch_hint "\01") + ;; CHECK-NEXT: (br_if $block + ;; CHECK-NEXT: (local.get $x) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (unreachable) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $br-unreachable-flip (param $x i32) + ;; As above, but flipped: the hint should be reversed, since the unreachable + ;; is if we do not branch. + (block $block + (br_if $block + (local.get $x) + ) + (unreachable) + ) + ) + + ;; CHECK: (func $br_on-unreachable (type $1) (param $x anyref) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (block $block (result (ref any)) + ;; CHECK-NEXT: (@metadata.code.branch_hint "\00") + ;; CHECK-NEXT: (br_on_non_null $block + ;; CHECK-NEXT: (local.get $x) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (return) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (unreachable) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $br_on-unreachable (param $x anyref) + ;; The br_on is unlikely, as it reaches an unreachable. + (drop + (block $block (result (ref any)) + (br_on_non_null $block + (local.get $x) + ) + (return) + ) + (unreachable) + ) + ) + + ;; CHECK: (func $br_on-unreachable-flip (type $1) (param $x anyref) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (block $block (result (ref any)) + ;; CHECK-NEXT: (@metadata.code.branch_hint "\01") + ;; CHECK-NEXT: (br_on_non_null $block + ;; CHECK-NEXT: (local.get $x) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (unreachable) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $br_on-unreachable-flip (param $x anyref) + ;; As above, but flipped, so the hint is flipped too. + (drop + (block $block (result (ref any)) + (br_on_non_null $block + (local.get $x) + ) + (unreachable) + ) + ) + ) + + ;; CHECK: (func $if-unreachable (type $0) (param $x i32) + ;; CHECK-NEXT: (@metadata.code.branch_hint "\00") + ;; CHECK-NEXT: (if + ;; CHECK-NEXT: (local.get $x) + ;; CHECK-NEXT: (then + ;; CHECK-NEXT: (unreachable) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (else + ;; CHECK-NEXT: (nop) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $if-unreachable (param $x i32) + ;; The unreachable means the condition is unlikely. + (if + (local.get $x) + (then + (unreachable) + ) + (else + (nop) + ) + ) + ) + + ;; CHECK: (func $if-unreachable-flip (type $0) (param $x i32) + ;; CHECK-NEXT: (@metadata.code.branch_hint "\01") + ;; CHECK-NEXT: (if + ;; CHECK-NEXT: (local.get $x) + ;; CHECK-NEXT: (then + ;; CHECK-NEXT: (nop) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (else + ;; CHECK-NEXT: (unreachable) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $if-unreachable-flip (param $x i32) + ;; As above, but flipped, so the hint is flipped too. + (if + (local.get $x) + (then + (nop) + ) + (else + (unreachable) + ) + ) + ) + + ;; CHECK: (func $if-unreachable-one-arm (type $0) (param $x i32) + ;; CHECK-NEXT: (@metadata.code.branch_hint "\00") + ;; CHECK-NEXT: (if + ;; CHECK-NEXT: (local.get $x) + ;; CHECK-NEXT: (then + ;; CHECK-NEXT: (unreachable) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $if-unreachable-one-arm (param $x i32) + ;; The unreachable means the condition is unlikely. + (if + (local.get $x) + (then + (unreachable) + ) + ) + ) + + ;; CHECK: (func $if-one-arm-unreachable-later (type $0) (param $x i32) + ;; CHECK-NEXT: (@metadata.code.branch_hint "\01") + ;; CHECK-NEXT: (if + ;; CHECK-NEXT: (local.get $x) + ;; CHECK-NEXT: (then + ;; CHECK-NEXT: (return) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (unreachable) + ;; CHECK-NEXT: ) + (func $if-one-arm-unreachable-later (param $x i32) + ;; The unreachable after means the condition is likely. + (if + (local.get $x) + (then + (return) + ) + ) + (unreachable) + ) + + ;; CHECK: (func $if-one-arm-unreachable-always (type $0) (param $x i32) + ;; CHECK-NEXT: (if + ;; CHECK-NEXT: (local.get $x) + ;; CHECK-NEXT: (then + ;; CHECK-NEXT: (nop) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (unreachable) + ;; CHECK-NEXT: ) + (func $if-one-arm-unreachable-always (param $x i32) + ;; The unreachable after means the if arm is unlikely, like the code after + ;; the if, so we do not hint. + (if + (local.get $x) + (then + (nop) ;; used to be a return here + ) + ) + (unreachable) + ) + + ;; CHECK: (func $if-throw (type $0) (param $x i32) + ;; CHECK-NEXT: (@metadata.code.branch_hint "\00") + ;; CHECK-NEXT: (if + ;; CHECK-NEXT: (local.get $x) + ;; CHECK-NEXT: (then + ;; CHECK-NEXT: (throw $e) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (else + ;; CHECK-NEXT: (nop) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $if-throw (param $x i32) + ;; A throw is unlikely. + (if + (local.get $x) + (then + (throw $e) + ) + (else + (nop) + ) + ) + ) + + ;; CHECK: (func $if-throw_ref (type $3) (param $x i32) (param $y exnref) + ;; CHECK-NEXT: (@metadata.code.branch_hint "\00") + ;; CHECK-NEXT: (if + ;; CHECK-NEXT: (local.get $x) + ;; CHECK-NEXT: (then + ;; CHECK-NEXT: (throw_ref + ;; CHECK-NEXT: (local.get $y) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (else + ;; CHECK-NEXT: (nop) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $if-throw_ref (param $x i32) (param $y exnref) + ;; A throw is unlikely. + (if + (local.get $x) + (then + (throw_ref + (local.get $y) + ) + ) + (else + (nop) + ) + ) + ) + + ;; CHECK: (func $if-throw-unreachable (type $0) (param $x i32) + ;; CHECK-NEXT: (@metadata.code.branch_hint "\01") + ;; CHECK-NEXT: (if + ;; CHECK-NEXT: (local.get $x) + ;; CHECK-NEXT: (then + ;; CHECK-NEXT: (throw $e) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (else + ;; CHECK-NEXT: (unreachable) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $if-throw-unreachable (param $x i32) + ;; A throw is less likely than an unreachable. + (if + (local.get $x) + (then + (throw $e) + ) + (else + (unreachable) + ) + ) + ) + + ;; CHECK: (func $if-unreachable-double (type $0) (param $x i32) + ;; CHECK-NEXT: (if + ;; CHECK-NEXT: (local.get $x) + ;; CHECK-NEXT: (then + ;; CHECK-NEXT: (unreachable) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (else + ;; CHECK-NEXT: (unreachable) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $if-unreachable-double (param $x i32) + ;; Two unreachables are equally unlikely, so nothing to hint. + (if + (local.get $x) + (then + (unreachable) + ) + (else + (unreachable) + ) + ) + ) + + ;; CHECK: (func $if-nesting (type $0) (param $x i32) + ;; CHECK-NEXT: (if + ;; CHECK-NEXT: (local.get $x) + ;; CHECK-NEXT: (then + ;; CHECK-NEXT: (return) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (else + ;; CHECK-NEXT: (@metadata.code.branch_hint "\00") + ;; CHECK-NEXT: (if + ;; CHECK-NEXT: (local.get $x) + ;; CHECK-NEXT: (then + ;; CHECK-NEXT: (unreachable) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $if-nesting (param $x i32) + ;; We know nothing for this if. + (if + (local.get $x) + (then + (return) + ) + (else + ;; This condition is likely false. + (if + (local.get $x) + (then + (unreachable) + ) + ) + ) + ) + ) + + ;; CHECK: (func $if-nesting-2 (type $0) (param $x i32) + ;; CHECK-NEXT: (@metadata.code.branch_hint "\01") + ;; CHECK-NEXT: (if + ;; CHECK-NEXT: (local.get $x) + ;; CHECK-NEXT: (then + ;; CHECK-NEXT: (return) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (else + ;; CHECK-NEXT: (if + ;; CHECK-NEXT: (local.get $x) + ;; CHECK-NEXT: (then + ;; CHECK-NEXT: (unreachable) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (else + ;; CHECK-NEXT: (unreachable) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $if-nesting-2 (param $x i32) + ;; The else is unlikely, so we hint here. + (if + (local.get $x) + (then + (return) + ) + (else + ;; Both arms are equally likely, so we do not hint here, but all this + ;; code is unlikely. + (if + (local.get $x) + (then + (unreachable) + ) + (else + (unreachable) + ) + ) + ) + ) + ) + + ;; CHECK: (func $loop (type $4) (param $x i32) (param $y i32) + ;; CHECK-NEXT: (loop $loop + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (i32.const 10) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (if + ;; CHECK-NEXT: (local.get $x) + ;; CHECK-NEXT: (then + ;; CHECK-NEXT: (return) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (i32.const 20) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (@metadata.code.branch_hint "\00") + ;; CHECK-NEXT: (if + ;; CHECK-NEXT: (local.get $y) + ;; CHECK-NEXT: (then + ;; CHECK-NEXT: (unreachable) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (i32.const 30) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (br $loop) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $loop (param $x i32) (param $y i32) + ;; We should not error when computing cycles. + (loop $loop + (drop (i32.const 10)) + ;; No hint for the first if, but the second is unlikely. + (if + (local.get $x) + (then + (return) + ) + ) + (drop (i32.const 20)) + (if + (local.get $y) + (then + (unreachable) + ) + ) + (drop (i32.const 30)) + (br $loop) + ) + ) + + ;; CHECK: (func $calls-unreachable (type $0) (param $x i32) + ;; CHECK-NEXT: (@metadata.code.branch_hint "\01") + ;; CHECK-NEXT: (if + ;; CHECK-NEXT: (local.get $x) + ;; CHECK-NEXT: (then + ;; CHECK-NEXT: (return) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (call $calls + ;; CHECK-NEXT: (local.get $x) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (unreachable) + ;; CHECK-NEXT: ) + (func $calls-unreachable (param $x i32) + ;; Calls may branch out, but only when throwing, which means the code after + ;; the if is unlikely, so the if is likely. + (if + (local.get $x) + (then + (return) + ) + ) + (call $calls + (local.get $x) + ) + (unreachable) + ) + + ;; CHECK: (func $calls (type $0) (param $x i32) + ;; CHECK-NEXT: (if + ;; CHECK-NEXT: (local.get $x) + ;; CHECK-NEXT: (then + ;; CHECK-NEXT: (return) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (call $calls + ;; CHECK-NEXT: (local.get $x) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $calls (param $x i32) + ;; As above but no unreachable at the end. The call might throw, but it also + ;; might exit normally, so we have no hint to give here. + (if + (local.get $x) + (then + (return) + ) + ) + (call $calls + (local.get $x) + ) + ) + + ;; CHECK: (func $calls-throw (type $0) (param $x i32) + ;; CHECK-NEXT: (@metadata.code.branch_hint "\00") + ;; CHECK-NEXT: (if + ;; CHECK-NEXT: (local.get $x) + ;; CHECK-NEXT: (then + ;; CHECK-NEXT: (throw $e) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (call $calls + ;; CHECK-NEXT: (local.get $x) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $calls-throw (param $x i32) + ;; The throw is less likely than a call, which only might throw. + (if + (local.get $x) + (then + (throw $e) + ) + ) + (call $calls + (local.get $x) + ) + ) + + ;; CHECK: (func $lots-in-middle (type $0) (param $x i32) + ;; CHECK-NEXT: (@metadata.code.branch_hint "\01") + ;; CHECK-NEXT: (if + ;; CHECK-NEXT: (local.get $x) + ;; CHECK-NEXT: (then + ;; CHECK-NEXT: (return) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (nop) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (i32.const 10) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (if + ;; CHECK-NEXT: (local.get $x) + ;; CHECK-NEXT: (then + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (i32.const 20) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (else + ;; CHECK-NEXT: (local.set $x + ;; CHECK-NEXT: (i32.const 30) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (block $block + ;; CHECK-NEXT: (br_if $block + ;; CHECK-NEXT: (local.get $x) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (i32.const 40) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (if + ;; CHECK-NEXT: (local.get $x) + ;; CHECK-NEXT: (then + ;; CHECK-NEXT: (if + ;; CHECK-NEXT: (local.get $x) + ;; CHECK-NEXT: (then + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (i32.const 50) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (unreachable) + ;; CHECK-NEXT: ) + (func $lots-in-middle (param $x i32) + ;; The unreachable after all the code below means the condition is likely. + (if + (local.get $x) + (then + (return) + ) + ) + ;; ...lots of code, but no exits from function... + (nop) + (drop + (i32.const 10) + ) + (if + (local.get $x) + (then + (drop + (i32.const 20) + ) + ) + (else + (local.set $x + (i32.const 30) + ) + ) + ) + (block $block + (br_if $block + (local.get $x) + ) + (drop + (i32.const 40) + ) + ) + (if + (local.get $x) + (then + (if + (local.get $x) + (then + (drop + (i32.const 50) + ) + ) + ) + ) + ) + ;; Finally, an unreachable. + (unreachable) + ) + + ;; CHECK: (func $lots-in-middle-return (type $0) (param $x i32) + ;; CHECK-NEXT: (if + ;; CHECK-NEXT: (local.get $x) + ;; CHECK-NEXT: (then + ;; CHECK-NEXT: (return) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (nop) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (i32.const 10) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (if + ;; CHECK-NEXT: (local.get $x) + ;; CHECK-NEXT: (then + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (i32.const 20) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (else + ;; CHECK-NEXT: (local.set $x + ;; CHECK-NEXT: (i32.const 30) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (block $block + ;; CHECK-NEXT: (@metadata.code.branch_hint "\00") + ;; CHECK-NEXT: (br_if $block + ;; CHECK-NEXT: (local.get $x) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (return) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (if + ;; CHECK-NEXT: (local.get $x) + ;; CHECK-NEXT: (then + ;; CHECK-NEXT: (if + ;; CHECK-NEXT: (local.get $x) + ;; CHECK-NEXT: (then + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (i32.const 50) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (unreachable) + ;; CHECK-NEXT: ) + (func $lots-in-middle-return (param $x i32) + ;; As above, but now a return in the middle of the code below. No hint here, + ;; but we can hint on the br_if near that return. + (if + (local.get $x) + (then + (return) + ) + ) + (nop) + (drop + (i32.const 10) + ) + (if + (local.get $x) + (then + (drop + (i32.const 20) + ) + ) + (else + (local.set $x + (i32.const 30) + ) + ) + ) + (block $block + (br_if $block + (local.get $x) + ) + (drop + (return) + ) + ) + (if + (local.get $x) + (then + (if + (local.get $x) + (then + (drop + (i32.const 50) + ) + ) + ) + ) + ) + (unreachable) + ) + + ;; CHECK: (func $lots-in-middle-nothing (type $0) (param $x i32) + ;; CHECK-NEXT: (if + ;; CHECK-NEXT: (local.get $x) + ;; CHECK-NEXT: (then + ;; CHECK-NEXT: (return) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (nop) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (i32.const 10) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (if + ;; CHECK-NEXT: (local.get $x) + ;; CHECK-NEXT: (then + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (i32.const 20) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (else + ;; CHECK-NEXT: (local.set $x + ;; CHECK-NEXT: (i32.const 30) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (block $block + ;; CHECK-NEXT: (br_if $block + ;; CHECK-NEXT: (local.get $x) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (i32.const 40) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (if + ;; CHECK-NEXT: (local.get $x) + ;; CHECK-NEXT: (then + ;; CHECK-NEXT: (if + ;; CHECK-NEXT: (local.get $x) + ;; CHECK-NEXT: (then + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (i32.const 50) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $lots-in-middle-nothing (param $x i32) + ;; As above, but no final unreachable, so no hints. + (if + (local.get $x) + (then + (return) + ) + ) + (nop) + (drop + (i32.const 10) + ) + (if + (local.get $x) + (then + (drop + (i32.const 20) + ) + ) + (else + (local.set $x + (i32.const 30) + ) + ) + ) + (block $block + (br_if $block + (local.get $x) + ) + (drop + (i32.const 40) + ) + ) + (if + (local.get $x) + (then + (if + (local.get $x) + (then + (drop + (i32.const 50) + ) + ) + ) + ) + ) + ) +)