From ae128ee7e7b67b1084755d3c9c23bad8a347cee5 Mon Sep 17 00:00:00 2001 From: Alon Zakai Date: Tue, 24 Feb 2026 11:41:17 -0800 Subject: [PATCH 1/9] start --- test/lit/idempotent.wast | 24 +++++++ test/lit/passes/local-cse_idempotent.wast | 79 +++++++++++++++++++++++ 2 files changed, 103 insertions(+) create mode 100644 test/lit/idempotent.wast create mode 100644 test/lit/passes/local-cse_idempotent.wast diff --git a/test/lit/idempotent.wast b/test/lit/idempotent.wast new file mode 100644 index 00000000000..d29c48ebbe1 --- /dev/null +++ b/test/lit/idempotent.wast @@ -0,0 +1,24 @@ +;; NOTE: Assertions have been generated by update_lit_checks.py --all-items and should not be edited. + +;; RUN: wasm-opt -all %s -S -o - | filecheck %s +;; RUN: wasm-opt -all --roundtrip %s -S -o - | filecheck %s + +;; Test text and binary handling of @binaryen.idempotent. + +(module + ;; CHECK: (type $0 (func)) + + ;; CHECK: (@binaryen.idempotent) + ;; CHECK-NEXT: (func $func-annotation (type $0) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (@binaryen.idempotent) + (func $func-annotation + (drop + (i32.const 0) + ) + ) +) + diff --git a/test/lit/passes/local-cse_idempotent.wast b/test/lit/passes/local-cse_idempotent.wast new file mode 100644 index 00000000000..e88c0fbe703 --- /dev/null +++ b/test/lit/passes/local-cse_idempotent.wast @@ -0,0 +1,79 @@ +;; NOTE: Assertions have been generated by update_lit_checks.py --all-items and should not be edited. +;; NOTE: This test was ported using port_passes_tests_to_lit.py and could be cleaned up. + +;; RUN: foreach %s %t wasm-opt --local-cse -all -S -o - | filecheck %s + +(module + ;; CHECK: (type $func (func)) + (type $func (func)) + ;; CHECK: (type $cont (cont $func)) + (type $cont (cont $func)) + + ;; CHECK: (type $func-i32 (func (param i32))) + (type $func-i32 (func (param i32))) + ;; CHECK: (type $cont-i32 (cont $func-i32)) + (type $cont-i32 (cont $func-i32)) + + ;; CHECK: (type $4 (func (param (ref $cont-i32)))) + + ;; CHECK: (elem declare func $cont.new) + + ;; CHECK: (func $cont.new (type $func) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (cont.new $cont + ;; CHECK-NEXT: (ref.func $cont.new) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (cont.new $cont + ;; CHECK-NEXT: (ref.func $cont.new) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $cont.new (type $func) + ;; We cannot CSE here, as each of these emits a unique continuation. + (drop + (cont.new $cont + (ref.func $cont.new) + ) + ) + (drop + (cont.new $cont + (ref.func $cont.new) + ) + ) + ) + + ;; CHECK: (func $cont.bind (type $4) (param $cont-i32 (ref $cont-i32)) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (cont.bind $cont-i32 $cont + ;; CHECK-NEXT: (i32.const 42) + ;; CHECK-NEXT: (local.get $cont-i32) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (cont.bind $cont-i32 $cont + ;; CHECK-NEXT: (i32.const 42) + ;; CHECK-NEXT: (local.get $cont-i32) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $cont.bind (param $cont-i32 (ref $cont-i32)) + ;; We cannot optimize here: Each of these has a side effect of modifying the + ;; continuation they were given, as it will trap if resumed, and in fact the + ;; second cont.bind here should trap, which we should not remove. + (drop + (cont.bind $cont-i32 $cont + (i32.const 42) + (local.get $cont-i32) + ) + ) + (drop + (cont.bind $cont-i32 $cont + (i32.const 42) + (local.get $cont-i32) + ) + ) + ) +) + From 634e41fabbef001dac3ce6d9d266ba41bd39ca3e Mon Sep 17 00:00:00 2001 From: Alon Zakai Date: Tue, 24 Feb 2026 11:46:01 -0800 Subject: [PATCH 2/9] testing --- test/lit/passes/local-cse_idempotent.wast | 160 +++++++++++++--------- 1 file changed, 97 insertions(+), 63 deletions(-) diff --git a/test/lit/passes/local-cse_idempotent.wast b/test/lit/passes/local-cse_idempotent.wast index e88c0fbe703..95229892a71 100644 --- a/test/lit/passes/local-cse_idempotent.wast +++ b/test/lit/passes/local-cse_idempotent.wast @@ -4,76 +4,110 @@ ;; RUN: foreach %s %t wasm-opt --local-cse -all -S -o - | filecheck %s (module - ;; CHECK: (type $func (func)) - (type $func (func)) - ;; CHECK: (type $cont (cont $func)) - (type $cont (cont $func)) + ;; CHECK: (type $0 (func)) - ;; CHECK: (type $func-i32 (func (param i32))) - (type $func-i32 (func (param i32))) - ;; CHECK: (type $cont-i32 (cont $func-i32)) - (type $cont-i32 (cont $func-i32)) + ;; CHECK: (type $1 (func (param i32) (result i32))) - ;; CHECK: (type $4 (func (param (ref $cont-i32)))) + ;; CHECK: (import "a" "b" (func $import (type $0))) + (import "a" "b" (func $import)) - ;; CHECK: (elem declare func $cont.new) + ;; CHECK: (@binaryen.idempotent) + ;; CHECK-NEXT: (func $idempotent (type $1) (param $0 i32) (result i32) + ;; CHECK-NEXT: (call $import) + ;; CHECK-NEXT: (i32.const 42) + ;; CHECK-NEXT: ) + (@binaryen.idempotent) + (func $idempotent (param i32) (result i32) + ;; This function has side effects, but is marked idempotent. + (call $import) + (i32.const 42) + ) - ;; CHECK: (func $cont.new (type $func) - ;; CHECK-NEXT: (drop - ;; CHECK-NEXT: (cont.new $cont - ;; CHECK-NEXT: (ref.func $cont.new) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: (drop - ;; CHECK-NEXT: (cont.new $cont - ;; CHECK-NEXT: (ref.func $cont.new) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: ) - (func $cont.new (type $func) - ;; We cannot CSE here, as each of these emits a unique continuation. - (drop - (cont.new $cont - (ref.func $cont.new) - ) + ;; CHECK: (func $potent (type $1) (param $0 i32) (result i32) + ;; CHECK-NEXT: (call $import) + ;; CHECK-NEXT: (i32.const 1337) + ;; CHECK-NEXT: ) + (func $potent (param i32) (result i32) + ;; As above, but not marked as idempotent. + (call $import) + (i32.const 1337) ) - (drop - (cont.new $cont - (ref.func $cont.new) - ) + + ;; CHECK: (func $yes (type $0) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (call $idempotent + ;; CHECK-NEXT: (i32.const 10) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (call $idempotent + ;; CHECK-NEXT: (i32.const 10) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $yes + ;; We can optimize here. + (drop + (call $idempotent + (i32.const 10) + ) + ) + (drop + (call $idempotent + (i32.const 10) + ) + ) ) - ) - ;; CHECK: (func $cont.bind (type $4) (param $cont-i32 (ref $cont-i32)) - ;; CHECK-NEXT: (drop - ;; CHECK-NEXT: (cont.bind $cont-i32 $cont - ;; CHECK-NEXT: (i32.const 42) - ;; CHECK-NEXT: (local.get $cont-i32) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: (drop - ;; CHECK-NEXT: (cont.bind $cont-i32 $cont - ;; CHECK-NEXT: (i32.const 42) - ;; CHECK-NEXT: (local.get $cont-i32) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: ) - (func $cont.bind (param $cont-i32 (ref $cont-i32)) - ;; We cannot optimize here: Each of these has a side effect of modifying the - ;; continuation they were given, as it will trap if resumed, and in fact the - ;; second cont.bind here should trap, which we should not remove. - (drop - (cont.bind $cont-i32 $cont - (i32.const 42) - (local.get $cont-i32) - ) + ;; CHECK: (func $no (type $0) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (call $potent + ;; CHECK-NEXT: (i32.const 10) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (call $potent + ;; CHECK-NEXT: (i32.const 10) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $no + ;; Without idempotency we cannot optimize. + (drop + (call $potent + (i32.const 10) + ) + ) + (drop + (call $potent + (i32.const 10) + ) + ) ) - (drop - (cont.bind $cont-i32 $cont - (i32.const 42) - (local.get $cont-i32) - ) + + ;; CHECK: (func $different-input (type $0) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (call $idempotent + ;; CHECK-NEXT: (i32.const 10) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (call $idempotent + ;; CHECK-NEXT: (i32.const 20) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $different-input + ;; We cannot optimize here. + (drop + (call $idempotent + (i32.const 10) + ) + ) + (drop + (call $idempotent + (i32.const 20) + ) + ) ) - ) ) - From ba578e855a42743d2bab527855d94e892a37983c Mon Sep 17 00:00:00 2001 From: Alon Zakai Date: Tue, 24 Feb 2026 12:35:38 -0800 Subject: [PATCH 3/9] sus --- src/passes/LocalCSE.cpp | 89 ++++++++++++++--------- test/lit/passes/local-cse_idempotent.wast | 71 ++++++++++++++++-- 2 files changed, 120 insertions(+), 40 deletions(-) diff --git a/src/passes/LocalCSE.cpp b/src/passes/LocalCSE.cpp index 79831fb89ab..7dd34f84f4a 100644 --- a/src/passes/LocalCSE.cpp +++ b/src/passes/LocalCSE.cpp @@ -118,6 +118,7 @@ #include #include +#include #include #include #include @@ -207,6 +208,15 @@ struct RequestInfoMap : public std::unordered_map { } }; +bool isIdempotent(Expression* curr, Module& wasm) { + if (auto* call = curr->dynCast()) { + if (Intrinsics::getAnnotations(wasm.getFunction(call->target)).idempotent) { + return true; + } + } + return false; +} + struct Scanner : public LinearExecutionWalker> { PassOptions& options; @@ -368,6 +378,13 @@ struct Scanner // total size big enough - while isPossible checks conditions that prevent // using an expression at all. bool isPossible(Expression* curr) { + // A call to an idempotent function is optimizable: it may have effects the + // first time, but those do not invalidate the second appearance, and also + // it will return the same value. + if (isIdempotent(curr, *getModule())) { + return true; + } + // We will fully compute effects later, but consider shallow effects at this // early time to ignore things that cannot be optimized later, because we // use a greedy algorithm. Specifically, imagine we see this: @@ -441,44 +458,46 @@ struct Checker // Given the current expression, see what it invalidates of the currently- // hashed expressions, if there are any. if (!activeOriginals.empty()) { - EffectAnalyzer effects(options, *getModule()); - // We can ignore traps here: - // - // (ORIGINAL) - // (curr) - // (COPY) - // - // We are some code in between an original and a copy of it, and we are - // trying to turn COPY into a local.get of a value that we stash at the - // original. If |curr| traps then we simply don't reach the copy anyhow. - effects.trap = false; - // We only need to visit this node itself, as we have already visited its - // children by the time we get here. - effects.visit(curr); - - std::vector invalidated; - for (auto& kv : activeOriginals) { - auto* original = kv.first; - auto& originalInfo = kv.second; - if (effects.invalidates(originalInfo.effects)) { - invalidated.push_back(original); + if (!isIdempotent(curr, *getModule())) { // XXX + EffectAnalyzer effects(options, *getModule()); + // We can ignore traps here: + // + // (ORIGINAL) + // (curr) + // (COPY) + // + // We are some code in between an original and a copy of it, and we are + // trying to turn COPY into a local.get of a value that we stash at the + // original. If |curr| traps then we simply don't reach the copy anyhow. + effects.trap = false; + // We only need to visit this node itself, as we have already visited its + // children by the time we get here. + effects.visit(curr); + + std::vector invalidated; + for (auto& kv : activeOriginals) { + auto* original = kv.first; + auto& originalInfo = kv.second; + if (effects.invalidates(originalInfo.effects)) { + invalidated.push_back(original); + } } - } - for (auto* original : invalidated) { - // Remove all requests after this expression, as we cannot optimize to - // them. - requestInfos[original].requests -= - activeOriginals.at(original).requestsLeft; - - // If no requests remain at all (that is, there were no requests we - // could provide before we ran into this invalidation) then we do not - // need this original at all. - if (requestInfos[original].requests == 0) { - requestInfos.erase(original); - } + for (auto* original : invalidated) { + // Remove all requests after this expression, as we cannot optimize to + // them. + requestInfos[original].requests -= + activeOriginals.at(original).requestsLeft; + + // If no requests remain at all (that is, there were no requests we + // could provide before we ran into this invalidation) then we do not + // need this original at all. + if (requestInfos[original].requests == 0) { + requestInfos.erase(original); + } - activeOriginals.erase(original); + activeOriginals.erase(original); + } } } diff --git a/test/lit/passes/local-cse_idempotent.wast b/test/lit/passes/local-cse_idempotent.wast index 95229892a71..4f92862aece 100644 --- a/test/lit/passes/local-cse_idempotent.wast +++ b/test/lit/passes/local-cse_idempotent.wast @@ -11,6 +11,12 @@ ;; CHECK: (import "a" "b" (func $import (type $0))) (import "a" "b" (func $import)) + ;; CHECK: (global $mutable (mut i32) (i32.const 42)) + (global $mutable (mut i32) (i32.const 42)) + + ;; CHECK: (global $immutable i32 (i32.const 1337)) + (global $immutable i32 (i32.const 1337)) + ;; CHECK: (@binaryen.idempotent) ;; CHECK-NEXT: (func $idempotent (type $1) (param $0 i32) (result i32) ;; CHECK-NEXT: (call $import) @@ -34,15 +40,16 @@ ) ;; CHECK: (func $yes (type $0) + ;; CHECK-NEXT: (local $0 i32) ;; CHECK-NEXT: (drop - ;; CHECK-NEXT: (call $idempotent - ;; CHECK-NEXT: (i32.const 10) + ;; CHECK-NEXT: (local.tee $0 + ;; CHECK-NEXT: (call $idempotent + ;; CHECK-NEXT: (i32.const 10) + ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) ;; CHECK-NEXT: (drop - ;; CHECK-NEXT: (call $idempotent - ;; CHECK-NEXT: (i32.const 10) - ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.get $0) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) (func $yes @@ -110,4 +117,58 @@ ) ) ) + + ;; CHECK: (func $idem-effects (type $0) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (call $idempotent + ;; CHECK-NEXT: (global.get $mutable) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (call $idempotent + ;; CHECK-NEXT: (global.get $mutable) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $idem-effects + ;; An idempotent function still has effects on the first call. Those effects + ;; can invalidate the global.get here. + (drop + (call $idempotent + (global.get $mutable) + ) + ) + (drop + (call $idempotent + (global.get $mutable) + ) + ) + ) + + ;; CHECK: (func $idem-effects-immutable (type $0) + ;; CHECK-NEXT: (local $0 i32) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (local.tee $0 + ;; CHECK-NEXT: (call $idempotent + ;; CHECK-NEXT: (global.get $immutable) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (local.get $0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $idem-effects-immutable + ;; But here we read an immutable value, so we can optimize. + (drop + (call $idempotent + (global.get $immutable) + ) + ) + (drop + (call $idempotent + (global.get $immutable) + ) + ) + ) ) From 86d2770258c8603a555050e6f94276f51c8ae268 Mon Sep 17 00:00:00 2001 From: Alon Zakai Date: Wed, 25 Feb 2026 11:15:59 -0800 Subject: [PATCH 4/9] fix --- src/passes/LocalCSE.cpp | 79 ++++++++++++----------- test/lit/passes/local-cse_idempotent.wast | 51 +++++++++++++++ 2 files changed, 93 insertions(+), 37 deletions(-) diff --git a/src/passes/LocalCSE.cpp b/src/passes/LocalCSE.cpp index 7dd34f84f4a..4acab85f066 100644 --- a/src/passes/LocalCSE.cpp +++ b/src/passes/LocalCSE.cpp @@ -458,46 +458,51 @@ struct Checker // Given the current expression, see what it invalidates of the currently- // hashed expressions, if there are any. if (!activeOriginals.empty()) { - if (!isIdempotent(curr, *getModule())) { // XXX - EffectAnalyzer effects(options, *getModule()); - // We can ignore traps here: - // - // (ORIGINAL) - // (curr) - // (COPY) - // - // We are some code in between an original and a copy of it, and we are - // trying to turn COPY into a local.get of a value that we stash at the - // original. If |curr| traps then we simply don't reach the copy anyhow. - effects.trap = false; - // We only need to visit this node itself, as we have already visited its - // children by the time we get here. - effects.visit(curr); - - std::vector invalidated; - for (auto& kv : activeOriginals) { - auto* original = kv.first; - auto& originalInfo = kv.second; - if (effects.invalidates(originalInfo.effects)) { - invalidated.push_back(original); - } + EffectAnalyzer effects(options, *getModule()); + // We can ignore traps here: + // + // (ORIGINAL) + // (curr) + // (COPY) + // + // We are some code in between an original and a copy of it, and we are + // trying to turn COPY into a local.get of a value that we stash at the + // original. If |curr| traps then we simply don't reach the copy anyhow. + effects.trap = false; + // We only need to visit this node itself, as we have already visited its + // children by the time we get here. + effects.visit(curr); + + auto idempotent = isIdempotent(curr, *getModule()); + + std::vector invalidated; + for (auto& kv : activeOriginals) { + auto* original = kv.first; + if (idempotent && ExpressionAnalyzer::shallowEqual(curr, original)) { + // |curr| is idempotent, so it does not invalidate later appearances + // of itself. + continue; } + auto& originalInfo = kv.second; + if (effects.invalidates(originalInfo.effects)) { + invalidated.push_back(original); + } + } - for (auto* original : invalidated) { - // Remove all requests after this expression, as we cannot optimize to - // them. - requestInfos[original].requests -= - activeOriginals.at(original).requestsLeft; - - // If no requests remain at all (that is, there were no requests we - // could provide before we ran into this invalidation) then we do not - // need this original at all. - if (requestInfos[original].requests == 0) { - requestInfos.erase(original); - } - - activeOriginals.erase(original); + for (auto* original : invalidated) { + // Remove all requests after this expression, as we cannot optimize to + // them. + requestInfos[original].requests -= + activeOriginals.at(original).requestsLeft; + + // If no requests remain at all (that is, there were no requests we + // could provide before we ran into this invalidation) then we do not + // need this original at all. + if (requestInfos[original].requests == 0) { + requestInfos.erase(original); } + + activeOriginals.erase(original); } } diff --git a/test/lit/passes/local-cse_idempotent.wast b/test/lit/passes/local-cse_idempotent.wast index 4f92862aece..9cd517c4d7f 100644 --- a/test/lit/passes/local-cse_idempotent.wast +++ b/test/lit/passes/local-cse_idempotent.wast @@ -171,4 +171,55 @@ ) ) ) + + ;; CHECK: (func $idem-effects-interact (type $0) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (i32.eq + ;; CHECK-NEXT: (global.get $mutable) + ;; CHECK-NEXT: (global.get $mutable) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (call $idempotent + ;; CHECK-NEXT: (global.get $immutable) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (i32.eq + ;; CHECK-NEXT: (global.get $mutable) + ;; CHECK-NEXT: (global.get $mutable) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (call $idempotent + ;; CHECK-NEXT: (global.get $immutable) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $idem-effects-interact + ;; An idempotent function still has effects on the first call. That + ;; prevents optimizing the i32.eq, as the first call might alter $mutable. + (drop + (i32.eq + (global.get $mutable) + (global.get $mutable) + ) + ) + (drop + (call $idempotent + (global.get $immutable) + ) + ) + (drop + (i32.eq + (global.get $mutable) + (global.get $mutable) + ) + ) + (drop + (call $idempotent + (global.get $immutable) + ) + ) + ) ) From cbd55d6f81128ee7ec3b1050abaded41b1a741d8 Mon Sep 17 00:00:00 2001 From: Alon Zakai Date: Wed, 25 Feb 2026 11:22:52 -0800 Subject: [PATCH 5/9] test --- test/lit/passes/local-cse_idempotent.wast | 54 +++++++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/test/lit/passes/local-cse_idempotent.wast b/test/lit/passes/local-cse_idempotent.wast index 9cd517c4d7f..18b83f95317 100644 --- a/test/lit/passes/local-cse_idempotent.wast +++ b/test/lit/passes/local-cse_idempotent.wast @@ -222,4 +222,58 @@ ) ) ) + + ;; CHECK: (func $idem-effects-interact-2 (type $0) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (call $idempotent + ;; CHECK-NEXT: (global.get $immutable) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (i32.eq + ;; CHECK-NEXT: (global.get $mutable) + ;; CHECK-NEXT: (global.get $mutable) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (call $idempotent + ;; CHECK-NEXT: (global.get $immutable) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (i32.eq + ;; CHECK-NEXT: (global.get $mutable) + ;; CHECK-NEXT: (global.get $mutable) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $idem-effects-interact-2 + ;; As above, but interleaved differently. The first i32.eq is now between + ;; the two idempotent calls. Here we could optimize, but do not atm, as the + ;; mutable reads invalidate the first call (only the reverse is true, but + ;; our check is symmetrical atm), and the second call - which we fail to + ;; optimize out - invalidates the later mutable reads. TODO + (drop + (call $idempotent + (global.get $immutable) + ) + ) + (drop + (i32.eq + (global.get $mutable) + (global.get $mutable) + ) + ) + (drop + (call $idempotent + (global.get $immutable) + ) + ) + (drop + (i32.eq + (global.get $mutable) + (global.get $mutable) + ) + ) + ) ) From ef597d37247f1fa8f6ca1fc2f95f971616a11521 Mon Sep 17 00:00:00 2001 From: Alon Zakai Date: Wed, 25 Feb 2026 11:23:35 -0800 Subject: [PATCH 6/9] fuzz --- scripts/test/fuzzing.py | 1 + 1 file changed, 1 insertion(+) diff --git a/scripts/test/fuzzing.py b/scripts/test/fuzzing.py index c534afb5014..6309b9d1115 100644 --- a/scripts/test/fuzzing.py +++ b/scripts/test/fuzzing.py @@ -119,6 +119,7 @@ 'idempotent.wast', 'optimize-instructions_idempotent.wast', 'duplicate-function-elimination_annotations.wast', + 'local-cse_idempotent.wast', # Not fully implemented. 'waitqueue.wast', ] From c03fc3062795089c6f6bd4e1579fc7d8d7223a02 Mon Sep 17 00:00:00 2001 From: Alon Zakai Date: Wed, 25 Feb 2026 14:02:12 -0800 Subject: [PATCH 7/9] todo --- src/passes/LocalCSE.cpp | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/passes/LocalCSE.cpp b/src/passes/LocalCSE.cpp index 4acab85f066..b17ee43157f 100644 --- a/src/passes/LocalCSE.cpp +++ b/src/passes/LocalCSE.cpp @@ -208,6 +208,13 @@ struct RequestInfoMap : public std::unordered_map { } }; +// Check if a call is to an idempotent function. Note that we do not check for +// an annotation on the call itself - optimizing that would require us to verify +// idempotency on the later one specifically, but that is complicated in this +// pass (we'd end up tracking the first one unnecessarily, hoping the second is +// idempotent). Instead, we look on the called function, which is identical for +// all callers. +// TODO if users want the call annotation, handle that too. bool isIdempotent(Expression* curr, Module& wasm) { if (auto* call = curr->dynCast()) { if (Intrinsics::getAnnotations(wasm.getFunction(call->target)).idempotent) { From 963524cfc0e11632d9a43ee233d5d4a3b22e82d9 Mon Sep 17 00:00:00 2001 From: Alon Zakai Date: Wed, 25 Feb 2026 14:03:58 -0800 Subject: [PATCH 8/9] i32.eq=>add --- test/lit/passes/local-cse_idempotent.wast | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/test/lit/passes/local-cse_idempotent.wast b/test/lit/passes/local-cse_idempotent.wast index 18b83f95317..a211e7e7d0f 100644 --- a/test/lit/passes/local-cse_idempotent.wast +++ b/test/lit/passes/local-cse_idempotent.wast @@ -174,7 +174,7 @@ ;; CHECK: (func $idem-effects-interact (type $0) ;; CHECK-NEXT: (drop - ;; CHECK-NEXT: (i32.eq + ;; CHECK-NEXT: (i32.add ;; CHECK-NEXT: (global.get $mutable) ;; CHECK-NEXT: (global.get $mutable) ;; CHECK-NEXT: ) @@ -185,7 +185,7 @@ ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) ;; CHECK-NEXT: (drop - ;; CHECK-NEXT: (i32.eq + ;; CHECK-NEXT: (i32.add ;; CHECK-NEXT: (global.get $mutable) ;; CHECK-NEXT: (global.get $mutable) ;; CHECK-NEXT: ) @@ -198,9 +198,9 @@ ;; CHECK-NEXT: ) (func $idem-effects-interact ;; An idempotent function still has effects on the first call. That - ;; prevents optimizing the i32.eq, as the first call might alter $mutable. + ;; prevents optimizing the i32.add, as the first call might alter $mutable. (drop - (i32.eq + (i32.add (global.get $mutable) (global.get $mutable) ) @@ -211,7 +211,7 @@ ) ) (drop - (i32.eq + (i32.add (global.get $mutable) (global.get $mutable) ) @@ -230,7 +230,7 @@ ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) ;; CHECK-NEXT: (drop - ;; CHECK-NEXT: (i32.eq + ;; CHECK-NEXT: (i32.add ;; CHECK-NEXT: (global.get $mutable) ;; CHECK-NEXT: (global.get $mutable) ;; CHECK-NEXT: ) @@ -241,14 +241,14 @@ ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) ;; CHECK-NEXT: (drop - ;; CHECK-NEXT: (i32.eq + ;; CHECK-NEXT: (i32.add ;; CHECK-NEXT: (global.get $mutable) ;; CHECK-NEXT: (global.get $mutable) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) (func $idem-effects-interact-2 - ;; As above, but interleaved differently. The first i32.eq is now between + ;; As above, but interleaved differently. The first i32.add is now between ;; the two idempotent calls. Here we could optimize, but do not atm, as the ;; mutable reads invalidate the first call (only the reverse is true, but ;; our check is symmetrical atm), and the second call - which we fail to @@ -259,7 +259,7 @@ ) ) (drop - (i32.eq + (i32.add (global.get $mutable) (global.get $mutable) ) @@ -270,7 +270,7 @@ ) ) (drop - (i32.eq + (i32.add (global.get $mutable) (global.get $mutable) ) From e683ccbbc30c1dc189ff47db35fe4f30a82f5ac4 Mon Sep 17 00:00:00 2001 From: Alon Zakai Date: Wed, 25 Feb 2026 15:33:35 -0800 Subject: [PATCH 9/9] test --- test/lit/idempotent.wast | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/test/lit/idempotent.wast b/test/lit/idempotent.wast index d29c48ebbe1..16cc293033d 100644 --- a/test/lit/idempotent.wast +++ b/test/lit/idempotent.wast @@ -20,5 +20,14 @@ (i32.const 0) ) ) + + ;; CHECK: (func $call (type $0) + ;; CHECK-NEXT: (@binaryen.idempotent) + ;; CHECK-NEXT: (call $call) + ;; CHECK-NEXT: ) + (func $call + (@binaryen.idempotent) + (call $call) + ) )