From df4c8aaa288e896936377c4cef501f39919184db Mon Sep 17 00:00:00 2001 From: Alon Zakai Date: Thu, 19 Feb 2026 13:15:30 -0800 Subject: [PATCH 01/29] almost --- src/parser/contexts.h | 2 ++ src/passes/Print.cpp | 6 ++++++ src/passes/StripToolchainAnnotations.cpp | 1 + src/wasm-annotations.h | 1 + src/wasm.h | 7 ++++++- src/wasm/wasm-ir-builder.cpp | 5 +++++ src/wasm/wasm.cpp | 1 + 7 files changed, 22 insertions(+), 1 deletion(-) diff --git a/src/parser/contexts.h b/src/parser/contexts.h index c16ac92268e..fc40fa7a0aa 100644 --- a/src/parser/contexts.h +++ b/src/parser/contexts.h @@ -1321,6 +1321,8 @@ struct AnnotationParserCtx { ret.removableIfUnused = true; } else if (a.kind == Annotations::JSCalledHint) { ret.jsCalled = true; + } else if (a.kind == Annotations::IdempotentHint) { + ret.idempotent = true; } } diff --git a/src/passes/Print.cpp b/src/passes/Print.cpp index 6f2f558e1ec..fabe4e8ed5a 100644 --- a/src/passes/Print.cpp +++ b/src/passes/Print.cpp @@ -2803,6 +2803,12 @@ void PrintSExpression::printCodeAnnotations(Expression* curr) { restoreNormalColor(o); doIndent(o, indent); } + if (annotation.idempotent) { + Colors::grey(o); + o << "(@" << Annotations::IdempotentHint << ")\n"; + restoreNormalColor(o); + doIndent(o, indent); + } } } diff --git a/src/passes/StripToolchainAnnotations.cpp b/src/passes/StripToolchainAnnotations.cpp index c590afe964d..e55f6e56a04 100644 --- a/src/passes/StripToolchainAnnotations.cpp +++ b/src/passes/StripToolchainAnnotations.cpp @@ -44,6 +44,7 @@ struct StripToolchainAnnotations auto& annotation = iter->second; annotation.removableIfUnused = false; annotation.jsCalled = false; + annotation.idempotent = false; // If nothing remains, remove the entire annotation. if (annotation == CodeAnnotation()) { diff --git a/src/wasm-annotations.h b/src/wasm-annotations.h index 17911a78ecb..b71424685c7 100644 --- a/src/wasm-annotations.h +++ b/src/wasm-annotations.h @@ -29,6 +29,7 @@ extern const Name BranchHint; extern const Name InlineHint; extern const Name RemovableIfUnusedHint; extern const Name JSCalledHint; +extern const Name IdempotentHint; } // namespace wasm::Annotations diff --git a/src/wasm.h b/src/wasm.h index c35b1ea2531..fe913badec9 100644 --- a/src/wasm.h +++ b/src/wasm.h @@ -2258,10 +2258,15 @@ struct CodeAnnotation { // identity does not matter for such functions. bool jsCalled = false; + // A function that may do something on the first call, but all subsequent + // calls can be assumed to have no effects. If a value is returned, it will be + // the same value as returned earlier. + bool idempotent = false; + bool operator==(const CodeAnnotation& other) const { return branchLikely == other.branchLikely && inline_ == other.inline_ && removableIfUnused == other.removableIfUnused && - jsCalled == other.jsCalled; + jsCalled == other.jsCalled && idempotent == other.idempotent; } }; diff --git a/src/wasm/wasm-ir-builder.cpp b/src/wasm/wasm-ir-builder.cpp index 23ff8764971..58a403c04fe 100644 --- a/src/wasm/wasm-ir-builder.cpp +++ b/src/wasm/wasm-ir-builder.cpp @@ -2677,6 +2677,11 @@ void IRBuilder::applyAnnotations(Expression* expr, assert(func); func->codeAnnotations[expr].jsCalled = true; } + + if (annotation.idempotent) { + assert(func); + func->codeAnnotations[expr].idempotent = true; + } } } // namespace wasm diff --git a/src/wasm/wasm.cpp b/src/wasm/wasm.cpp index 959b6cd4bfe..19910b3a4f9 100644 --- a/src/wasm/wasm.cpp +++ b/src/wasm/wasm.cpp @@ -71,6 +71,7 @@ const Name BranchHint = "metadata.code.branch_hint"; const Name InlineHint = "metadata.code.inline"; const Name RemovableIfUnusedHint = "binaryen.removable.if.unused"; const Name JSCalledHint = "binaryen.js.called"; +const Name IdempotentHint = "binaryen.idempotent"; } // namespace Annotations From 0b326e2121174fa9f303793b0dd04421c5ae05bf Mon Sep 17 00:00:00 2001 From: Alon Zakai Date: Thu, 19 Feb 2026 15:34:21 -0800 Subject: [PATCH 02/29] go --- src/wasm-binary.h | 2 ++ src/wasm/wasm-binary.cpp | 34 ++++++++++++++++++---------------- 2 files changed, 20 insertions(+), 16 deletions(-) diff --git a/src/wasm-binary.h b/src/wasm-binary.h index 52a6ad9dfeb..2c661d97110 100644 --- a/src/wasm-binary.h +++ b/src/wasm-binary.h @@ -1444,6 +1444,7 @@ class WasmBinaryWriter { std::optional getInlineHintsBuffer(); std::optional getRemovableIfUnusedHintsBuffer(); std::optional getJSCalledHintsBuffer(); + std::optional getIdempotentHintsBuffer(); // helpers void writeInlineString(std::string_view name); @@ -1738,6 +1739,7 @@ class WasmBinaryReader { void readInlineHints(size_t payloadLen); void readRemovableIfUnusedHints(size_t payloadLen); void readJSCalledHints(size_t payloadLen); + void readIdempotentHints(size_t payloadLen); std::tuple readMemoryAccess(bool isAtomic, bool isRMW); diff --git a/src/wasm/wasm-binary.cpp b/src/wasm/wasm-binary.cpp index 6783b1eafbb..c19779c4842 100644 --- a/src/wasm/wasm-binary.cpp +++ b/src/wasm/wasm-binary.cpp @@ -1629,6 +1629,7 @@ std::optional WasmBinaryWriter::writeCodeAnnotations() { append(getInlineHintsBuffer()); append(getRemovableIfUnusedHintsBuffer()); append(getJSCalledHintsBuffer()); + append(getIdempotentHintsBuffer()); return ret; } @@ -1771,28 +1772,29 @@ std::optional WasmBinaryWriter::getInlineHintsBuffer() { }); } +#define WRITE_BOOLEAN_HINT(code, field) \ + return writeExpressionHints( \ + code, \ + [](const CodeAnnotation& annotation) { \ + return annotation.#field; \ + }, \ + [](const CodeAnnotation& annotation, BufferWithRandomAccess& buffer) { \ + buffer << U32LEB(0); \ + }); + std::optional WasmBinaryWriter::getRemovableIfUnusedHintsBuffer() { - return writeExpressionHints( - Annotations::RemovableIfUnusedHint, - [](const CodeAnnotation& annotation) { - return annotation.removableIfUnused; - }, - [](const CodeAnnotation& annotation, BufferWithRandomAccess& buffer) { - // Hint size, always empty. - buffer << U32LEB(0); - }); + WRITE_BOOLEAN_HINT(Annotations::RemovableIfUnusedHint, removableIfUnused); } std::optional WasmBinaryWriter::getJSCalledHintsBuffer() { - return writeExpressionHints( - Annotations::JSCalledHint, - [](const CodeAnnotation& annotation) { return annotation.jsCalled; }, - [](const CodeAnnotation& annotation, BufferWithRandomAccess& buffer) { - // Hint size, always empty. - buffer << U32LEB(0); - }); + WRITE_BOOLEAN_HINT(Annotations::JSCalledHint, jsCalled); +} + +std::optional +WasmBinaryWriter::getIdempotentHintsBuffer() { + WRITE_BOOLEAN_HINT(Annotations::IdempotentHint, idempotent); } void WasmBinaryWriter::writeData(const char* data, size_t size) { From a95d3edc7c5f1dcf544ac12bd8ce3996e2976dbb Mon Sep 17 00:00:00 2001 From: Alon Zakai Date: Thu, 19 Feb 2026 15:34:27 -0800 Subject: [PATCH 03/29] fmt --- src/wasm/wasm-binary.cpp | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/src/wasm/wasm-binary.cpp b/src/wasm/wasm-binary.cpp index c19779c4842..103f105ae24 100644 --- a/src/wasm/wasm-binary.cpp +++ b/src/wasm/wasm-binary.cpp @@ -1772,14 +1772,12 @@ std::optional WasmBinaryWriter::getInlineHintsBuffer() { }); } -#define WRITE_BOOLEAN_HINT(code, field) \ - return writeExpressionHints( \ - code, \ - [](const CodeAnnotation& annotation) { \ - return annotation.#field; \ - }, \ - [](const CodeAnnotation& annotation, BufferWithRandomAccess& buffer) { \ - buffer << U32LEB(0); \ +#define WRITE_BOOLEAN_HINT(code, field) \ + return writeExpressionHints( \ + code, \ + [](const CodeAnnotation& annotation) { return annotation.#field; }, \ + [](const CodeAnnotation& annotation, BufferWithRandomAccess& buffer) { \ + buffer << U32LEB(0); \ }); std::optional From d66a33a9015c10adcd58ab06d4f7c0be50e0fa5e Mon Sep 17 00:00:00 2001 From: Alon Zakai Date: Thu, 19 Feb 2026 15:35:35 -0800 Subject: [PATCH 04/29] comment --- src/wasm/wasm-binary.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/wasm/wasm-binary.cpp b/src/wasm/wasm-binary.cpp index 103f105ae24..f8112824339 100644 --- a/src/wasm/wasm-binary.cpp +++ b/src/wasm/wasm-binary.cpp @@ -1772,6 +1772,8 @@ std::optional WasmBinaryWriter::getInlineHintsBuffer() { }); } +// Writes a simple boolean hint of size 0. Receives the code and the field name +// on the annotation object. #define WRITE_BOOLEAN_HINT(code, field) \ return writeExpressionHints( \ code, \ From 881576bdbcfecd8690c7788bca21cb6338faea4f Mon Sep 17 00:00:00 2001 From: Alon Zakai Date: Thu, 19 Feb 2026 15:38:56 -0800 Subject: [PATCH 05/29] more --- src/wasm/wasm-binary.cpp | 42 ++++++++++++++++++++++------------------ 1 file changed, 23 insertions(+), 19 deletions(-) diff --git a/src/wasm/wasm-binary.cpp b/src/wasm/wasm-binary.cpp index f8112824339..411f409dc23 100644 --- a/src/wasm/wasm-binary.cpp +++ b/src/wasm/wasm-binary.cpp @@ -2072,7 +2072,8 @@ void WasmBinaryReader::preScan() { if (sectionName == Annotations::BranchHint || sectionName == Annotations::InlineHint || sectionName == Annotations::RemovableIfUnusedHint || - sectionName == Annotations::JSCalledHint) { + sectionName == Annotations::JSCalledHint || + sectionName == Annotations::IdempotentHint) { // Code annotations require code locations. // TODO: We could note which functions require code locations, as an // optimization. @@ -2237,6 +2238,9 @@ void WasmBinaryReader::readCustomSection(size_t payloadLen) { } else if (sectionName == Annotations::JSCalledHint) { deferredAnnotationSections.push_back(AnnotationSectionInfo{ pos, [this, payloadLen]() { this->readJSCalledHints(payloadLen); }}); + } else if (sectionName == Annotations::IdempotentHint) { + deferredAnnotationSections.push_back(AnnotationSectionInfo{ + pos, [this, payloadLen]() { this->readIdempotentHints(payloadLen); }}); } else { // an unfamiliar custom section if (sectionName.equals(BinaryConsts::CustomSections::Linking)) { @@ -5551,29 +5555,29 @@ void WasmBinaryReader::readInlineHints(size_t payloadLen) { }); } -void WasmBinaryReader::readRemovableIfUnusedHints(size_t payloadLen) { - readExpressionHints(Annotations::RemovableIfUnusedHint, - payloadLen, - [&](CodeAnnotation& annotation) { - auto size = getU32LEB(); - if (size != 0) { - throwError("bad removableIfUnusedHint size"); - } - - annotation.removableIfUnused = true; +// Reads a simple boolean hint of size 0. Receives the code and the field name +// on the annotation object. +#define READ_BOOLEAN_HINT(code, field) \ + readExpressionHints(code, \ + payloadLen, \ + [&](CodeAnnotation& annotation) { \ + auto size = getU32LEB(); \ + if (size != 0) { \ + throwError("bad " ##field " hint size"); \ + } \ + annotation.#field = true; \ }); + +void WasmBinaryReader::readRemovableIfUnusedHints(size_t payloadLen) { + READ_BOOLEAN_HINT(Annotations::RemovableIfUnusedHint, removableIfUnused); } void WasmBinaryReader::readJSCalledHints(size_t payloadLen) { - readExpressionHints( - Annotations::JSCalledHint, payloadLen, [&](CodeAnnotation& annotation) { - auto size = getU32LEB(); - if (size != 0) { - throwError("bad jsCalledHint size"); - } + READ_BOOLEAN_HINT(Annotations::RemovableIfUnusedHint, removableIfUnused); +} - annotation.jsCalled = true; - }); +void WasmBinaryReader::readIdempotentHints(size_t payloadLen) { + READ_BOOLEAN_HINT(Annotations::IdempotentHint, idempotent); } std::tuple From 48d8384189b7ab11d4c7fe5cb0aca089c4477665 Mon Sep 17 00:00:00 2001 From: Alon Zakai Date: Thu, 19 Feb 2026 15:43:56 -0800 Subject: [PATCH 06/29] more --- src/wasm/wasm-binary.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/wasm/wasm-binary.cpp b/src/wasm/wasm-binary.cpp index 411f409dc23..0a030d5e9e4 100644 --- a/src/wasm/wasm-binary.cpp +++ b/src/wasm/wasm-binary.cpp @@ -1777,7 +1777,7 @@ std::optional WasmBinaryWriter::getInlineHintsBuffer() { #define WRITE_BOOLEAN_HINT(code, field) \ return writeExpressionHints( \ code, \ - [](const CodeAnnotation& annotation) { return annotation.#field; }, \ + [](const CodeAnnotation& annotation) { return annotation.##field; }, \ [](const CodeAnnotation& annotation, BufferWithRandomAccess& buffer) { \ buffer << U32LEB(0); \ }); @@ -5563,9 +5563,9 @@ void WasmBinaryReader::readInlineHints(size_t payloadLen) { [&](CodeAnnotation& annotation) { \ auto size = getU32LEB(); \ if (size != 0) { \ - throwError("bad " ##field " hint size"); \ + throwError("bad " #field " hint size"); \ } \ - annotation.#field = true; \ + annotation.##field = true; \ }); void WasmBinaryReader::readRemovableIfUnusedHints(size_t payloadLen) { @@ -5573,7 +5573,7 @@ void WasmBinaryReader::readRemovableIfUnusedHints(size_t payloadLen) { } void WasmBinaryReader::readJSCalledHints(size_t payloadLen) { - READ_BOOLEAN_HINT(Annotations::RemovableIfUnusedHint, removableIfUnused); + READ_BOOLEAN_HINT(Annotations::JSCalledHint, jsCalled); } void WasmBinaryReader::readIdempotentHints(size_t payloadLen) { From c769ed0774f50988c4e683d735c138f830129af1 Mon Sep 17 00:00:00 2001 From: Alon Zakai Date: Thu, 19 Feb 2026 15:45:30 -0800 Subject: [PATCH 07/29] more --- src/wasm/wasm-binary.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/wasm/wasm-binary.cpp b/src/wasm/wasm-binary.cpp index 0a030d5e9e4..96c092941ba 100644 --- a/src/wasm/wasm-binary.cpp +++ b/src/wasm/wasm-binary.cpp @@ -1777,7 +1777,7 @@ std::optional WasmBinaryWriter::getInlineHintsBuffer() { #define WRITE_BOOLEAN_HINT(code, field) \ return writeExpressionHints( \ code, \ - [](const CodeAnnotation& annotation) { return annotation.##field; }, \ + [](const CodeAnnotation& annotation) { return annotation.field; }, \ [](const CodeAnnotation& annotation, BufferWithRandomAccess& buffer) { \ buffer << U32LEB(0); \ }); @@ -5565,7 +5565,7 @@ void WasmBinaryReader::readInlineHints(size_t payloadLen) { if (size != 0) { \ throwError("bad " #field " hint size"); \ } \ - annotation.##field = true; \ + annotation.field = true; \ }); void WasmBinaryReader::readRemovableIfUnusedHints(size_t payloadLen) { From bcd5dacbd9a4228ec4a74c5859ea2ffe6f204fe4 Mon Sep 17 00:00:00 2001 From: Alon Zakai Date: Thu, 19 Feb 2026 15:58:00 -0800 Subject: [PATCH 08/29] go --- src/ir/properties.cpp | 15 ++++++++++++--- src/ir/properties.h | 4 ++-- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/src/ir/properties.cpp b/src/ir/properties.cpp index 8bc8c1d8f6f..5b776693b74 100644 --- a/src/ir/properties.cpp +++ b/src/ir/properties.cpp @@ -14,6 +14,7 @@ * limitations under the License. */ +#include "ir/intrinsics.h" #include "ir/properties.h" #include "wasm-traversal.h" @@ -25,7 +26,13 @@ struct GenerativityScanner : public PostWalker { bool generative = false; void visitCall(Call* curr) { - // TODO: We could in principle look at the called function to see if it is + // If the called function is idempotent, then it does not generate new + // values on each call. + auto* target = getModule()->getFunction(curr->target); + if (Intrinsics::getAnnotations(target).idempotent) { + return; + } + // TODO: We could look at the called function's contents to see if it is // generative. To do that we'd need to compute generativity like we // compute global effects (we can't just peek from here, as the // other function might be modified in parallel). @@ -43,15 +50,17 @@ struct GenerativityScanner : public PostWalker { } // anonymous namespace -bool isGenerative(Expression* curr) { +bool isGenerative(Expression* curr, Module& wasm) { GenerativityScanner scanner; + scanner.setModule(&wasm); scanner.walk(curr); return scanner.generative; } // As above, but only checks |curr| and not children. -bool isShallowlyGenerative(Expression* curr) { +bool isShallowlyGenerative(Expression* curr, Module& wasm) { GenerativityScanner scanner; + scanner.setModule(&wasm); scanner.visit(curr); return scanner.generative; } diff --git a/src/ir/properties.h b/src/ir/properties.h index e763ccd9098..f3bc55ac4ab 100644 --- a/src/ir/properties.h +++ b/src/ir/properties.h @@ -597,10 +597,10 @@ inline bool hasUnwritableTypeImmediate(Expression* curr) { // the latter because calls are already handled best in other manners (using // EffectAnalyzer). // -bool isGenerative(Expression* curr); +bool isGenerative(Expression* curr, Module& wasm); // As above, but only checks |curr| and not children. -bool isShallowlyGenerative(Expression* curr); +bool isShallowlyGenerative(Expression* curr, Module& wasm); // Whether this expression is valid in a context where WebAssembly requires a // constant expression, such as a global initializer. From ef44cc232fe263c82133ebc4e2c3fe51ea255515 Mon Sep 17 00:00:00 2001 From: Alon Zakai Date: Thu, 19 Feb 2026 15:58:11 -0800 Subject: [PATCH 09/29] form --- src/ir/properties.cpp | 2 +- src/wasm/wasm-binary.cpp | 24 +++++++++++------------- 2 files changed, 12 insertions(+), 14 deletions(-) diff --git a/src/ir/properties.cpp b/src/ir/properties.cpp index 5b776693b74..c4efa686e2b 100644 --- a/src/ir/properties.cpp +++ b/src/ir/properties.cpp @@ -14,8 +14,8 @@ * limitations under the License. */ -#include "ir/intrinsics.h" #include "ir/properties.h" +#include "ir/intrinsics.h" #include "wasm-traversal.h" namespace wasm::Properties { diff --git a/src/wasm/wasm-binary.cpp b/src/wasm/wasm-binary.cpp index 96c092941ba..7b6e9f6ac56 100644 --- a/src/wasm/wasm-binary.cpp +++ b/src/wasm/wasm-binary.cpp @@ -1777,7 +1777,7 @@ std::optional WasmBinaryWriter::getInlineHintsBuffer() { #define WRITE_BOOLEAN_HINT(code, field) \ return writeExpressionHints( \ code, \ - [](const CodeAnnotation& annotation) { return annotation.field; }, \ + [](const CodeAnnotation& annotation) { return annotation.field; }, \ [](const CodeAnnotation& annotation, BufferWithRandomAccess& buffer) { \ buffer << U32LEB(0); \ }); @@ -5557,18 +5557,16 @@ void WasmBinaryReader::readInlineHints(size_t payloadLen) { // Reads a simple boolean hint of size 0. Receives the code and the field name // on the annotation object. -#define READ_BOOLEAN_HINT(code, field) \ - readExpressionHints(code, \ - payloadLen, \ - [&](CodeAnnotation& annotation) { \ - auto size = getU32LEB(); \ - if (size != 0) { \ - throwError("bad " #field " hint size"); \ - } \ - annotation.field = true; \ - }); - -void WasmBinaryReader::readRemovableIfUnusedHints(size_t payloadLen) { +#define READ_BOOLEAN_HINT(code, field) \ + readExpressionHints(code, payloadLen, [&](CodeAnnotation& annotation) { \ + auto size = getU32LEB(); \ + if (size != 0) { \ + throwError("bad " #field " hint size"); \ + } \ + annotation.field = true; \ + }); + +void WasmBinaryReader::readRemovableIfUnusedHints(size_t payloadLen) { READ_BOOLEAN_HINT(Annotations::RemovableIfUnusedHint, removableIfUnused); } From 148fa86be02b9ec1907dfa4294bb244ecee102c3 Mon Sep 17 00:00:00 2001 From: Alon Zakai Date: Thu, 19 Feb 2026 15:59:53 -0800 Subject: [PATCH 10/29] more --- src/wasm.h | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/wasm.h b/src/wasm.h index fe913badec9..b7e2fa2962a 100644 --- a/src/wasm.h +++ b/src/wasm.h @@ -2327,6 +2327,13 @@ class Function : public Importable { // the 0 byte offset in the spec. As with debug info, we do not store these on // Expressions as we assume most instances are unannotated, and do not want to // add constant memory overhead. + // XXX As an unordered map, if this is modified by one thread, another should + // not be reading it. That should not happen atm - all annotations are + // set up in dedicated passes or in the binary reader - but if one pass + // could add an expression annotation, another should not at the same time + // read the function-level annotations, even though that is natural to do. + // We may want to move the function-level annotations to a dedicated + // field outside the map. std::unordered_map codeAnnotations; // The effects for this function, if they have been computed. We use a shared From 80ad047da4fbbc4d69d98a7d16b03ba6d9a72368 Mon Sep 17 00:00:00 2001 From: Alon Zakai Date: Thu, 19 Feb 2026 16:05:17 -0800 Subject: [PATCH 11/29] more --- src/ir/intrinsics.h | 4 ++-- src/passes/LocalCSE.cpp | 2 +- src/passes/OptimizeInstructions.cpp | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/ir/intrinsics.h b/src/ir/intrinsics.h index 9be2caebc7d..343fc2d07a4 100644 --- a/src/ir/intrinsics.h +++ b/src/ir/intrinsics.h @@ -116,7 +116,7 @@ class Intrinsics { std::vector getJSCalledFunctions(); // Get the code annotations for an expression in a function. - CodeAnnotation getAnnotations(Expression* curr, Function* func) { + static CodeAnnotation getAnnotations(Expression* curr, Function* func) { auto& annotations = func->codeAnnotations; auto iter = annotations.find(curr); if (iter != annotations.end()) { @@ -126,7 +126,7 @@ class Intrinsics { } // Get the code annotations for a function itself. - CodeAnnotation getAnnotations(Function* func) { + static CodeAnnotation getAnnotations(Function* func) { return getAnnotations(nullptr, func); } diff --git a/src/passes/LocalCSE.cpp b/src/passes/LocalCSE.cpp index 9c9a8198f89..6f3f90bd4a5 100644 --- a/src/passes/LocalCSE.cpp +++ b/src/passes/LocalCSE.cpp @@ -398,7 +398,7 @@ struct Scanner // We also cannot optimize away something that is intrinsically // nondeterministic: even if it has no side effects, if it may return a // different result each time, and then we cannot optimize away repeats. - if (Properties::isShallowlyGenerative(curr)) { + if (Properties::isShallowlyGenerative(curr, *getModule())) { return false; } diff --git a/src/passes/OptimizeInstructions.cpp b/src/passes/OptimizeInstructions.cpp index fca53446d1f..3c5407e34c5 100644 --- a/src/passes/OptimizeInstructions.cpp +++ b/src/passes/OptimizeInstructions.cpp @@ -2823,7 +2823,7 @@ struct OptimizeInstructions // To be equal, they must also be known to return the same result // deterministically. - return !Properties::isGenerative(left); + return !Properties::isGenerative(left, *getModule()); } // Similar to areConsecutiveInputsEqual() but also checks if we can remove From 6493931d3fb5f5da75d0a329badacd26f9675c35 Mon Sep 17 00:00:00 2001 From: Alon Zakai Date: Thu, 19 Feb 2026 16:25:26 -0800 Subject: [PATCH 12/29] test --- src/passes/OptimizeInstructions.cpp | 4 +- .../optimize-instructions_idempotent.wast | 38 +++++++++++++++++++ 2 files changed, 41 insertions(+), 1 deletion(-) create mode 100644 test/lit/passes/optimize-instructions_idempotent.wast diff --git a/src/passes/OptimizeInstructions.cpp b/src/passes/OptimizeInstructions.cpp index 3c5407e34c5..c0817058d47 100644 --- a/src/passes/OptimizeInstructions.cpp +++ b/src/passes/OptimizeInstructions.cpp @@ -2850,7 +2850,7 @@ struct OptimizeInstructions // always fold them). This is similar to areConsecutiveInputsEqualAndRemovable // but also identifies reads from the same local variable when the first of // them is a "tee" operation and the second is a get (in which case, it is - // fine to remove the get, but not the tee). + // fine to remove the get, but not the tee), and similar things. // // The inputs here must be consecutive, but it is also ok to have code with no // side effects at all in the middle. For example, a Const in between is ok. @@ -2862,6 +2862,8 @@ struct OptimizeInstructions return true; } + // TODO + // stronger property than we need - we can not only fold // them but remove them entirely. return areConsecutiveInputsEqualAndRemovable(left, right); diff --git a/test/lit/passes/optimize-instructions_idempotent.wast b/test/lit/passes/optimize-instructions_idempotent.wast new file mode 100644 index 00000000000..94cfb7da44f --- /dev/null +++ b/test/lit/passes/optimize-instructions_idempotent.wast @@ -0,0 +1,38 @@ +;; NOTE: Assertions have been generated by update_lit_checks.py and should not be edited. + +;; RUN: wasm-opt %s --optimize-instructions -all -S -o - | filecheck %s + +;; Idempotent-marked functions can be assumed to always return the same value. + +;; RUN: wasm-opt %s --optimize-instructions -all -S -o - | filecheck %s --check-prefix=NO_FO + +(module + (@binaryen.idempotent) + (func $idempotent (param $x f32) (result f32) + ;; This function is idempotent. + (f32.const 13.37) + ) + + (func $potent (param $x f32) (result f32) + ;; This function is not idempotent + (local.get $x) + ) + + (func $test-abs + ;; These calls are identical, since the second returns the same. We can + ;; remove the abs, as multiplying a value by itself is non-negative anyhow. + (drop + (f32.abs + (call $idempotent) + (call $idempotent) + ) + ) + ;; But here we can do nothing + (drop + (f32.abs + (call $potent) + (call $potent) + ) + ) + ) +) From af34e7bb3bd2941b3f00266a573a4b1077837cbb Mon Sep 17 00:00:00 2001 From: Alon Zakai Date: Thu, 19 Feb 2026 16:32:07 -0800 Subject: [PATCH 13/29] works --- .../optimize-instructions_idempotent.wast | 91 ++++++++++++++++--- 1 file changed, 80 insertions(+), 11 deletions(-) diff --git a/test/lit/passes/optimize-instructions_idempotent.wast b/test/lit/passes/optimize-instructions_idempotent.wast index 94cfb7da44f..006f195b659 100644 --- a/test/lit/passes/optimize-instructions_idempotent.wast +++ b/test/lit/passes/optimize-instructions_idempotent.wast @@ -4,34 +4,103 @@ ;; Idempotent-marked functions can be assumed to always return the same value. -;; RUN: wasm-opt %s --optimize-instructions -all -S -o - | filecheck %s --check-prefix=NO_FO - (module + ;; CHECK: (import "a" "b" (func $import (type $1) (result f32))) + (import "a" "b" (func $import (result f32))) + + ;; CHECK: (@binaryen.idempotent) + ;; CHECK-NEXT: (func $idempotent (type $0) (param $x f32) (result f32) + ;; CHECK-NEXT: (local.get $x) + ;; CHECK-NEXT: ) (@binaryen.idempotent) (func $idempotent (param $x f32) (result f32) - ;; This function is idempotent. - (f32.const 13.37) + ;; This function is idempotent: same inputs, same outputs. TODO: document that part + (local.get $x) ) + ;; CHECK: (func $potent (type $0) (param $x f32) (result f32) + ;; CHECK-NEXT: (call $import) + ;; CHECK-NEXT: ) (func $potent (param $x f32) (result f32) - ;; This function is not idempotent - (local.get $x) + ;; This function is not idempotent - anything might happen here. + (call $import) ) + ;; CHECK: (func $test-abs (type $2) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (f32.mul + ;; CHECK-NEXT: (call $idempotent + ;; CHECK-NEXT: (f32.const 10) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (call $idempotent + ;; CHECK-NEXT: (f32.const 10) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (f32.abs + ;; CHECK-NEXT: (f32.mul + ;; CHECK-NEXT: (call $potent + ;; CHECK-NEXT: (f32.const 10) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (call $potent + ;; CHECK-NEXT: (f32.const 10) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (f32.abs + ;; CHECK-NEXT: (f32.mul + ;; CHECK-NEXT: (call $idempotent + ;; CHECK-NEXT: (f32.const 10) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (call $idempotent + ;; CHECK-NEXT: (f32.const 20) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) (func $test-abs ;; These calls are identical, since the second returns the same. We can ;; remove the abs, as multiplying a value by itself is non-negative anyhow. (drop (f32.abs - (call $idempotent) - (call $idempotent) + (f32.mul + (call $idempotent + (f32.const 10) + ) + (call $idempotent + (f32.const 10) + ) + ) + ) + ) + ;; But here we can do nothing, as we lack idempotency. + (drop + (f32.abs + (f32.mul + (call $potent + (f32.const 10) + ) + (call $potent + (f32.const 10) + ) + ) ) ) - ;; But here we can do nothing + ;; Here we fail as well, as while we have idempotency, the params differ. (drop (f32.abs - (call $potent) - (call $potent) + (f32.mul + (call $idempotent + (f32.const 10) + ) + (call $idempotent + (f32.const 20) + ) + ) ) ) ) From 97a7a52a583653e1724289361c644f7297094713 Mon Sep 17 00:00:00 2001 From: Alon Zakai Date: Thu, 19 Feb 2026 16:50:41 -0800 Subject: [PATCH 14/29] work --- scripts/test/fuzzing.py | 1 + src/ir/properties.cpp | 2 ++ src/passes/OptimizeInstructions.cpp | 23 +++++++++++++++++++---- 3 files changed, 22 insertions(+), 4 deletions(-) diff --git a/scripts/test/fuzzing.py b/scripts/test/fuzzing.py index 7f8897835ea..e0504d23e75 100644 --- a/scripts/test/fuzzing.py +++ b/scripts/test/fuzzing.py @@ -116,6 +116,7 @@ 'vacuum-removable-if-unused.wast', 'vacuum-removable-if-unused-func.wast', 'strip-toolchain-annotations-func.wast', + XXX # Not fully implemented. 'waitqueue.wast', ] diff --git a/src/ir/properties.cpp b/src/ir/properties.cpp index c4efa686e2b..093042ff288 100644 --- a/src/ir/properties.cpp +++ b/src/ir/properties.cpp @@ -28,6 +28,8 @@ struct GenerativityScanner : public PostWalker { void visitCall(Call* curr) { // If the called function is idempotent, then it does not generate new // values on each call. + // TODO: We could also check for an annotation on the call instruction + // itself, if we passed the function to isGenerative* auto* target = getModule()->getFunction(curr->target); if (Intrinsics::getAnnotations(target).idempotent) { return; diff --git a/src/passes/OptimizeInstructions.cpp b/src/passes/OptimizeInstructions.cpp index c0817058d47..6cd12129f83 100644 --- a/src/passes/OptimizeInstructions.cpp +++ b/src/passes/OptimizeInstructions.cpp @@ -2862,11 +2862,26 @@ struct OptimizeInstructions return true; } - // TODO + // To fold the right side into the left, it must have no effects. + auto rightMightHaveEffects = true; + if (auto* call = right->dynCast()) { + // If these are a pair of idempotent calls, then the second has no + // effects. (We didn't check if left is a call, but the equality check + // below does that.) + if (Intrinsics(*getModule()).getCallAnnotations(call, getFunction()).idempotent) { + rightMightHaveEffects = false; + } + } + if (rightMightHaveEffects) { + // So far it looks like right has effects, so check fully. + auto& passOptions = getPassOptions(); + if (EffectAnalyzer(passOptions, *getModule(), right) + .hasUnremovableSideEffects()) { + return false; + } + } - // stronger property than we need - we can not only fold - // them but remove them entirely. - return areConsecutiveInputsEqualAndRemovable(left, right); + return areConsecutiveInputsEqual(left, right); } // Canonicalizing the order of a symmetric binary helps us From 9b1a94cb4ce4ca5d0cdd7e46ed7f2104440c5297 Mon Sep 17 00:00:00 2001 From: Alon Zakai Date: Thu, 19 Feb 2026 16:51:33 -0800 Subject: [PATCH 15/29] work --- src/passes/OptimizeInstructions.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/passes/OptimizeInstructions.cpp b/src/passes/OptimizeInstructions.cpp index 6cd12129f83..7474c53543e 100644 --- a/src/passes/OptimizeInstructions.cpp +++ b/src/passes/OptimizeInstructions.cpp @@ -2874,8 +2874,7 @@ struct OptimizeInstructions } if (rightMightHaveEffects) { // So far it looks like right has effects, so check fully. - auto& passOptions = getPassOptions(); - if (EffectAnalyzer(passOptions, *getModule(), right) + if (EffectAnalyzer(getPassOptions(), *getModule(), right) .hasUnremovableSideEffects()) { return false; } From b38290d8c12c9e8e44b1de8b48817770c7430d51 Mon Sep 17 00:00:00 2001 From: Alon Zakai Date: Thu, 19 Feb 2026 16:57:56 -0800 Subject: [PATCH 16/29] work --- .../optimize-instructions_idempotent.wast | 104 +++++++++++++++++- 1 file changed, 103 insertions(+), 1 deletion(-) diff --git a/test/lit/passes/optimize-instructions_idempotent.wast b/test/lit/passes/optimize-instructions_idempotent.wast index 006f195b659..d9a90914cc5 100644 --- a/test/lit/passes/optimize-instructions_idempotent.wast +++ b/test/lit/passes/optimize-instructions_idempotent.wast @@ -1,6 +1,6 @@ ;; NOTE: Assertions have been generated by update_lit_checks.py and should not be edited. -;; RUN: wasm-opt %s --optimize-instructions -all -S -o - | filecheck %s +;; RUN: foreach %s %t wasm-opt --optimize-instructions -all -S -o - | filecheck %s ;; Idempotent-marked functions can be assumed to always return the same value. @@ -105,3 +105,105 @@ ) ) ) + +;; References. +(module + ;; CHECK: (type $struct (struct)) + (type $struct (struct)) + + ;; CHECK: (import "a" "b" (func $import (type $2) (result eqref))) + (import "a" "b" (func $import (result eqref))) + + ;; CHECK: (global $g1 (ref $struct) (struct.new_default $struct)) + (global $g1 (ref $struct) (struct.new $struct)) + + ;; CHECK: (global $g2 (ref $struct) (struct.new_default $struct)) + (global $g2 (ref $struct) (struct.new $struct)) + + ;; CHECK: (@binaryen.idempotent) + ;; CHECK-NEXT: (func $idempotent (type $1) (param $x eqref) (result eqref) + ;; CHECK-NEXT: (local.get $x) + ;; CHECK-NEXT: ) + (@binaryen.idempotent) + (func $idempotent (param $x eqref) (result eqref) + ;; This function is idempotent: same inputs, same outputs. TODO: document that part + (local.get $x) + ) + + ;; CHECK: (func $potent (type $1) (param $x eqref) (result eqref) + ;; CHECK-NEXT: (call $import) + ;; CHECK-NEXT: ) + (func $potent (param $x eqref) (result eqref) + ;; This function is not idempotent - anything might happen here. + (call $import) + ) + + ;; CHECK: (func $test-ref.eq (type $3) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (ref.eq + ;; CHECK-NEXT: (call $idempotent + ;; CHECK-NEXT: (global.get $g1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (call $idempotent + ;; CHECK-NEXT: (global.get $g1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (ref.eq + ;; CHECK-NEXT: (call $potent + ;; CHECK-NEXT: (global.get $g1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (call $potent + ;; CHECK-NEXT: (global.get $g1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (ref.eq + ;; CHECK-NEXT: (call $idempotent + ;; CHECK-NEXT: (global.get $g1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (call $idempotent + ;; CHECK-NEXT: (global.get $g2) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $test-ref.eq + ;; These calls are identical, since the second returns the same. This + ;; results in 1. + (drop + (ref.eq + (call $idempotent + (global.get $g1) + ) + (call $idempotent + (global.get $g1) + ) + ) + ) + ;; We cannot optimize without idempotency. + (drop + (ref.eq + (call $potent + (global.get $g1) + ) + (call $potent + (global.get $g1) + ) + ) + ) + ;; We cannot optimize here either - we have idempotency, but params differ. + (drop + (ref.eq + (call $idempotent + (global.get $g1) + ) + (call $idempotent + (global.get $g2) + ) + ) + ) + ) +) From 001edbe0f9ec97ea71b675f62f6b1d3ca8e1afca Mon Sep 17 00:00:00 2001 From: Alon Zakai Date: Thu, 19 Feb 2026 16:58:53 -0800 Subject: [PATCH 17/29] work --- test/lit/passes/optimize-instructions_idempotent.wast | 1 + 1 file changed, 1 insertion(+) diff --git a/test/lit/passes/optimize-instructions_idempotent.wast b/test/lit/passes/optimize-instructions_idempotent.wast index d9a90914cc5..30cfa78c6c6 100644 --- a/test/lit/passes/optimize-instructions_idempotent.wast +++ b/test/lit/passes/optimize-instructions_idempotent.wast @@ -173,6 +173,7 @@ (func $test-ref.eq ;; These calls are identical, since the second returns the same. This ;; results in 1. + ;; XXX wrong - first call might modify global (drop (ref.eq (call $idempotent From 868a60cf70b5cf7e78e3d9553506dab390a55e2a Mon Sep 17 00:00:00 2001 From: Alon Zakai Date: Thu, 19 Feb 2026 17:19:58 -0800 Subject: [PATCH 18/29] work --- test/lit/passes/optimize-instructions_idempotent.wast | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/lit/passes/optimize-instructions_idempotent.wast b/test/lit/passes/optimize-instructions_idempotent.wast index 30cfa78c6c6..3bb03b6077e 100644 --- a/test/lit/passes/optimize-instructions_idempotent.wast +++ b/test/lit/passes/optimize-instructions_idempotent.wast @@ -173,7 +173,7 @@ (func $test-ref.eq ;; These calls are identical, since the second returns the same. This ;; results in 1. - ;; XXX wrong - first call might modify global + ;; XXX wrong - first call might modify global. chak effects of params of right to all of left. (drop (ref.eq (call $idempotent From d446f0ce616f22c420cc0b2819ec696df9d6518b Mon Sep 17 00:00:00 2001 From: Alon Zakai Date: Fri, 20 Feb 2026 09:03:54 -0800 Subject: [PATCH 19/29] work --- .../optimize-instructions_idempotent.wast | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/test/lit/passes/optimize-instructions_idempotent.wast b/test/lit/passes/optimize-instructions_idempotent.wast index 3bb03b6077e..cd7a6fe4021 100644 --- a/test/lit/passes/optimize-instructions_idempotent.wast +++ b/test/lit/passes/optimize-instructions_idempotent.wast @@ -120,6 +120,9 @@ ;; CHECK: (global $g2 (ref $struct) (struct.new_default $struct)) (global $g2 (ref $struct) (struct.new $struct)) + ;; CHECK: (global $g-mut (mut (ref $struct)) (struct.new_default $struct)) + (global $g-mut (mut (ref $struct)) (struct.new $struct)) + ;; CHECK: (@binaryen.idempotent) ;; CHECK-NEXT: (func $idempotent (type $1) (param $x eqref) (result eqref) ;; CHECK-NEXT: (local.get $x) @@ -169,6 +172,16 @@ ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (ref.eq + ;; CHECK-NEXT: (call $idempotent + ;; CHECK-NEXT: (global.get $g-mut) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (call $idempotent + ;; CHECK-NEXT: (global.get $g-mut) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) (func $test-ref.eq ;; These calls are identical, since the second returns the same. This @@ -206,5 +219,18 @@ ) ) ) + ;; We cannot optimize here either - we have idempotency, but the global + ;; read is mutable, so the first call might modify it, making it different + ;; the second time it is read. + (drop + (ref.eq + (call $idempotent + (global.get $g-mut) + ) + (call $idempotent + (global.get $g-mut) + ) + ) + ) ) ) From 357e5197cfddfcd53817dd691b3cff36c11b382c Mon Sep 17 00:00:00 2001 From: Alon Zakai Date: Fri, 20 Feb 2026 09:11:03 -0800 Subject: [PATCH 20/29] fix --- src/ir/intrinsics.h | 3 +++ src/passes/OptimizeInstructions.cpp | 8 +++----- test/lit/passes/optimize-instructions_idempotent.wast | 1 - 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/ir/intrinsics.h b/src/ir/intrinsics.h index 343fc2d07a4..aa0cfd62cc0 100644 --- a/src/ir/intrinsics.h +++ b/src/ir/intrinsics.h @@ -155,6 +155,9 @@ class Intrinsics { if (!ret.jsCalled) { ret.jsCalled = funcAnnotations.jsCalled; } + if (!ret.idempotent) { + ret.idempotent = funcAnnotations.idempotent; + } } return ret; diff --git a/src/passes/OptimizeInstructions.cpp b/src/passes/OptimizeInstructions.cpp index 7474c53543e..dba926ec240 100644 --- a/src/passes/OptimizeInstructions.cpp +++ b/src/passes/OptimizeInstructions.cpp @@ -2833,11 +2833,9 @@ struct OptimizeInstructions // First, check for side effects. If there are any, then we can't even // assume things like local.get's of the same index being identical. (It is // also ok to have removable side effects here, see the function - // description.) - auto& passOptions = getPassOptions(); - if (EffectAnalyzer(passOptions, *getModule(), left) - .hasUnremovableSideEffects() || - EffectAnalyzer(passOptions, *getModule(), right) + // description.) We only check one side as we are going to check for + // equality below anyhow. + if (EffectAnalyzer(getPassOptions(), *getModule(), left) .hasUnremovableSideEffects()) { return false; } diff --git a/test/lit/passes/optimize-instructions_idempotent.wast b/test/lit/passes/optimize-instructions_idempotent.wast index cd7a6fe4021..1428e952cc5 100644 --- a/test/lit/passes/optimize-instructions_idempotent.wast +++ b/test/lit/passes/optimize-instructions_idempotent.wast @@ -186,7 +186,6 @@ (func $test-ref.eq ;; These calls are identical, since the second returns the same. This ;; results in 1. - ;; XXX wrong - first call might modify global. chak effects of params of right to all of left. (drop (ref.eq (call $idempotent From 314443fb1b9b00333619d72b871756126a90a618 Mon Sep 17 00:00:00 2001 From: Alon Zakai Date: Fri, 20 Feb 2026 09:11:13 -0800 Subject: [PATCH 21/29] format --- src/passes/OptimizeInstructions.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/passes/OptimizeInstructions.cpp b/src/passes/OptimizeInstructions.cpp index dba926ec240..f65c0214318 100644 --- a/src/passes/OptimizeInstructions.cpp +++ b/src/passes/OptimizeInstructions.cpp @@ -2866,7 +2866,9 @@ struct OptimizeInstructions // If these are a pair of idempotent calls, then the second has no // effects. (We didn't check if left is a call, but the equality check // below does that.) - if (Intrinsics(*getModule()).getCallAnnotations(call, getFunction()).idempotent) { + if (Intrinsics(*getModule()) + .getCallAnnotations(call, getFunction()) + .idempotent) { rightMightHaveEffects = false; } } From 0f429e491a9f294b1f6fc2e382a173032e727c3c Mon Sep 17 00:00:00 2001 From: Alon Zakai Date: Fri, 20 Feb 2026 09:15:10 -0800 Subject: [PATCH 22/29] work --- .../optimize-instructions_idempotent.wast | 30 ++++++++++++------- 1 file changed, 20 insertions(+), 10 deletions(-) diff --git a/test/lit/passes/optimize-instructions_idempotent.wast b/test/lit/passes/optimize-instructions_idempotent.wast index 1428e952cc5..9604ee40206 100644 --- a/test/lit/passes/optimize-instructions_idempotent.wast +++ b/test/lit/passes/optimize-instructions_idempotent.wast @@ -143,13 +143,18 @@ ;; CHECK: (func $test-ref.eq (type $3) ;; CHECK-NEXT: (drop - ;; CHECK-NEXT: (ref.eq - ;; CHECK-NEXT: (call $idempotent - ;; CHECK-NEXT: (global.get $g1) + ;; CHECK-NEXT: (block (result i32) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (call $idempotent + ;; CHECK-NEXT: (global.get $g1) + ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) - ;; CHECK-NEXT: (call $idempotent - ;; CHECK-NEXT: (global.get $g1) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (call $idempotent + ;; CHECK-NEXT: (global.get $g1) + ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (i32.const 1) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) ;; CHECK-NEXT: (drop @@ -173,13 +178,18 @@ ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) ;; CHECK-NEXT: (drop - ;; CHECK-NEXT: (ref.eq - ;; CHECK-NEXT: (call $idempotent - ;; CHECK-NEXT: (global.get $g-mut) + ;; CHECK-NEXT: (block (result i32) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (call $idempotent + ;; CHECK-NEXT: (global.get $g-mut) + ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) - ;; CHECK-NEXT: (call $idempotent - ;; CHECK-NEXT: (global.get $g-mut) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (call $idempotent + ;; CHECK-NEXT: (global.get $g-mut) + ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (i32.const 1) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) From 65f10b372841cc050b6e69cc92ce5e516c137a6b Mon Sep 17 00:00:00 2001 From: Alon Zakai Date: Fri, 20 Feb 2026 09:27:53 -0800 Subject: [PATCH 23/29] work --- src/passes/OptimizeInstructions.cpp | 27 +++++ .../optimize-instructions_idempotent.wast | 109 ++++++++++++++++-- 2 files changed, 124 insertions(+), 12 deletions(-) diff --git a/src/passes/OptimizeInstructions.cpp b/src/passes/OptimizeInstructions.cpp index f65c0214318..b72e9d74a0f 100644 --- a/src/passes/OptimizeInstructions.cpp +++ b/src/passes/OptimizeInstructions.cpp @@ -2869,6 +2869,33 @@ struct OptimizeInstructions if (Intrinsics(*getModule()) .getCallAnnotations(call, getFunction()) .idempotent) { + // We must still check for effects in the parameters. Imagine that we + // have + // + // (call $idempotent (global.get $g)) + // (call $idempotent (global.get $g)) + // + // Then the first call has effects, and those might alter $g if the + // global is mutable. That is, all that idempotency tells us is that + // the second call has no effects, but its parameters can still have + // read effects that interact. Also, the parameter might have write + // effects, + // + // (call $idempotent (call $other)) + // + // We must check that as well. + EffectAnalyzer childEffects(getPassOptions(), *getModule()); + for (auto* child : call->operands) { + childEffects.walk(child); + } + if (childEffects.hasUnremovableSideEffects()) { + return false; + } + ShallowEffectAnalyzer parentEffects(getPassOptions(), *getModule(), call); + if (parentEffects.invalidates(childEffects)) { + return false; + } + // No effects are possible. rightMightHaveEffects = false; } } diff --git a/test/lit/passes/optimize-instructions_idempotent.wast b/test/lit/passes/optimize-instructions_idempotent.wast index 9604ee40206..89d96d78d44 100644 --- a/test/lit/passes/optimize-instructions_idempotent.wast +++ b/test/lit/passes/optimize-instructions_idempotent.wast @@ -52,6 +52,20 @@ ;; CHECK-NEXT: (drop ;; CHECK-NEXT: (f32.abs ;; CHECK-NEXT: (f32.mul + ;; CHECK-NEXT: (@binaryen.idempotent) + ;; CHECK-NEXT: (call $potent + ;; CHECK-NEXT: (f32.const 10) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (@binaryen.idempotent) + ;; CHECK-NEXT: (call $potent + ;; CHECK-NEXT: (f32.const 10) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (f32.abs + ;; CHECK-NEXT: (f32.mul ;; CHECK-NEXT: (call $idempotent ;; CHECK-NEXT: (f32.const 10) ;; CHECK-NEXT: ) @@ -90,6 +104,21 @@ ) ) ) + ;; Here we succeed, as the calls are marked idempotent. + (drop + (f32.abs + (f32.mul + (@binaryen.idempotent) + (call $potent + (f32.const 10) + ) + (@binaryen.idempotent) + (call $potent + (f32.const 10) + ) + ) + ) + ) ;; Here we fail as well, as while we have idempotency, the params differ. (drop (f32.abs @@ -111,7 +140,7 @@ ;; CHECK: (type $struct (struct)) (type $struct (struct)) - ;; CHECK: (import "a" "b" (func $import (type $2) (result eqref))) + ;; CHECK: (import "a" "b" (func $import (type $3) (result eqref))) (import "a" "b" (func $import (result eqref))) ;; CHECK: (global $g1 (ref $struct) (struct.new_default $struct)) @@ -141,7 +170,7 @@ (call $import) ) - ;; CHECK: (func $test-ref.eq (type $3) + ;; CHECK: (func $test-ref.eq (type $2) ;; CHECK-NEXT: (drop ;; CHECK-NEXT: (block (result i32) ;; CHECK-NEXT: (drop @@ -178,18 +207,13 @@ ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) ;; CHECK-NEXT: (drop - ;; CHECK-NEXT: (block (result i32) - ;; CHECK-NEXT: (drop - ;; CHECK-NEXT: (call $idempotent - ;; CHECK-NEXT: (global.get $g-mut) - ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (ref.eq + ;; CHECK-NEXT: (call $idempotent + ;; CHECK-NEXT: (global.get $g-mut) ;; CHECK-NEXT: ) - ;; CHECK-NEXT: (drop - ;; CHECK-NEXT: (call $idempotent - ;; CHECK-NEXT: (global.get $g-mut) - ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (call $idempotent + ;; CHECK-NEXT: (global.get $g-mut) ;; CHECK-NEXT: ) - ;; CHECK-NEXT: (i32.const 1) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) @@ -242,4 +266,65 @@ ) ) ) + + ;; CHECK: (func $test-ref.eq-nested-calls (type $2) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (ref.eq + ;; CHECK-NEXT: (call $idempotent + ;; CHECK-NEXT: (call $get-struct) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (call $idempotent + ;; CHECK-NEXT: (call $get-struct) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (ref.eq + ;; CHECK-NEXT: (call $idempotent + ;; CHECK-NEXT: (@binaryen.idempotent) + ;; CHECK-NEXT: (call $get-struct) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (call $idempotent + ;; CHECK-NEXT: (@binaryen.idempotent) + ;; CHECK-NEXT: (call $get-struct) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $test-ref.eq-nested-calls + ;; We cannot optimize here - we have idempotency, but the children + ;; have effects themselves so we can't tell if they are equal. + (drop + (ref.eq + (call $idempotent + (call $get-struct) + ) + (call $idempotent + (call $get-struct) + ) + ) + ) + ;; Marking those children as idempotent should help, but we do not handle + ;; that yet. TODO + (drop + (ref.eq + (call $idempotent + (@binaryen.idempotent) + (call $get-struct) + ) + (call $idempotent + (@binaryen.idempotent) + (call $get-struct) + ) + ) + ) + ) + + ;; CHECK: (func $get-struct (type $4) (result (ref null $struct)) + ;; CHECK-NEXT: (unreachable) + ;; CHECK-NEXT: ) + (func $get-struct (result (ref null $struct)) + ;; Helper for above + (unreachable) + ) ) From f791aa62a01aa350e85d7bebef517d1c92572017 Mon Sep 17 00:00:00 2001 From: Alon Zakai Date: Fri, 20 Feb 2026 09:33:31 -0800 Subject: [PATCH 24/29] work --- scripts/test/fuzzing.py | 3 ++- src/ir/properties.cpp | 11 +++++------ src/ir/properties.h | 4 ++-- src/passes/LocalCSE.cpp | 2 +- src/passes/OptimizeInstructions.cpp | 2 +- 5 files changed, 11 insertions(+), 11 deletions(-) diff --git a/scripts/test/fuzzing.py b/scripts/test/fuzzing.py index e0504d23e75..d0355f7d402 100644 --- a/scripts/test/fuzzing.py +++ b/scripts/test/fuzzing.py @@ -116,7 +116,8 @@ 'vacuum-removable-if-unused.wast', 'vacuum-removable-if-unused-func.wast', 'strip-toolchain-annotations-func.wast', - XXX + 'idempotent.wast', + 'optimize-instructions_idempotent.wast', # Not fully implemented. 'waitqueue.wast', ] diff --git a/src/ir/properties.cpp b/src/ir/properties.cpp index 093042ff288..f2d83da8e9d 100644 --- a/src/ir/properties.cpp +++ b/src/ir/properties.cpp @@ -28,10 +28,7 @@ struct GenerativityScanner : public PostWalker { void visitCall(Call* curr) { // If the called function is idempotent, then it does not generate new // values on each call. - // TODO: We could also check for an annotation on the call instruction - // itself, if we passed the function to isGenerative* - auto* target = getModule()->getFunction(curr->target); - if (Intrinsics::getAnnotations(target).idempotent) { + if (Intrinsics(*getModule()).getCallAnnotations(curr, getFunction()).idempotent) { return; } // TODO: We could look at the called function's contents to see if it is @@ -52,16 +49,18 @@ struct GenerativityScanner : public PostWalker { } // anonymous namespace -bool isGenerative(Expression* curr, Module& wasm) { +bool isGenerative(Expression* curr, Function* func, Module& wasm) { GenerativityScanner scanner; + scanner.setFunction(func); scanner.setModule(&wasm); scanner.walk(curr); return scanner.generative; } // As above, but only checks |curr| and not children. -bool isShallowlyGenerative(Expression* curr, Module& wasm) { +bool isShallowlyGenerative(Expression* curr, Function* func, Module& wasm) { GenerativityScanner scanner; + scanner.setFunction(func); scanner.setModule(&wasm); scanner.visit(curr); return scanner.generative; diff --git a/src/ir/properties.h b/src/ir/properties.h index f3bc55ac4ab..e52902597a0 100644 --- a/src/ir/properties.h +++ b/src/ir/properties.h @@ -597,10 +597,10 @@ inline bool hasUnwritableTypeImmediate(Expression* curr) { // the latter because calls are already handled best in other manners (using // EffectAnalyzer). // -bool isGenerative(Expression* curr, Module& wasm); +bool isGenerative(Expression* curr, Function* func, Module& wasm); // As above, but only checks |curr| and not children. -bool isShallowlyGenerative(Expression* curr, Module& wasm); +bool isShallowlyGenerative(Expression* curr, Function* func, Module& wasm); // Whether this expression is valid in a context where WebAssembly requires a // constant expression, such as a global initializer. diff --git a/src/passes/LocalCSE.cpp b/src/passes/LocalCSE.cpp index 6f3f90bd4a5..79831fb89ab 100644 --- a/src/passes/LocalCSE.cpp +++ b/src/passes/LocalCSE.cpp @@ -398,7 +398,7 @@ struct Scanner // We also cannot optimize away something that is intrinsically // nondeterministic: even if it has no side effects, if it may return a // different result each time, and then we cannot optimize away repeats. - if (Properties::isShallowlyGenerative(curr, *getModule())) { + if (Properties::isShallowlyGenerative(curr, getFunction(), *getModule())) { return false; } diff --git a/src/passes/OptimizeInstructions.cpp b/src/passes/OptimizeInstructions.cpp index b72e9d74a0f..c45f3e5015e 100644 --- a/src/passes/OptimizeInstructions.cpp +++ b/src/passes/OptimizeInstructions.cpp @@ -2823,7 +2823,7 @@ struct OptimizeInstructions // To be equal, they must also be known to return the same result // deterministically. - return !Properties::isGenerative(left, *getModule()); + return !Properties::isGenerative(left, getFunction(), *getModule()); } // Similar to areConsecutiveInputsEqual() but also checks if we can remove From ed0dc576e54d81c9a4638f43a308f6e27c0162c1 Mon Sep 17 00:00:00 2001 From: Alon Zakai Date: Fri, 20 Feb 2026 09:36:29 -0800 Subject: [PATCH 25/29] fix --- .../optimize-instructions_idempotent.wast | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/test/lit/passes/optimize-instructions_idempotent.wast b/test/lit/passes/optimize-instructions_idempotent.wast index 89d96d78d44..5b5adf8103e 100644 --- a/test/lit/passes/optimize-instructions_idempotent.wast +++ b/test/lit/passes/optimize-instructions_idempotent.wast @@ -50,16 +50,14 @@ ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) ;; CHECK-NEXT: (drop - ;; CHECK-NEXT: (f32.abs - ;; CHECK-NEXT: (f32.mul - ;; CHECK-NEXT: (@binaryen.idempotent) - ;; CHECK-NEXT: (call $potent - ;; CHECK-NEXT: (f32.const 10) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: (@binaryen.idempotent) - ;; CHECK-NEXT: (call $potent - ;; CHECK-NEXT: (f32.const 10) - ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (f32.mul + ;; CHECK-NEXT: (@binaryen.idempotent) + ;; CHECK-NEXT: (call $potent + ;; CHECK-NEXT: (f32.const 10) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (@binaryen.idempotent) + ;; CHECK-NEXT: (call $potent + ;; CHECK-NEXT: (f32.const 10) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) From ef4d2f0017d90f90dc5be0741a1740256b23b8be Mon Sep 17 00:00:00 2001 From: Alon Zakai Date: Fri, 20 Feb 2026 09:36:35 -0800 Subject: [PATCH 26/29] format --- src/ir/properties.cpp | 4 +++- src/passes/OptimizeInstructions.cpp | 3 ++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/ir/properties.cpp b/src/ir/properties.cpp index f2d83da8e9d..2ca3158ff0e 100644 --- a/src/ir/properties.cpp +++ b/src/ir/properties.cpp @@ -28,7 +28,9 @@ struct GenerativityScanner : public PostWalker { void visitCall(Call* curr) { // If the called function is idempotent, then it does not generate new // values on each call. - if (Intrinsics(*getModule()).getCallAnnotations(curr, getFunction()).idempotent) { + if (Intrinsics(*getModule()) + .getCallAnnotations(curr, getFunction()) + .idempotent) { return; } // TODO: We could look at the called function's contents to see if it is diff --git a/src/passes/OptimizeInstructions.cpp b/src/passes/OptimizeInstructions.cpp index c45f3e5015e..fcc4cec6637 100644 --- a/src/passes/OptimizeInstructions.cpp +++ b/src/passes/OptimizeInstructions.cpp @@ -2891,7 +2891,8 @@ struct OptimizeInstructions if (childEffects.hasUnremovableSideEffects()) { return false; } - ShallowEffectAnalyzer parentEffects(getPassOptions(), *getModule(), call); + ShallowEffectAnalyzer parentEffects( + getPassOptions(), *getModule(), call); if (parentEffects.invalidates(childEffects)) { return false; } From fb1df2776ed55ff0584a49e21b4f5062bf1585df Mon Sep 17 00:00:00 2001 From: Alon Zakai Date: Fri, 20 Feb 2026 09:44:21 -0800 Subject: [PATCH 27/29] finish --- src/wasm.h | 5 +++-- test/lit/passes/optimize-instructions_idempotent.wast | 4 ++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/wasm.h b/src/wasm.h index b7e2fa2962a..bbc192cc6df 100644 --- a/src/wasm.h +++ b/src/wasm.h @@ -2259,8 +2259,9 @@ struct CodeAnnotation { bool jsCalled = false; // A function that may do something on the first call, but all subsequent - // calls can be assumed to have no effects. If a value is returned, it will be - // the same value as returned earlier. + // calls with the same parameters can be assumed to have no effects. If a + // value is returned, it will be the same value as returned earlier (for the + // same parameters). bool idempotent = false; bool operator==(const CodeAnnotation& other) const { diff --git a/test/lit/passes/optimize-instructions_idempotent.wast b/test/lit/passes/optimize-instructions_idempotent.wast index 5b5adf8103e..97fd1ea36d9 100644 --- a/test/lit/passes/optimize-instructions_idempotent.wast +++ b/test/lit/passes/optimize-instructions_idempotent.wast @@ -14,7 +14,7 @@ ;; CHECK-NEXT: ) (@binaryen.idempotent) (func $idempotent (param $x f32) (result f32) - ;; This function is idempotent: same inputs, same outputs. TODO: document that part + ;; This function is idempotent: same inputs, same outputs. (local.get $x) ) @@ -156,7 +156,7 @@ ;; CHECK-NEXT: ) (@binaryen.idempotent) (func $idempotent (param $x eqref) (result eqref) - ;; This function is idempotent: same inputs, same outputs. TODO: document that part + ;; This function is idempotent: same inputs, same outputs. (local.get $x) ) From 9f1fc5335803ff85d0e5a9ea5e9b4c5f496b0e47 Mon Sep 17 00:00:00 2001 From: Alon Zakai Date: Fri, 20 Feb 2026 10:44:44 -0800 Subject: [PATCH 28/29] simplify --- src/passes/OptimizeInstructions.cpp | 22 +------------------ .../lit/passes/optimize-instructions-mvp.wast | 20 ++++++++--------- 2 files changed, 10 insertions(+), 32 deletions(-) diff --git a/src/passes/OptimizeInstructions.cpp b/src/passes/OptimizeInstructions.cpp index fcc4cec6637..8af526852b1 100644 --- a/src/passes/OptimizeInstructions.cpp +++ b/src/passes/OptimizeInstructions.cpp @@ -2826,29 +2826,9 @@ struct OptimizeInstructions return !Properties::isGenerative(left, getFunction(), *getModule()); } - // Similar to areConsecutiveInputsEqual() but also checks if we can remove - // them (but we do not assume the caller will always remove them). - bool areConsecutiveInputsEqualAndRemovable(Expression* left, - Expression* right) { - // First, check for side effects. If there are any, then we can't even - // assume things like local.get's of the same index being identical. (It is - // also ok to have removable side effects here, see the function - // description.) We only check one side as we are going to check for - // equality below anyhow. - if (EffectAnalyzer(getPassOptions(), *getModule(), left) - .hasUnremovableSideEffects()) { - return false; - } - - return areConsecutiveInputsEqual(left, right); - } - // Check if two consecutive inputs to an instruction are equal and can also be // folded into the first of the two (but we do not assume the caller will - // always fold them). This is similar to areConsecutiveInputsEqualAndRemovable - // but also identifies reads from the same local variable when the first of - // them is a "tee" operation and the second is a get (in which case, it is - // fine to remove the get, but not the tee), and similar things. + // always fold them). // // The inputs here must be consecutive, but it is also ok to have code with no // side effects at all in the middle. For example, a Const in between is ok. diff --git a/test/lit/passes/optimize-instructions-mvp.wast b/test/lit/passes/optimize-instructions-mvp.wast index 61afa766927..8a4b729fa9c 100644 --- a/test/lit/passes/optimize-instructions-mvp.wast +++ b/test/lit/passes/optimize-instructions-mvp.wast @@ -16141,17 +16141,13 @@ ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) ;; CHECK-NEXT: (drop - ;; CHECK-NEXT: (select - ;; CHECK-NEXT: (block (result i32) - ;; CHECK-NEXT: (call $send-i32 - ;; CHECK-NEXT: (i32.const 1337) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: (local.tee $x - ;; CHECK-NEXT: (local.get $param) - ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (block (result i32) + ;; CHECK-NEXT: (call $send-i32 + ;; CHECK-NEXT: (i32.const 1337) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.tee $x + ;; CHECK-NEXT: (local.get $param) ;; CHECK-NEXT: ) - ;; CHECK-NEXT: (i32.const 0) - ;; CHECK-NEXT: (local.get $x) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) ;; CHECK-NEXT: (drop @@ -16179,7 +16175,9 @@ ) ) ) - ;; Side effect on the ifTrue - same outcome, we cannot optimize yet. + ;; Side effect on the ifTrue - we handle this case, as we can easily see + ;; that those effects are not a problem. We keep those effects around, of + ;; course. (drop (select (block (result i32) From 2f2d04336056eb2e8735a808ed3d6e58a78af431 Mon Sep 17 00:00:00 2001 From: Alon Zakai Date: Fri, 20 Feb 2026 16:04:51 -0800 Subject: [PATCH 29/29] handle a single annotation + testing --- src/passes/OptimizeInstructions.cpp | 8 +- .../optimize-instructions_idempotent.wast | 96 +++++++++++++++---- .../strip-toolchain-annotations-func.wast | 7 ++ .../passes/strip-toolchain-annotations.wast | 7 ++ 4 files changed, 97 insertions(+), 21 deletions(-) diff --git a/src/passes/OptimizeInstructions.cpp b/src/passes/OptimizeInstructions.cpp index 8af526852b1..28d5ae68153 100644 --- a/src/passes/OptimizeInstructions.cpp +++ b/src/passes/OptimizeInstructions.cpp @@ -2822,8 +2822,12 @@ struct OptimizeInstructions } // To be equal, they must also be known to return the same result - // deterministically. - return !Properties::isGenerative(left, getFunction(), *getModule()); + // deterministically. We check the right side, as if the right is marked + // idempotent, that is enough (that tells us it does not generate a new + // value; logically, of course, as left is equal to right, they are calling + // the same thing, so it is odd to only annotate one, but this is consistent + // and easy to check). + return !Properties::isGenerative(right, getFunction(), *getModule()); } // Check if two consecutive inputs to an instruction are equal and can also be diff --git a/test/lit/passes/optimize-instructions_idempotent.wast b/test/lit/passes/optimize-instructions_idempotent.wast index 97fd1ea36d9..d5ade65810c 100644 --- a/test/lit/passes/optimize-instructions_idempotent.wast +++ b/test/lit/passes/optimize-instructions_idempotent.wast @@ -5,7 +5,7 @@ ;; Idempotent-marked functions can be assumed to always return the same value. (module - ;; CHECK: (import "a" "b" (func $import (type $1) (result f32))) + ;; CHECK: (import "a" "b" (func $import (type $2) (result f32))) (import "a" "b" (func $import (result f32))) ;; CHECK: (@binaryen.idempotent) @@ -26,7 +26,7 @@ (call $import) ) - ;; CHECK: (func $test-abs (type $2) + ;; CHECK: (func $test-abs (type $1) ;; CHECK-NEXT: (drop ;; CHECK-NEXT: (f32.mul ;; CHECK-NEXT: (call $idempotent @@ -50,18 +50,6 @@ ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) ;; CHECK-NEXT: (drop - ;; CHECK-NEXT: (f32.mul - ;; CHECK-NEXT: (@binaryen.idempotent) - ;; CHECK-NEXT: (call $potent - ;; CHECK-NEXT: (f32.const 10) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: (@binaryen.idempotent) - ;; CHECK-NEXT: (call $potent - ;; CHECK-NEXT: (f32.const 10) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: (drop ;; CHECK-NEXT: (f32.abs ;; CHECK-NEXT: (f32.mul ;; CHECK-NEXT: (call $idempotent @@ -102,7 +90,62 @@ ) ) ) - ;; Here we succeed, as the calls are marked idempotent. + ;; Here we fail as well, as while we have idempotency, the params differ. + (drop + (f32.abs + (f32.mul + (call $idempotent + (f32.const 10) + ) + (call $idempotent + (f32.const 20) + ) + ) + ) + ) + ) + + ;; CHECK: (func $test-abs-calls (type $1) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (f32.mul + ;; CHECK-NEXT: (@binaryen.idempotent) + ;; CHECK-NEXT: (call $potent + ;; CHECK-NEXT: (f32.const 10) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (@binaryen.idempotent) + ;; CHECK-NEXT: (call $potent + ;; CHECK-NEXT: (f32.const 10) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (f32.abs + ;; CHECK-NEXT: (f32.mul + ;; CHECK-NEXT: (@binaryen.idempotent) + ;; CHECK-NEXT: (call $potent + ;; CHECK-NEXT: (f32.const 10) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (call $potent + ;; CHECK-NEXT: (f32.const 10) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (f32.mul + ;; CHECK-NEXT: (call $potent + ;; CHECK-NEXT: (f32.const 10) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (@binaryen.idempotent) + ;; CHECK-NEXT: (call $potent + ;; CHECK-NEXT: (f32.const 10) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $test-abs-calls + ;; Here we succeed, as the calls (as opposed to the functions) are marked + ;; idempotent. (drop (f32.abs (f32.mul @@ -117,15 +160,30 @@ ) ) ) - ;; Here we fail as well, as while we have idempotency, the params differ. + ;; Only one is marked. Marking the first is not enough for us to optimize. (drop (f32.abs (f32.mul - (call $idempotent + (@binaryen.idempotent) + (call $potent (f32.const 10) ) - (call $idempotent - (f32.const 20) + (call $potent + (f32.const 10) + ) + ) + ) + ) + ;; Marking the second *is* enough for us to optimize. + (drop + (f32.abs + (f32.mul + (call $potent + (f32.const 10) + ) + (@binaryen.idempotent) + (call $potent + (f32.const 10) ) ) ) diff --git a/test/lit/passes/strip-toolchain-annotations-func.wast b/test/lit/passes/strip-toolchain-annotations-func.wast index 80cf695273c..eb80273d70a 100644 --- a/test/lit/passes/strip-toolchain-annotations-func.wast +++ b/test/lit/passes/strip-toolchain-annotations-func.wast @@ -35,6 +35,13 @@ (func $test-func-d ;; Reverse order of above, and also includes js.called which is removed. ) + + ;; CHECK: (func $idempotent (type $0) + ;; CHECK-NEXT: ) + (@binaryen.idempotent) + (func $idempotent + ;; This hint should be removed too. + ) ) diff --git a/test/lit/passes/strip-toolchain-annotations.wast b/test/lit/passes/strip-toolchain-annotations.wast index 8e51cc993be..1c13cb9b3a9 100644 --- a/test/lit/passes/strip-toolchain-annotations.wast +++ b/test/lit/passes/strip-toolchain-annotations.wast @@ -19,6 +19,9 @@ ;; CHECK-NEXT: (call $test ;; CHECK-NEXT: (i32.const 3) ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (call $test + ;; CHECK-NEXT: (i32.const 4) + ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) (func $test (param i32) ;; Inlining hints are not removed, as they are for the VM too. @@ -37,5 +40,9 @@ (@metadata.code.inline "\00") (@binaryen.removable.if.unused) (call $test (i32.const 3)) + + ;; This should be removed too. + (@binaryen.idempotent) + (call $test (i32.const 4)) ) )