diff --git a/scripts/test/fuzzing.py b/scripts/test/fuzzing.py index 7f8897835ea..d0355f7d402 100644 --- a/scripts/test/fuzzing.py +++ b/scripts/test/fuzzing.py @@ -116,6 +116,8 @@ 'vacuum-removable-if-unused.wast', 'vacuum-removable-if-unused-func.wast', 'strip-toolchain-annotations-func.wast', + 'idempotent.wast', + 'optimize-instructions_idempotent.wast', # Not fully implemented. 'waitqueue.wast', ] diff --git a/src/ir/intrinsics.h b/src/ir/intrinsics.h index d2e99460685..10983a6cc5a 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); } @@ -165,6 +165,9 @@ class Intrinsics { if (!ret.jsCalled) { ret.jsCalled = funcAnnotations.jsCalled; } + if (!ret.idempotent) { + ret.idempotent = funcAnnotations.idempotent; + } } return ret; diff --git a/src/ir/properties.cpp b/src/ir/properties.cpp index 8bc8c1d8f6f..2ca3158ff0e 100644 --- a/src/ir/properties.cpp +++ b/src/ir/properties.cpp @@ -15,6 +15,7 @@ */ #include "ir/properties.h" +#include "ir/intrinsics.h" #include "wasm-traversal.h" namespace wasm::Properties { @@ -25,7 +26,14 @@ 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. + if (Intrinsics(*getModule()) + .getCallAnnotations(curr, getFunction()) + .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 +51,19 @@ struct GenerativityScanner : public PostWalker { } // anonymous namespace -bool isGenerative(Expression* curr) { +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) { +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 e763ccd9098..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); +bool isGenerative(Expression* curr, Function* func, Module& wasm); // As above, but only checks |curr| and not children. -bool isShallowlyGenerative(Expression* curr); +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/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/LocalCSE.cpp b/src/passes/LocalCSE.cpp index 9c9a8198f89..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)) { + if (Properties::isShallowlyGenerative(curr, getFunction(), *getModule())) { return false; } diff --git a/src/passes/OptimizeInstructions.cpp b/src/passes/OptimizeInstructions.cpp index f0e56c5f0f9..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); + // 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 @@ -2841,9 +2845,51 @@ struct OptimizeInstructions } // To fold the right side into the left, it must have no effects. - if (EffectAnalyzer(getPassOptions(), *getModule(), right) - .hasUnremovableSideEffects()) { - return false; + 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) { + // 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; + } + } + if (rightMightHaveEffects) { + // So far it looks like right has effects, so check fully. + if (EffectAnalyzer(getPassOptions(), *getModule(), right) + .hasUnremovableSideEffects()) { + return false; + } } return areConsecutiveInputsEqual(left, right); 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-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.h b/src/wasm.h index c35b1ea2531..bbc192cc6df 100644 --- a/src/wasm.h +++ b/src/wasm.h @@ -2258,10 +2258,16 @@ 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 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 { return branchLikely == other.branchLikely && inline_ == other.inline_ && removableIfUnused == other.removableIfUnused && - jsCalled == other.jsCalled; + jsCalled == other.jsCalled && idempotent == other.idempotent; } }; @@ -2322,6 +2328,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 diff --git a/src/wasm/wasm-binary.cpp b/src/wasm/wasm-binary.cpp index 6783b1eafbb..7b6e9f6ac56 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() { }); } +// 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, \ + [](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) { @@ -2070,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. @@ -2235,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)) { @@ -5549,29 +5555,27 @@ 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"); - } +// 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; \ + }); - annotation.removableIfUnused = 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::JSCalledHint, jsCalled); +} - annotation.jsCalled = true; - }); +void WasmBinaryReader::readIdempotentHints(size_t payloadLen) { + READ_BOOLEAN_HINT(Annotations::IdempotentHint, idempotent); } std::tuple 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 diff --git a/test/lit/passes/optimize-instructions_idempotent.wast b/test/lit/passes/optimize-instructions_idempotent.wast new file mode 100644 index 00000000000..d5ade65810c --- /dev/null +++ b/test/lit/passes/optimize-instructions_idempotent.wast @@ -0,0 +1,386 @@ +;; NOTE: Assertions have been generated by update_lit_checks.py and should not be edited. + +;; 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. + +(module + ;; CHECK: (import "a" "b" (func $import (type $2) (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: same inputs, same outputs. + (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 - anything might happen here. + (call $import) + ) + + ;; CHECK: (func $test-abs (type $1) + ;; 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 + (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) + ) + ) + ) + ) + ;; 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 + (@binaryen.idempotent) + (call $potent + (f32.const 10) + ) + (@binaryen.idempotent) + (call $potent + (f32.const 10) + ) + ) + ) + ) + ;; Only one is marked. Marking the first is not enough for us to optimize. + (drop + (f32.abs + (f32.mul + (@binaryen.idempotent) + (call $potent + (f32.const 10) + ) + (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) + ) + ) + ) + ) + ) +) + +;; References. +(module + ;; CHECK: (type $struct (struct)) + (type $struct (struct)) + + ;; 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)) + (global $g1 (ref $struct) (struct.new $struct)) + + ;; 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) + ;; CHECK-NEXT: ) + (@binaryen.idempotent) + (func $idempotent (param $x eqref) (result eqref) + ;; This function is idempotent: same inputs, same outputs. + (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 $2) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (block (result i32) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (call $idempotent + ;; CHECK-NEXT: (global.get $g1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; 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 + ;; 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: (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 + ;; 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) + ) + ) + ) + ;; 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) + ) + ) + ) + ) + + ;; 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) + ) +) 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)) ) )