diff options
41 files changed, 2764 insertions, 420 deletions
diff --git a/scripts/fuzz_opt.py b/scripts/fuzz_opt.py index 28426f773..e06e13b72 100755 --- a/scripts/fuzz_opt.py +++ b/scripts/fuzz_opt.py @@ -352,10 +352,6 @@ INITIAL_CONTENTS_IGNORE = [ 'typed_continuations_contnew.wast', 'typed_continuations_contbind.wast', 'typed_continuations_suspend.wast', - # New EH implementation is in progress - 'exception-handling.wast', - 'translate-to-new-eh.wast', - 'rse-eh.wast', ] @@ -841,7 +837,9 @@ class CompareVMs(TestCaseHandler): # V8 does not support shared memories when running with # shared-everything enabled, so do not fuzz shared-everything # for now. - return all_disallowed(['shared-everything']) + # Due to the V8 bug https://issues.chromium.org/issues/332931390 + # we do not fuzz exception-handling either. + return all_disallowed(['shared-everything', 'exception-handling']) def can_compare_to_self(self): # With nans, VM differences can confuse us, so only very simple VMs @@ -1649,6 +1647,9 @@ def get_random_opts(): print('avoiding --flatten due to multivalue + reference types not supporting it (spilling of non-nullable tuples)') print('TODO: Resolving https://github.com/WebAssembly/binaryen/issues/4824 may fix this') continue + if '--enable-exception-handling' in FEATURE_OPTS: + print('avoiding --flatten due to exception-handling not supporting it (requires blocks with results)') + continue if '--gc' not in FEATURE_OPTS: print('avoiding --flatten due to GC not supporting it (spilling of non-nullable locals)') continue @@ -1707,7 +1708,7 @@ print('FEATURE_DISABLE_FLAGS:', FEATURE_DISABLE_FLAGS) # some features depend on other features, so if a required feature is # disabled, its dependent features need to be disabled as well. IMPLIED_FEATURE_OPTS = { - '--disable-reference-types': ['--disable-gc', '--disable-strings'], + '--disable-reference-types': ['--disable-gc', '--disable-exception-handling', '--disable-strings'], '--disable-gc': ['--disable-strings'], } diff --git a/src/ir/ReFinalize.cpp b/src/ir/ReFinalize.cpp index 0f78d37b7..c32d6efbf 100644 --- a/src/ir/ReFinalize.cpp +++ b/src/ir/ReFinalize.cpp @@ -128,7 +128,12 @@ void ReFinalize::visitTableFill(TableFill* curr) { curr->finalize(); } void ReFinalize::visitTableCopy(TableCopy* curr) { curr->finalize(); } void ReFinalize::visitTableInit(TableInit* curr) { curr->finalize(); } void ReFinalize::visitTry(Try* curr) { curr->finalize(); } -void ReFinalize::visitTryTable(TryTable* curr) { curr->finalize(); } +void ReFinalize::visitTryTable(TryTable* curr) { + curr->finalize(); + for (size_t i = 0; i < curr->catchDests.size(); i++) { + updateBreakValueType(curr->catchDests[i], curr->sentTypes[i]); + } +} void ReFinalize::visitThrow(Throw* curr) { curr->finalize(); } void ReFinalize::visitRethrow(Rethrow* curr) { curr->finalize(); } void ReFinalize::visitThrowRef(ThrowRef* curr) { curr->finalize(); } diff --git a/src/ir/effects.h b/src/ir/effects.h index fee8b3441..716624d64 100644 --- a/src/ir/effects.h +++ b/src/ir/effects.h @@ -431,6 +431,14 @@ private: self->pushTask(doStartTry, currp); return; } + if (auto* tryTable = curr->dynCast<TryTable>()) { + // We need to increment try depth before starting. + self->pushTask(doEndTryTable, currp); + self->pushTask(doVisitTryTable, currp); + self->pushTask(scan, &tryTable->body); + self->pushTask(doStartTryTable, currp); + return; + } PostWalker<InternalAnalyzer, OverriddenVisitor<InternalAnalyzer>>::scan( self, currp); } @@ -472,6 +480,24 @@ private: self->parent.catchDepth--; } + static void doStartTryTable(InternalAnalyzer* self, Expression** currp) { + auto* curr = (*currp)->cast<TryTable>(); + // We only count 'try_table's with a 'catch_all' because instructions + // within a 'try_table' without a 'catch_all' can still throw outside of + // the try. + if (curr->hasCatchAll()) { + self->parent.tryDepth++; + } + } + + static void doEndTryTable(InternalAnalyzer* self, Expression** currp) { + auto* curr = (*currp)->cast<TryTable>(); + if (curr->hasCatchAll()) { + assert(self->parent.tryDepth > 0 && "try depth cannot be negative"); + self->parent.tryDepth--; + } + } + void visitBlock(Block* curr) { if (curr->name.is()) { parent.breakTargets.erase(curr->name); // these were internal breaks diff --git a/src/ir/linear-execution.h b/src/ir/linear-execution.h index c6593bd64..e8b1923aa 100644 --- a/src/ir/linear-execution.h +++ b/src/ir/linear-execution.h @@ -171,6 +171,12 @@ struct LinearExecutionWalker : public PostWalker<SubType, VisitorType> { self->pushTask(SubType::scan, &curr->cast<Try>()->body); break; } + case Expression::Id::TryTableId: { + self->pushTask(SubType::doVisitTryTable, currp); + self->pushTask(SubType::doNoteNonLinear, currp); + self->pushTask(SubType::scan, &curr->cast<TryTable>()->body); + break; + } case Expression::Id::ThrowId: { self->pushTask(SubType::doVisitThrow, currp); self->pushTask(SubType::doNoteNonLinear, currp); diff --git a/src/ir/possible-contents.cpp b/src/ir/possible-contents.cpp index e7454c7c6..e5e6cf659 100644 --- a/src/ir/possible-contents.cpp +++ b/src/ir/possible-contents.cpp @@ -1135,8 +1135,38 @@ struct InfoCollector } } void visitTryTable(TryTable* curr) { - // TODO: optimize when possible - addRoot(curr); + receiveChildValue(curr->body, curr); + + // Connect caught tags with their branch targets, and materialize non-null + // exnref values. + auto numTags = curr->catchTags.size(); + for (Index tagIndex = 0; tagIndex < numTags; tagIndex++) { + auto tag = curr->catchTags[tagIndex]; + auto target = curr->catchDests[tagIndex]; + + Index exnrefIndex = 0; + if (tag.is()) { + auto params = getModule()->getTag(tag)->sig.params; + + for (Index i = 0; i < params.size(); i++) { + if (isRelevant(params[i])) { + info.links.push_back( + {TagLocation{tag, i}, + BreakTargetLocation{getFunction(), target, i}}); + } + } + + exnrefIndex = params.size(); + } + + if (curr->catchRefs[tagIndex]) { + auto location = CaughtExnRefLocation{}; + addRoot(location, + PossibleContents::fromType(Type(HeapType::exn, NonNullable))); + info.links.push_back( + {location, BreakTargetLocation{getFunction(), target, exnrefIndex}}); + } + } } void visitThrow(Throw* curr) { auto& operands = curr->operands; diff --git a/src/ir/possible-contents.h b/src/ir/possible-contents.h index 5ec4f758f..7b88483cf 100644 --- a/src/ir/possible-contents.h +++ b/src/ir/possible-contents.h @@ -473,6 +473,15 @@ struct TagLocation { } }; +// The location of an exnref materialized by a catch_ref or catch_all_ref clause +// of a try_table. No data is stored here. exnrefs contain a tag and a payload +// at run-time, as well as potential metadata such as stack traces, but we don't +// track that. So this is the same as NullLocation in a way: we just need *a* +// source of contents for places that receive an exnref. +struct CaughtExnRefLocation { + bool operator==(const CaughtExnRefLocation& other) const { return true; } +}; + // A null value. This is used as the location of the default value of a var in a // function, a null written to a struct field in struct.new_with_default, etc. struct NullLocation { @@ -520,6 +529,7 @@ using Location = std::variant<ExpressionLocation, SignatureResultLocation, DataLocation, TagLocation, + CaughtExnRefLocation, NullLocation, ConeReadLocation>; @@ -608,6 +618,12 @@ template<> struct hash<wasm::TagLocation> { } }; +template<> struct hash<wasm::CaughtExnRefLocation> { + size_t operator()(const wasm::CaughtExnRefLocation& loc) const { + return std::hash<const char*>()("caught-exnref-location"); + } +}; + template<> struct hash<wasm::NullLocation> { size_t operator()(const wasm::NullLocation& loc) const { return std::hash<wasm::Type>{}(loc.type); diff --git a/src/literal.h b/src/literal.h index d247b0c84..dd6247d00 100644 --- a/src/literal.h +++ b/src/literal.h @@ -32,6 +32,7 @@ namespace wasm { class Literals; struct GCData; +struct ExnData; class Literal { // store only integers, whose bits are deterministic. floats @@ -44,6 +45,7 @@ class Literal { int64_t i64; uint8_t v128[16]; // funcref function name. `isNull()` indicates a `null` value. + // TODO: handle cross-module calls using something other than a Name here. Name func; // A reference to GC data, either a Struct or an Array. For both of those we // store the referred data as a Literals object (which is natural for an @@ -56,6 +58,8 @@ class Literal { // reference as its sole value even though internal i31 references do not // have a gcData. std::shared_ptr<GCData> gcData; + // A reference to Exn data. + std::shared_ptr<ExnData> exnData; }; public: @@ -85,6 +89,7 @@ public: assert(type.isSignature()); } explicit Literal(std::shared_ptr<GCData> gcData, HeapType type); + explicit Literal(std::shared_ptr<ExnData> exnData); explicit Literal(std::string_view string); Literal(const Literal& other); Literal& operator=(const Literal& other); @@ -96,6 +101,7 @@ public: // Whether this is GC data, that is, something stored on the heap (aside from // a null or i31). This includes structs, arrays, and also strings. bool isData() const { return type.isData(); } + bool isExn() const { return type.isExn(); } bool isString() const { return type.isString(); } bool isNull() const { return type.isNull(); } @@ -303,6 +309,7 @@ public: return func; } std::shared_ptr<GCData> getGCData() const; + std::shared_ptr<ExnData> getExnData() const; // careful! int32_t* geti32Ptr() { @@ -742,6 +749,18 @@ struct GCData { GCData(HeapType type, Literals values) : type(type), values(values) {} }; +// The data of a (ref exn) literal. +struct ExnData { + // The tag of this exn data. + // TODO: handle cross-module calls using something other than a Name here. + Name tag; + + // The payload of this exn data. + Literals payload; + + ExnData(Name tag, Literals payload) : tag(tag), payload(payload) {} +}; + } // namespace wasm namespace std { diff --git a/src/passes/CodeFolding.cpp b/src/passes/CodeFolding.cpp index dc2301f9c..2d17f6d31 100644 --- a/src/passes/CodeFolding.cpp +++ b/src/passes/CodeFolding.cpp @@ -343,15 +343,18 @@ private: if (effects.danglingPop) { return false; } - // When an expression can throw and it is within a try scope, taking it - // out of the try scope changes the program's behavior, because the - // expression that would otherwise have been caught by the try now - // throws up to the next try scope or even up to the caller. We restrict - // the move if 'outOf' contains a 'try' anywhere in it. This is a - // conservative approximation because there can be cases that 'try' is - // within the expression that may throw so it is safe to take the - // expression out. - if (effects.throws() && !FindAll<Try>(outOf).list.empty()) { + // When an expression can throw and it is within a try/try_table scope, + // taking it out of the try/try_table scope changes the program's + // behavior, because the expression that would otherwise have been + // caught by the try/try_table now throws up to the next try/try_table + // scope or even up to the caller. We restrict the move if 'outOf' + // contains a 'try' or 'try_table' anywhere in it. This is a + // conservative approximation because there can be cases that + // 'try'/'try_table' is within the expression that may throw so it is + // safe to take the expression out. + // TODO: optimize this check to avoid two FindAlls. + if (effects.throws() && + (FindAll<Try>(outOf).has() || FindAll<TryTable>(outOf).has())) { return false; } } diff --git a/src/passes/DeadCodeElimination.cpp b/src/passes/DeadCodeElimination.cpp index 996133ad5..f8acde7b0 100644 --- a/src/passes/DeadCodeElimination.cpp +++ b/src/passes/DeadCodeElimination.cpp @@ -185,6 +185,12 @@ struct DeadCodeElimination tryy->body->type == Type::unreachable && allCatchesUnreachable) { typeUpdater.changeType(tryy, Type::unreachable); } + } else if (auto* tryTable = curr->dynCast<TryTable>()) { + // try_table can finish normally only if its body finishes normally. + if (tryTable->type != Type::unreachable && + tryTable->body->type == Type::unreachable) { + typeUpdater.changeType(tryTable, Type::unreachable); + } } else { WASM_UNREACHABLE("unimplemented DCE control flow structure"); } diff --git a/src/passes/SimplifyLocals.cpp b/src/passes/SimplifyLocals.cpp index 28b13980b..eca503ad4 100644 --- a/src/passes/SimplifyLocals.cpp +++ b/src/passes/SimplifyLocals.cpp @@ -321,9 +321,9 @@ struct SimplifyLocals Expression** currp) { Expression* curr = *currp; - // Certain expressions cannot be sinked into 'try', and so at the start of - // 'try' we forget about them. - if (curr->is<Try>()) { + // Certain expressions cannot be sinked into 'try'/'try_table', and so at + // the start of 'try'/'try_table' we forget about them. + if (curr->is<Try>() || curr->is<TryTable>()) { std::vector<Index> invalidated; for (auto& [index, info] : self->sinkables) { // Expressions that may throw cannot be moved into a try (which might diff --git a/src/passes/Vacuum.cpp b/src/passes/Vacuum.cpp index 4a4963291..0e49f19c8 100644 --- a/src/passes/Vacuum.cpp +++ b/src/passes/Vacuum.cpp @@ -87,7 +87,7 @@ struct Vacuum : public WalkerPass<ExpressionStackWalker<Vacuum>> { // Some instructions have special handling in visit*, and we should do // nothing for them here. if (curr->is<Drop>() || curr->is<Block>() || curr->is<If>() || - curr->is<Loop>() || curr->is<Try>()) { + curr->is<Loop>() || curr->is<Try>() || curr->is<TryTable>()) { return curr; } // Check if this expression itself has side effects, ignoring children. @@ -435,6 +435,15 @@ struct Vacuum : public WalkerPass<ExpressionStackWalker<Vacuum>> { } } + void visitTryTable(TryTable* curr) { + // If try_table's body does not throw, the whole try_table can be replaced + // with the try_table's body. + if (!EffectAnalyzer(getPassOptions(), *getModule(), curr->body).throws()) { + replaceCurrent(curr->body); + return; + } + } + void visitFunction(Function* curr) { auto* optimized = optimize(curr->body, curr->getResults() != Type::none, true); diff --git a/src/wasm-interpreter.h b/src/wasm-interpreter.h index 644a141a2..1bf135c40 100644 --- a/src/wasm-interpreter.h +++ b/src/wasm-interpreter.h @@ -47,8 +47,7 @@ namespace wasm { struct WasmException { - Name tag; - Literals values; + Literal exn; }; std::ostream& operator<<(std::ostream& o, const WasmException& exn); @@ -204,6 +203,15 @@ protected: return Literal(allocation, type.getHeapType()); } + // Same as makeGCData but for ExnData. + Literal makeExnData(Name tag, const Literals& payload) { + auto allocation = std::make_shared<ExnData>(tag, payload); +#if __has_feature(leak_sanitizer) || __has_feature(address_sanitizer) + __lsan_ignore_object(allocation.get()); +#endif + return Literal(allocation); + } + public: // Indicates no limit of maxDepth or maxLoopIterations. static const Index NO_LIMIT = 0; @@ -1428,16 +1436,25 @@ public: return flow; } NOTE_EVAL1(curr->tag); - WasmException exn; - exn.tag = curr->tag; - for (auto item : arguments) { - exn.values.push_back(item); - } - throwException(exn); + throwException(WasmException{makeExnData(curr->tag, arguments)}); WASM_UNREACHABLE("throw"); } Flow visitRethrow(Rethrow* curr) { WASM_UNREACHABLE("unimp"); } - Flow visitThrowRef(ThrowRef* curr) { WASM_UNREACHABLE("unimp"); } + Flow visitThrowRef(ThrowRef* curr) { + NOTE_ENTER("ThrowRef"); + Flow flow = visit(curr->exnref); + if (flow.breaking()) { + return flow; + } + const auto& exnref = flow.getSingleValue(); + NOTE_EVAL1(exnref); + if (exnref.isNull()) { + trap("null ref"); + } + assert(exnref.isExn()); + throwException(WasmException{exnref}); + WASM_UNREACHABLE("throw"); + } Flow visitRefI31(RefI31* curr) { NOTE_ENTER("RefI31"); Flow flow = visit(curr->value); @@ -2455,6 +2472,10 @@ public: NOTE_ENTER("Try"); return Flow(NONCONSTANT_FLOW); } + Flow visitTryTable(TryTable* curr) { + NOTE_ENTER("TryTable"); + return Flow(NONCONSTANT_FLOW); + } Flow visitRethrow(Rethrow* curr) { NOTE_ENTER("Rethrow"); return Flow(NONCONSTANT_FLOW); @@ -4103,9 +4124,10 @@ public: return ret; }; + auto exnData = e.exn.getExnData(); for (size_t i = 0; i < curr->catchTags.size(); i++) { - if (curr->catchTags[i] == e.tag) { - multiValues.push_back(e.values); + if (curr->catchTags[i] == exnData->tag) { + multiValues.push_back(exnData->payload); return processCatchBody(curr->catchBodies[i]); } } @@ -4119,6 +4141,32 @@ public: throw; } } + Flow visitTryTable(TryTable* curr) { + NOTE_ENTER("TryTable"); + try { + return self()->visit(curr->body); + } catch (const WasmException& e) { + auto exnData = e.exn.getExnData(); + for (size_t i = 0; i < curr->catchTags.size(); i++) { + auto catchTag = curr->catchTags[i]; + if (!catchTag.is() || catchTag == exnData->tag) { + Flow ret; + ret.breakTo = curr->catchDests[i]; + if (catchTag.is()) { + for (auto item : exnData->payload) { + ret.values.push_back(item); + } + } + if (curr->catchRefs[i]) { + ret.values.push_back(e.exn); + } + return ret; + } + } + // This exception is not caught by this try_table. Rethrow it. + throw; + } + } Flow visitRethrow(Rethrow* curr) { for (int i = exceptionStack.size() - 1; i >= 0; i--) { if (exceptionStack[i].second == curr->target) { diff --git a/src/wasm-type.h b/src/wasm-type.h index ff1358a12..03c4f8e77 100644 --- a/src/wasm-type.h +++ b/src/wasm-type.h @@ -170,6 +170,7 @@ public: bool isSignature() const; bool isStruct() const; bool isArray() const; + bool isExn() const; bool isString() const; bool isDefaultable() const; @@ -388,6 +389,7 @@ public: bool isContinuation() const { return getKind() == HeapTypeKind::Cont; } bool isStruct() const { return getKind() == HeapTypeKind::Struct; } bool isArray() const { return getKind() == HeapTypeKind::Array; } + bool isExn() const { return isMaybeShared(HeapType::exn); } bool isString() const { return isMaybeShared(HeapType::string); } bool isBottom() const; bool isOpen() const; diff --git a/src/wasm/literal.cpp b/src/wasm/literal.cpp index 9d659f753..650318be3 100644 --- a/src/wasm/literal.cpp +++ b/src/wasm/literal.cpp @@ -80,6 +80,12 @@ Literal::Literal(std::shared_ptr<GCData> gcData, HeapType type) (type.isBottom() && !gcData)); } +Literal::Literal(std::shared_ptr<ExnData> exnData) + : exnData(exnData), type(HeapType::exn, NonNullable) { + // The data must not be null. + assert(exnData); +} + Literal::Literal(std::string_view string) : gcData(nullptr), type(Type(HeapType::string, NonNullable)) { // TODO: we could in theory internalize strings @@ -133,6 +139,9 @@ Literal::Literal(const Literal& other) : type(other.type) { case HeapType::i31: i32 = other.i32; return; + case HeapType::exn: + new (&exnData) std::shared_ptr<ExnData>(other.exnData); + return; case HeapType::ext: WASM_UNREACHABLE("handled above with isData()"); case HeapType::none: @@ -147,7 +156,6 @@ Literal::Literal(const Literal& other) : type(other.type) { case HeapType::cont: case HeapType::struct_: case HeapType::array: - case HeapType::exn: WASM_UNREACHABLE("invalid type"); case HeapType::string: WASM_UNREACHABLE("TODO: string literals"); @@ -161,6 +169,8 @@ Literal::~Literal() { } if (isNull() || isData() || type.getHeapType().isMaybeShared(HeapType::ext)) { gcData.~shared_ptr(); + } else if (isExn()) { + exnData.~shared_ptr(); } } @@ -328,6 +338,12 @@ std::shared_ptr<GCData> Literal::getGCData() const { return gcData; } +std::shared_ptr<ExnData> Literal::getExnData() const { + assert(isExn()); + assert(exnData); + return exnData; +} + Literal Literal::castToF32() { assert(type == Type::i32); Literal ret(Type::f32); diff --git a/src/wasm/wasm-interpreter.cpp b/src/wasm/wasm-interpreter.cpp index b49eeef4b..cd6c232b5 100644 --- a/src/wasm/wasm-interpreter.cpp +++ b/src/wasm/wasm-interpreter.cpp @@ -20,7 +20,8 @@ void Indenter::print() { #endif // WASM_INTERPRETER_DEBUG std::ostream& operator<<(std::ostream& o, const WasmException& exn) { - return o << exn.tag << " " << exn.values; + auto exnData = exn.exn.getExnData(); + return o << exnData->tag << " " << exnData->payload; } } // namespace wasm diff --git a/src/wasm/wasm-type.cpp b/src/wasm/wasm-type.cpp index 84ae51521..1730f1b85 100644 --- a/src/wasm/wasm-type.cpp +++ b/src/wasm/wasm-type.cpp @@ -814,6 +814,8 @@ bool Type::isStruct() const { return isRef() && getHeapType().isStruct(); } bool Type::isArray() const { return isRef() && getHeapType().isArray(); } +bool Type::isExn() const { return isRef() && getHeapType().isExn(); } + bool Type::isString() const { return isRef() && getHeapType().isString(); } bool Type::isDefaultable() const { diff --git a/test/lit/basic/reference-types.wast b/test/lit/basic/reference-types.wast index 7f9ba4219..335939573 100644 --- a/test/lit/basic/reference-types.wast +++ b/test/lit/basic/reference-types.wast @@ -642,6 +642,62 @@ ;; CHECK-TEXT-NEXT: ) ;; CHECK-TEXT-NEXT: ) ;; CHECK-TEXT-NEXT: (drop + ;; CHECK-TEXT-NEXT: (block $tryend (result eqref) + ;; CHECK-TEXT-NEXT: (drop + ;; CHECK-TEXT-NEXT: (block $catch (result i32) + ;; CHECK-TEXT-NEXT: (br $tryend + ;; CHECK-TEXT-NEXT: (try_table (result eqref) (catch $e-i32 $catch) + ;; CHECK-TEXT-NEXT: (local.get $local_eqref) + ;; CHECK-TEXT-NEXT: ) + ;; CHECK-TEXT-NEXT: ) + ;; CHECK-TEXT-NEXT: ) + ;; CHECK-TEXT-NEXT: ) + ;; CHECK-TEXT-NEXT: (ref.null none) + ;; CHECK-TEXT-NEXT: ) + ;; CHECK-TEXT-NEXT: ) + ;; CHECK-TEXT-NEXT: (drop + ;; CHECK-TEXT-NEXT: (block $tryend0 (result funcref) + ;; CHECK-TEXT-NEXT: (drop + ;; CHECK-TEXT-NEXT: (block $catch0 (result i32) + ;; CHECK-TEXT-NEXT: (br $tryend0 + ;; CHECK-TEXT-NEXT: (try_table (result funcref) (catch $e-i32 $catch0) + ;; CHECK-TEXT-NEXT: (ref.func $foo) + ;; CHECK-TEXT-NEXT: ) + ;; CHECK-TEXT-NEXT: ) + ;; CHECK-TEXT-NEXT: ) + ;; CHECK-TEXT-NEXT: ) + ;; CHECK-TEXT-NEXT: (ref.null nofunc) + ;; CHECK-TEXT-NEXT: ) + ;; CHECK-TEXT-NEXT: ) + ;; CHECK-TEXT-NEXT: (drop + ;; CHECK-TEXT-NEXT: (block $tryend1 (result anyref) + ;; CHECK-TEXT-NEXT: (drop + ;; CHECK-TEXT-NEXT: (block $catch1 (result i32) + ;; CHECK-TEXT-NEXT: (br $tryend1 + ;; CHECK-TEXT-NEXT: (try_table (result anyref) (catch $e-i32 $catch1) + ;; CHECK-TEXT-NEXT: (local.get $local_eqref) + ;; CHECK-TEXT-NEXT: ) + ;; CHECK-TEXT-NEXT: ) + ;; CHECK-TEXT-NEXT: ) + ;; CHECK-TEXT-NEXT: ) + ;; CHECK-TEXT-NEXT: (ref.null none) + ;; CHECK-TEXT-NEXT: ) + ;; CHECK-TEXT-NEXT: ) + ;; CHECK-TEXT-NEXT: (drop + ;; CHECK-TEXT-NEXT: (block $tryend2 (result anyref) + ;; CHECK-TEXT-NEXT: (drop + ;; CHECK-TEXT-NEXT: (block $catch2 (result i32) + ;; CHECK-TEXT-NEXT: (br $tryend2 + ;; CHECK-TEXT-NEXT: (try_table (result anyref) (catch $e-i32 $catch2) + ;; CHECK-TEXT-NEXT: (ref.null none) + ;; CHECK-TEXT-NEXT: ) + ;; CHECK-TEXT-NEXT: ) + ;; CHECK-TEXT-NEXT: ) + ;; CHECK-TEXT-NEXT: ) + ;; CHECK-TEXT-NEXT: (local.get $local_eqref) + ;; CHECK-TEXT-NEXT: ) + ;; CHECK-TEXT-NEXT: ) + ;; CHECK-TEXT-NEXT: (drop ;; CHECK-TEXT-NEXT: (select (result eqref) ;; CHECK-TEXT-NEXT: (local.get $local_eqref) ;; CHECK-TEXT-NEXT: (ref.null none) @@ -1189,6 +1245,62 @@ ;; CHECK-BIN-NEXT: ) ;; CHECK-BIN-NEXT: ) ;; CHECK-BIN-NEXT: (drop + ;; CHECK-BIN-NEXT: (block $label$50 (result eqref) + ;; CHECK-BIN-NEXT: (drop + ;; CHECK-BIN-NEXT: (block $label$51 (result i32) + ;; CHECK-BIN-NEXT: (br $label$50 + ;; CHECK-BIN-NEXT: (try_table (result eqref) (catch $e-i32 $label$51) + ;; CHECK-BIN-NEXT: (local.get $local_eqref) + ;; CHECK-BIN-NEXT: ) + ;; CHECK-BIN-NEXT: ) + ;; CHECK-BIN-NEXT: ) + ;; CHECK-BIN-NEXT: ) + ;; CHECK-BIN-NEXT: (ref.null none) + ;; CHECK-BIN-NEXT: ) + ;; CHECK-BIN-NEXT: ) + ;; CHECK-BIN-NEXT: (drop + ;; CHECK-BIN-NEXT: (block $label$53 (result funcref) + ;; CHECK-BIN-NEXT: (drop + ;; CHECK-BIN-NEXT: (block $label$54 (result i32) + ;; CHECK-BIN-NEXT: (br $label$53 + ;; CHECK-BIN-NEXT: (try_table (result funcref) (catch $e-i32 $label$54) + ;; CHECK-BIN-NEXT: (ref.func $foo) + ;; CHECK-BIN-NEXT: ) + ;; CHECK-BIN-NEXT: ) + ;; CHECK-BIN-NEXT: ) + ;; CHECK-BIN-NEXT: ) + ;; CHECK-BIN-NEXT: (ref.null nofunc) + ;; CHECK-BIN-NEXT: ) + ;; CHECK-BIN-NEXT: ) + ;; CHECK-BIN-NEXT: (drop + ;; CHECK-BIN-NEXT: (block $label$56 (result anyref) + ;; CHECK-BIN-NEXT: (drop + ;; CHECK-BIN-NEXT: (block $label$57 (result i32) + ;; CHECK-BIN-NEXT: (br $label$56 + ;; CHECK-BIN-NEXT: (try_table (result anyref) (catch $e-i32 $label$57) + ;; CHECK-BIN-NEXT: (local.get $local_eqref) + ;; CHECK-BIN-NEXT: ) + ;; CHECK-BIN-NEXT: ) + ;; CHECK-BIN-NEXT: ) + ;; CHECK-BIN-NEXT: ) + ;; CHECK-BIN-NEXT: (ref.null none) + ;; CHECK-BIN-NEXT: ) + ;; CHECK-BIN-NEXT: ) + ;; CHECK-BIN-NEXT: (drop + ;; CHECK-BIN-NEXT: (block $label$59 (result anyref) + ;; CHECK-BIN-NEXT: (drop + ;; CHECK-BIN-NEXT: (block $label$60 (result i32) + ;; CHECK-BIN-NEXT: (br $label$59 + ;; CHECK-BIN-NEXT: (try_table (result anyref) (catch $e-i32 $label$60) + ;; CHECK-BIN-NEXT: (ref.null none) + ;; CHECK-BIN-NEXT: ) + ;; CHECK-BIN-NEXT: ) + ;; CHECK-BIN-NEXT: ) + ;; CHECK-BIN-NEXT: ) + ;; CHECK-BIN-NEXT: (local.get $local_eqref) + ;; CHECK-BIN-NEXT: ) + ;; CHECK-BIN-NEXT: ) + ;; CHECK-BIN-NEXT: (drop ;; CHECK-BIN-NEXT: (select (result eqref) ;; CHECK-BIN-NEXT: (local.get $local_eqref) ;; CHECK-BIN-NEXT: (ref.null none) @@ -1597,6 +1709,66 @@ ) ) + ;; Test try_table return type + (drop + (block $tryend (result eqref) + (drop + (block $catch (result i32) + (br $tryend + (try_table (result eqref) (catch $e-i32 $catch) + (local.get $local_eqref) + ) + ) + ) + ) + (ref.null eq) + ) + ) + (drop + (block $tryend (result funcref) + (drop + (block $catch (result i32) + (br $tryend + (try_table (result funcref) (catch $e-i32 $catch) + (ref.func $foo) + ) + ) + ) + ) + (ref.null func) + ) + ) + + ;; Test subtype relationship for try_table return type + (drop + (block $tryend (result anyref) + (drop + (block $catch (result i32) + (br $tryend + (try_table (result anyref) (catch $e-i32 $catch) + (local.get $local_eqref) + ) + ) + ) + ) + (ref.null any) + ) + ) + (drop + (block $tryend (result anyref) + (drop + (block $catch (result i32) + (br $tryend + (try_table (result anyref) (catch $e-i32 $catch) + (ref.null eq) + ) + ) + ) + ) + (local.get $local_eqref) + ) + ) + ;; Test typed select (drop (select (result eqref) @@ -2416,6 +2588,62 @@ ;; CHECK-BIN-NODEBUG-NEXT: ) ;; CHECK-BIN-NODEBUG-NEXT: ) ;; CHECK-BIN-NODEBUG-NEXT: (drop +;; CHECK-BIN-NODEBUG-NEXT: (block $label$50 (result eqref) +;; CHECK-BIN-NODEBUG-NEXT: (drop +;; CHECK-BIN-NODEBUG-NEXT: (block $label$51 (result i32) +;; CHECK-BIN-NODEBUG-NEXT: (br $label$50 +;; CHECK-BIN-NODEBUG-NEXT: (try_table (result eqref) (catch $tag$0 $label$51) +;; CHECK-BIN-NODEBUG-NEXT: (local.get $0) +;; CHECK-BIN-NODEBUG-NEXT: ) +;; CHECK-BIN-NODEBUG-NEXT: ) +;; CHECK-BIN-NODEBUG-NEXT: ) +;; CHECK-BIN-NODEBUG-NEXT: ) +;; CHECK-BIN-NODEBUG-NEXT: (ref.null none) +;; CHECK-BIN-NODEBUG-NEXT: ) +;; CHECK-BIN-NODEBUG-NEXT: ) +;; CHECK-BIN-NODEBUG-NEXT: (drop +;; CHECK-BIN-NODEBUG-NEXT: (block $label$53 (result funcref) +;; CHECK-BIN-NODEBUG-NEXT: (drop +;; CHECK-BIN-NODEBUG-NEXT: (block $label$54 (result i32) +;; CHECK-BIN-NODEBUG-NEXT: (br $label$53 +;; CHECK-BIN-NODEBUG-NEXT: (try_table (result funcref) (catch $tag$0 $label$54) +;; CHECK-BIN-NODEBUG-NEXT: (ref.func $3) +;; CHECK-BIN-NODEBUG-NEXT: ) +;; CHECK-BIN-NODEBUG-NEXT: ) +;; CHECK-BIN-NODEBUG-NEXT: ) +;; CHECK-BIN-NODEBUG-NEXT: ) +;; CHECK-BIN-NODEBUG-NEXT: (ref.null nofunc) +;; CHECK-BIN-NODEBUG-NEXT: ) +;; CHECK-BIN-NODEBUG-NEXT: ) +;; CHECK-BIN-NODEBUG-NEXT: (drop +;; CHECK-BIN-NODEBUG-NEXT: (block $label$56 (result anyref) +;; CHECK-BIN-NODEBUG-NEXT: (drop +;; CHECK-BIN-NODEBUG-NEXT: (block $label$57 (result i32) +;; CHECK-BIN-NODEBUG-NEXT: (br $label$56 +;; CHECK-BIN-NODEBUG-NEXT: (try_table (result anyref) (catch $tag$0 $label$57) +;; CHECK-BIN-NODEBUG-NEXT: (local.get $0) +;; CHECK-BIN-NODEBUG-NEXT: ) +;; CHECK-BIN-NODEBUG-NEXT: ) +;; CHECK-BIN-NODEBUG-NEXT: ) +;; CHECK-BIN-NODEBUG-NEXT: ) +;; CHECK-BIN-NODEBUG-NEXT: (ref.null none) +;; CHECK-BIN-NODEBUG-NEXT: ) +;; CHECK-BIN-NODEBUG-NEXT: ) +;; CHECK-BIN-NODEBUG-NEXT: (drop +;; CHECK-BIN-NODEBUG-NEXT: (block $label$59 (result anyref) +;; CHECK-BIN-NODEBUG-NEXT: (drop +;; CHECK-BIN-NODEBUG-NEXT: (block $label$60 (result i32) +;; CHECK-BIN-NODEBUG-NEXT: (br $label$59 +;; CHECK-BIN-NODEBUG-NEXT: (try_table (result anyref) (catch $tag$0 $label$60) +;; CHECK-BIN-NODEBUG-NEXT: (ref.null none) +;; CHECK-BIN-NODEBUG-NEXT: ) +;; CHECK-BIN-NODEBUG-NEXT: ) +;; CHECK-BIN-NODEBUG-NEXT: ) +;; CHECK-BIN-NODEBUG-NEXT: ) +;; CHECK-BIN-NODEBUG-NEXT: (local.get $0) +;; CHECK-BIN-NODEBUG-NEXT: ) +;; CHECK-BIN-NODEBUG-NEXT: ) +;; CHECK-BIN-NODEBUG-NEXT: (drop ;; CHECK-BIN-NODEBUG-NEXT: (select (result eqref) ;; CHECK-BIN-NODEBUG-NEXT: (local.get $0) ;; CHECK-BIN-NODEBUG-NEXT: (ref.null none) diff --git a/test/lit/exec/eh-gc.wast b/test/lit/exec/eh-gc.wast new file mode 100644 index 000000000..6f97f44f8 --- /dev/null +++ b/test/lit/exec/eh-gc.wast @@ -0,0 +1,27 @@ +;; NOTE: Assertions have been generated by update_lit_checks.py --output=fuzz-exec and should not be edited. + +;; RUN: wasm-opt %s -all --fuzz-exec -q -o /dev/null 2>&1 | filecheck %s + +(module + (tag $tag (param externref)) + + ;; CHECK: [fuzz-exec] calling catch-null + (func $catch-null (export "catch-null") + (block $tryend + ;; The actual resulting value type is more refined than externref (it is a + ;; bottom type) which we should not error on. + (drop + (block $catch (result externref) + (try_table (catch $tag $catch) + (throw $tag + (ref.null noextern) + ) + ) + (br $tryend) + ) + ) + ) + ) +) +;; CHECK: [fuzz-exec] calling catch-null +;; CHECK-NEXT: [fuzz-exec] comparing catch-null diff --git a/test/lit/exec/eh-legacy-print.wast b/test/lit/exec/eh-print.wast index f50164631..f50164631 100644 --- a/test/lit/exec/eh-legacy-print.wast +++ b/test/lit/exec/eh-print.wast diff --git a/test/lit/exec/eh.wast b/test/lit/exec/eh.wast new file mode 100644 index 000000000..45215b048 --- /dev/null +++ b/test/lit/exec/eh.wast @@ -0,0 +1,45 @@ +;; NOTE: Assertions have been generated by update_lit_checks.py --output=fuzz-exec and should not be edited. + +;; RUN: wasm-opt %s -all --fuzz-exec -q -o /dev/null 2>&1 | filecheck %s + +(module + (tag $e-i32 (param i32)) + + ;; CHECK: [fuzz-exec] calling throw + ;; CHECK-NEXT: [exception thrown: e-i32 1] + (func $throw (export "throw") + (throw $e-i32 (i32.const 1)) + ) + + ;; CHECK: [fuzz-exec] calling try_table-catch + (func $try_table-catch (export "try_table-catch") + (block $tryend + (drop + (block $catch (result i32) + (try_table (catch $e-i32 $catch) + (throw $e-i32 (i32.const 2)) + ) + (br $tryend) + ) + ) + ) + ) + + ;; CHECK: [fuzz-exec] calling catchless-try_table + ;; CHECK-NEXT: [exception thrown: e-i32 3] + (func $catchless-try_table (export "catchless-try_table") + (try_table + (throw $e-i32 (i32.const 3)) + ) + ) +) +;; CHECK: [fuzz-exec] calling throw +;; CHECK-NEXT: [exception thrown: e-i32 1] + +;; CHECK: [fuzz-exec] calling try_table-catch + +;; CHECK: [fuzz-exec] calling catchless-try_table +;; CHECK-NEXT: [exception thrown: e-i32 3] +;; CHECK-NEXT: [fuzz-exec] comparing catchless-try_table +;; CHECK-NEXT: [fuzz-exec] comparing throw +;; CHECK-NEXT: [fuzz-exec] comparing try_table-catch diff --git a/test/lit/merge/renamings.wat b/test/lit/merge/renamings.wat index c6a22542a..9c54f3514 100644 --- a/test/lit/merge/renamings.wat +++ b/test/lit/merge/renamings.wat @@ -160,6 +160,22 @@ ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (block $catch (result i32) + ;; CHECK-NEXT: (try_table (catch $foo $catch) + ;; CHECK-NEXT: (nop) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (block $catch0 (result i64) + ;; CHECK-NEXT: (try_table (catch $bar $catch0) + ;; CHECK-NEXT: (nop) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (i64.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop ;; CHECK-NEXT: (i32.load $foo ;; CHECK-NEXT: (i32.const 1) ;; CHECK-NEXT: ) @@ -220,6 +236,22 @@ ) ) ) + (drop + (block $catch (result i32) + (try_table (catch $foo $catch) + (nop) + ) + (i32.const 0) + ) + ) + (drop + (block $catch (result i64) + (try_table (catch $bar $catch) + (nop) + ) + (i64.const 0) + ) + ) ;; Memories (drop @@ -310,6 +342,22 @@ ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) ;; CHECK-NEXT: (drop +;; CHECK-NEXT: (block $catch (result f32) +;; CHECK-NEXT: (try_table (catch $foo_2 $catch) +;; CHECK-NEXT: (nop) +;; CHECK-NEXT: ) +;; CHECK-NEXT: (f32.const 0) +;; CHECK-NEXT: ) +;; CHECK-NEXT: ) +;; CHECK-NEXT: (drop +;; CHECK-NEXT: (block $catch0 (result f64) +;; CHECK-NEXT: (try_table (catch $other $catch0) +;; CHECK-NEXT: (nop) +;; CHECK-NEXT: ) +;; CHECK-NEXT: (f64.const 0) +;; CHECK-NEXT: ) +;; CHECK-NEXT: ) +;; CHECK-NEXT: (drop ;; CHECK-NEXT: (i32.load $foo_2 ;; CHECK-NEXT: (i32.const 3) ;; CHECK-NEXT: ) diff --git a/test/lit/merge/renamings.wat.second b/test/lit/merge/renamings.wat.second index 25d3d5e81..c17b00cc5 100644 --- a/test/lit/merge/renamings.wat.second +++ b/test/lit/merge/renamings.wat.second @@ -70,6 +70,22 @@ ) ) ) + (drop + (block $catch (result f32) + (try_table (catch $foo $catch) + (nop) + ) + (f32.const 0.0) + ) + ) + (drop + (block $catch (result f64) + (try_table (catch $other $catch) + (nop) + ) + (f64.const 0.0) + ) + ) ;; Memories (drop diff --git a/test/lit/passes/coalesce-locals-eh-legacy.wast b/test/lit/passes/coalesce-locals-eh-legacy.wast index 63b1445dd..9091fdcb9 100644 --- a/test/lit/passes/coalesce-locals-eh-legacy.wast +++ b/test/lit/passes/coalesce-locals-eh-legacy.wast @@ -3,8 +3,10 @@ (module ;; CHECK: (tag $e) + (tag $e) ;; CHECK: (tag $any (param (ref any))) + (tag $any (param (ref any))) ;; CHECK: (func $bar (type $2) (result i32) ;; CHECK-NEXT: (i32.const 1984) @@ -13,10 +15,6 @@ (i32.const 1984) ) - (tag $e) - - (tag $any (param (ref any))) - ;; CHECK: (func $bug-cfg-traversal (type $3) (param $0 i32) (result i32) ;; CHECK-NEXT: (try ;; CHECK-NEXT: (do diff --git a/test/lit/passes/coalesce-locals-eh.wast b/test/lit/passes/coalesce-locals-eh.wast new file mode 100644 index 000000000..90458f32f --- /dev/null +++ b/test/lit/passes/coalesce-locals-eh.wast @@ -0,0 +1,84 @@ +;; NOTE: Assertions have been generated by update_lit_checks.py and should not be edited. +;; RUN: wasm-opt %s --coalesce-locals -all -S -o - | filecheck %s + +(module + ;; CHECK: (tag $e) + (tag $e) + + ;; CHECK: (tag $any (param (ref any))) + (tag $any (param (ref any))) + + ;; CHECK: (func $bar (type $2) (result i32) + ;; CHECK-NEXT: (i32.const 1984) + ;; CHECK-NEXT: ) + (func $bar (result i32) + (i32.const 1984) + ) + + ;; CHECK: (func $bug-cfg-traversal (type $3) (param $0 i32) (result i32) + ;; CHECK-NEXT: (block $tryend + ;; CHECK-NEXT: (block $catch + ;; CHECK-NEXT: (try_table (catch_all $catch) + ;; CHECK-NEXT: (local.set $0 + ;; CHECK-NEXT: (call $bar) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (br $tryend) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (unreachable) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.get $0) + ;; CHECK-NEXT: ) + (func $bug-cfg-traversal (param $0 i32) (result i32) + (local $x i32) + ;; This is a regression test case for a bug in cfg-traversal for EH. + ;; See https://github.com/WebAssembly/binaryen/pull/3594 + (block $tryend + (block $catch + (try_table (catch_all $catch) + (local.set $x + ;; the call may or may not throw, so we may reach the get of $x + (call $bar) + ) + ) + (br $tryend) + ) + (unreachable) + ) + (local.get $x) + ) + + ;; CHECK: (func $0 (type $0) + ;; CHECK-NEXT: (local $0 anyref) + ;; CHECK-NEXT: (block $tryend + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (block (result anyref) + ;; CHECK-NEXT: (block $catch (result (ref any)) + ;; CHECK-NEXT: (try_table (catch $any $catch) + ;; CHECK-NEXT: (nop) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (br $tryend) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $0 + (local $0 (ref null any)) + (block $tryend + (drop + ;; There is a difference between the type of the value here and the type + ;; of the local, due to the local being nullable. We should not error on + ;; that as we replace the tee with a drop (as it has no gets). + (local.tee $0 + (block $catch (result (ref any)) + (try_table (catch $any $catch) + (nop) + ) + (br $tryend) + ) + ) + ) + ) + ) +) diff --git a/test/lit/passes/code-folding-eh-legacy.wast b/test/lit/passes/code-folding-eh-legacy.wast index 05cd8db8b..852ec126a 100644 --- a/test/lit/passes/code-folding-eh-legacy.wast +++ b/test/lit/passes/code-folding-eh-legacy.wast @@ -96,6 +96,7 @@ (func $try-call-optimize-terminating-tails-success (result i32) (try (do + ;; Expressions that cannot throw can be taken out of 'try' scope. (drop (i32.const 1)) (drop (i32.const 1)) (return (i32.const 0)) @@ -243,6 +244,9 @@ (drop (i32.const 1)) (drop (i32.const 1)) (drop (i32.const 1)) + ;; return_call executes the call after returning from this function. + ;; This try cannot catch exceptions it throws, so we can fold it out of + ;; the try. (return_call $foo-i32) ) (catch_all @@ -281,6 +285,7 @@ (block $x (try (do + ;; Expressions that cannot throw can be taken out of 'try' scope. (drop (i32.const 1)) (drop (i32.const 1)) (drop (i32.const 1)) diff --git a/test/lit/passes/code-folding-eh.wast b/test/lit/passes/code-folding-eh.wast new file mode 100644 index 000000000..5a7cd68c7 --- /dev/null +++ b/test/lit/passes/code-folding-eh.wast @@ -0,0 +1,296 @@ +;; NOTE: Assertions have been generated by update_lit_checks.py and should not be edited. +;; RUN: wasm-opt %s --remove-unused-names --code-folding -all -S -o - \ +;; RUN: | filecheck %s + +(module + ;; CHECK: (tag $e-i32 (param i32)) + (tag $e-i32 (param i32)) + + ;; CHECK: (func $try_table-call-optimize-terminating-tails-success (type $0) (result i32) + ;; CHECK-NEXT: (block $folding-inner0 + ;; CHECK-NEXT: (return + ;; CHECK-NEXT: (block (result i32) + ;; CHECK-NEXT: (block $tryend + ;; CHECK-NEXT: (block $catch + ;; CHECK-NEXT: (try_table (catch_all $catch) + ;; CHECK-NEXT: (br $folding-inner0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (br $tryend) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (br $folding-inner0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (i32.const 1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (i32.const 1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (return + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $try_table-call-optimize-terminating-tails-success (result i32) + (block $tryend + (block $catch + (try_table (catch_all $catch) + ;; Expressions that cannot throw can be taken out of 'try' scope. + (drop (i32.const 1)) + (drop (i32.const 1)) + (return (i32.const 0)) + ) + (br $tryend) + ) + (drop (i32.const 1)) + (drop (i32.const 1)) + (return (i32.const 0)) + ) + (i32.const 0) + ) + + + ;; CHECK: (func $foo (type $1) + ;; CHECK-NEXT: (nop) + ;; CHECK-NEXT: ) + (func $foo) + + ;; CHECK: (func $try_table-call-optimize-terminating-tails (type $0) (result i32) + ;; CHECK-NEXT: (block $tryend + ;; CHECK-NEXT: (block $catch + ;; CHECK-NEXT: (try_table (catch_all $catch) + ;; CHECK-NEXT: (call $foo) + ;; CHECK-NEXT: (call $foo) + ;; CHECK-NEXT: (call $foo) + ;; CHECK-NEXT: (call $foo) + ;; CHECK-NEXT: (return + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (br $tryend) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (call $foo) + ;; CHECK-NEXT: (call $foo) + ;; CHECK-NEXT: (call $foo) + ;; CHECK-NEXT: (call $foo) + ;; CHECK-NEXT: (return + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + (func $try_table-call-optimize-terminating-tails (result i32) + (block $tryend + (block $catch + (try_table (catch_all $catch) + ;; Expressions that can throw should NOT be taken out of 'try' scope. + (call $foo) + (call $foo) + (call $foo) + (call $foo) + (return (i32.const 0)) + ) + (br $tryend) + ) + (call $foo) + (call $foo) + (call $foo) + (call $foo) + (return (i32.const 0)) + ) + (i32.const 0) + ) + + ;; CHECK: (func $foo-i32 (type $0) (result i32) + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + (func $foo-i32 (result i32) + (i32.const 0) + ) + + ;; CHECK: (func $try_table-call-optimize-terminating-tails-call-return (type $0) (result i32) + ;; CHECK-NEXT: (block $tryend + ;; CHECK-NEXT: (block $catch + ;; CHECK-NEXT: (try_table (catch_all $catch) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (i32.const 1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (i32.const 1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (return + ;; CHECK-NEXT: (call $foo-i32) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (br $tryend) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (i32.const 1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (i32.const 1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (return + ;; CHECK-NEXT: (call $foo-i32) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + (func $try_table-call-optimize-terminating-tails-call-return (result i32) + (block $tryend + (block $catch + (try_table (catch_all $catch) + (drop (i32.const 1)) + (drop (i32.const 1)) + ;; Cannot be folded out of the try because it might throw. + (return (call $foo-i32)) + ) + (br $tryend) + ) + (drop (i32.const 1)) + (drop (i32.const 1)) + (return (call $foo-i32)) + ) + (i32.const 0) + ) + + ;; CHECK: (func $try_table-call-optimize-terminating-tails-return-call (type $0) (result i32) + ;; CHECK-NEXT: (block $folding-inner0 + ;; CHECK-NEXT: (return + ;; CHECK-NEXT: (block (result i32) + ;; CHECK-NEXT: (block $tryend + ;; CHECK-NEXT: (block $catch + ;; CHECK-NEXT: (try_table (catch_all $catch) + ;; CHECK-NEXT: (br $folding-inner0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (br $tryend) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (br $folding-inner0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (i32.const 1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (i32.const 1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (i32.const 1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (return_call $foo-i32) + ;; CHECK-NEXT: ) + (func $try_table-call-optimize-terminating-tails-return-call (result i32) + (block $tryend + (block $catch + (try_table (catch_all $catch) + (drop (i32.const 1)) + (drop (i32.const 1)) + (drop (i32.const 1)) + ;; return_call executes the call after returning from this function. + ;; This try_table cannot catch exceptions it throws, so we can fold it + ;; out of the try_table. + (return_call $foo-i32) + ) + (br $tryend) + ) + (drop (i32.const 1)) + (drop (i32.const 1)) + (drop (i32.const 1)) + (return_call $foo-i32) + ) + (i32.const 0) + ) + + ;; CHECK: (func $try_table-call-optimize-expression-tails-success (type $1) + ;; CHECK-NEXT: (block $x + ;; CHECK-NEXT: (block $tryend + ;; CHECK-NEXT: (block $catch + ;; CHECK-NEXT: (try_table (catch_all $catch) + ;; CHECK-NEXT: (br $x) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (br $tryend) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (br $x) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (unreachable) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (i32.const 1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (i32.const 1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (i32.const 1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $try_table-call-optimize-expression-tails-success + (block $x + (block $tryend + (block $catch + (try_table (catch_all $catch) + ;; Expressions that cannot throw can be taken out of 'try_table' + ;; scope. + (drop (i32.const 1)) + (drop (i32.const 1)) + (drop (i32.const 1)) + (br $x) + ) + (br $tryend) + ) + (drop (i32.const 1)) + (drop (i32.const 1)) + (drop (i32.const 1)) + (br $x) + ) + (unreachable) + ) + ) + + ;; CHECK: (func $try_table-call-optimize-expression-tails (type $1) + ;; CHECK-NEXT: (block $x + ;; CHECK-NEXT: (block $tryend + ;; CHECK-NEXT: (block $catch + ;; CHECK-NEXT: (try_table (catch_all $catch) + ;; CHECK-NEXT: (call $foo) + ;; CHECK-NEXT: (call $foo) + ;; CHECK-NEXT: (call $foo) + ;; CHECK-NEXT: (br $x) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (br $tryend) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (call $foo) + ;; CHECK-NEXT: (call $foo) + ;; CHECK-NEXT: (call $foo) + ;; CHECK-NEXT: (br $x) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (unreachable) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $try_table-call-optimize-expression-tails + (block $x + (block $tryend + (block $catch + (try_table (catch_all $catch) + ;; Expressions that can throw should NOT be taken out of 'try' scope. + (call $foo) + (call $foo) + (call $foo) + (br $x) + ) + (br $tryend) + ) + (call $foo) + (call $foo) + (call $foo) + (br $x) + ) + (unreachable) + ) + ) +) diff --git a/test/lit/passes/code-pushing-eh-legacy.wast b/test/lit/passes/code-pushing-eh-legacy.wast index 8fc0d423d..9511d244a 100644 --- a/test/lit/passes/code-pushing-eh-legacy.wast +++ b/test/lit/passes/code-pushing-eh-legacy.wast @@ -7,69 +7,6 @@ ;; CHECK: (tag $e (param i32)) (tag $e (param i32)) - ;; CHECK: (func $cannot-push-past-call (type $0) - ;; CHECK-NEXT: (local $x i32) - ;; CHECK-NEXT: (block $out - ;; CHECK-NEXT: (local.set $x - ;; CHECK-NEXT: (i32.const 1) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: (call $cannot-push-past-call) - ;; CHECK-NEXT: (drop - ;; CHECK-NEXT: (i32.const 1) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: (br_if $out - ;; CHECK-NEXT: (i32.const 2) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: (drop - ;; CHECK-NEXT: (local.get $x) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: ) - (func $cannot-push-past-call - (local $x i32) - (block $out - ;; This local.set cannot be pushed down, because the call below can throw - (local.set $x (i32.const 1)) - (call $cannot-push-past-call) - (drop (i32.const 1)) - (br_if $out (i32.const 2)) - (drop (local.get $x)) - ) - ) - - ;; CHECK: (func $cannot-push-past-throw (type $0) - ;; CHECK-NEXT: (local $x i32) - ;; CHECK-NEXT: (block $out - ;; CHECK-NEXT: (local.set $x - ;; CHECK-NEXT: (i32.const 1) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: (throw $e - ;; CHECK-NEXT: (i32.const 0) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: (drop - ;; CHECK-NEXT: (i32.const 1) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: (br_if $out - ;; CHECK-NEXT: (i32.const 2) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: (drop - ;; CHECK-NEXT: (local.get $x) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: ) - (func $cannot-push-past-throw - (local $x i32) - (block $out - ;; This local.set cannot be pushed down, because there is 'throw' below. - ;; This pass only pushes past conditional control flow atm. - (local.set $x (i32.const 1)) - (throw $e (i32.const 0)) - (drop (i32.const 1)) - (br_if $out (i32.const 2)) - (drop (local.get $x)) - ) - ) - ;; CHECK: (func $can-push-past-try (type $0) ;; CHECK-NEXT: (local $x i32) ;; CHECK-NEXT: (block $out @@ -323,80 +260,4 @@ (drop (local.get $x)) ) ) - - ;; CHECK: (func $can-push-past-conditional-throw (type $1) (param $param i32) - ;; CHECK-NEXT: (local $x i32) - ;; CHECK-NEXT: (block $block - ;; CHECK-NEXT: (if - ;; CHECK-NEXT: (local.get $param) - ;; CHECK-NEXT: (then - ;; CHECK-NEXT: (throw $e - ;; CHECK-NEXT: (i32.const 0) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: (local.set $x - ;; CHECK-NEXT: (i32.const 1) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: (drop - ;; CHECK-NEXT: (local.get $x) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: ) - (func $can-push-past-conditional-throw (param $param i32) - (local $x i32) - (block $block - ;; We can push past an if containing a throw. The if is conditional - ;; control flow, which is what we look for in this optimization, and a - ;; throw is like a break - it will jump out of the current block - so we - ;; can push the set past it, as the set is only needed in this block. - (local.set $x (i32.const 1)) - (if - (local.get $param) - (then - (throw $e (i32.const 0)) - ) - ) - (drop (local.get $x)) - ) - ) - - ;; CHECK: (func $cannot-push-past-conditional-throw-extra-use (type $1) (param $param i32) - ;; CHECK-NEXT: (local $x i32) - ;; CHECK-NEXT: (block $block - ;; CHECK-NEXT: (local.set $x - ;; CHECK-NEXT: (i32.const 1) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: (if - ;; CHECK-NEXT: (local.get $param) - ;; CHECK-NEXT: (then - ;; CHECK-NEXT: (throw $e - ;; CHECK-NEXT: (i32.const 0) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: (drop - ;; CHECK-NEXT: (local.get $x) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: (drop - ;; CHECK-NEXT: (local.get $x) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: ) - (func $cannot-push-past-conditional-throw-extra-use (param $param i32) - (local $x i32) - ;; As above, but now there is another local.get outside of the block. That - ;; means the local.set cannot be pushed to a place it might not execute. - (block $block - (local.set $x (i32.const 1)) - (if - (local.get $param) - (then - (throw $e (i32.const 0)) - ) - ) - (drop (local.get $x)) - ) - (drop (local.get $x)) - ) ) diff --git a/test/lit/passes/code-pushing-eh.wast b/test/lit/passes/code-pushing-eh.wast new file mode 100644 index 000000000..ee2798c46 --- /dev/null +++ b/test/lit/passes/code-pushing-eh.wast @@ -0,0 +1,300 @@ +;; NOTE: Assertions have been generated by update_lit_checks.py and should not be edited. +;; RUN: wasm-opt %s --code-pushing -all -S -o - | filecheck %s + +;; The tests in this file test EffectAnalyzer, which is used by CodePushing. + +(module + ;; CHECK: (tag $e (param i32)) + (tag $e (param i32)) + + ;; CHECK: (func $cannot-push-past-call (type $0) + ;; CHECK-NEXT: (local $x i32) + ;; CHECK-NEXT: (block $out + ;; CHECK-NEXT: (local.set $x + ;; CHECK-NEXT: (i32.const 1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (call $cannot-push-past-call) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (i32.const 1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (br_if $out + ;; CHECK-NEXT: (i32.const 2) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (local.get $x) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $cannot-push-past-call + (local $x i32) + (block $out + ;; This local.set cannot be pushed down, because the call below can throw. + (local.set $x (i32.const 1)) + (call $cannot-push-past-call) + (drop (i32.const 1)) + (br_if $out (i32.const 2)) + (drop (local.get $x)) + ) + ) + + ;; CHECK: (func $cannot-push-past-throw (type $0) + ;; CHECK-NEXT: (local $x i32) + ;; CHECK-NEXT: (block $out + ;; CHECK-NEXT: (local.set $x + ;; CHECK-NEXT: (i32.const 1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (throw $e + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (i32.const 1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (br_if $out + ;; CHECK-NEXT: (i32.const 2) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (local.get $x) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $cannot-push-past-throw + (local $x i32) + (block $out + ;; This local.set cannot be pushed down, because there is 'throw' below. + ;; This pass only pushes past conditional control flow atm. + (local.set $x (i32.const 1)) + (throw $e (i32.const 0)) + (drop (i32.const 1)) + (br_if $out (i32.const 2)) + (drop (local.get $x)) + ) + ) + + ;; CHECK: (func $can-push-past-try_table (type $0) + ;; CHECK-NEXT: (local $x i32) + ;; CHECK-NEXT: (block $out + ;; CHECK-NEXT: (block $tryend + ;; CHECK-NEXT: (block $catch + ;; CHECK-NEXT: (try_table (catch_all $catch) + ;; CHECK-NEXT: (throw $e + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (br $tryend) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (i32.const 1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (br_if $out + ;; CHECK-NEXT: (i32.const 2) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.set $x + ;; CHECK-NEXT: (i32.const 1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (local.get $x) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $can-push-past-try_table + (local $x i32) + (block $out + ;; This local.set can be pushed down, because the 'throw' below is going + ;; to be caught by the inner catch_all. + (local.set $x (i32.const 1)) + (block $tryend + (block $catch + (try_table (catch_all $catch) + (throw $e (i32.const 0)) + ) + (br $tryend) + ) + ) + (drop (i32.const 1)) + (br_if $out (i32.const 2)) + (drop (local.get $x)) + ) + ) + + ;; CHECK: (func $foo (type $0) + ;; CHECK-NEXT: (nop) + ;; CHECK-NEXT: ) + (func $foo) + + ;; CHECK: (func $cannot-push-past-try_table (type $0) + ;; CHECK-NEXT: (local $x i32) + ;; CHECK-NEXT: (block $out + ;; CHECK-NEXT: (local.set $x + ;; CHECK-NEXT: (i32.const 1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (block $tryend + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (block $catch (result i32) + ;; CHECK-NEXT: (try_table (catch $e $catch) + ;; CHECK-NEXT: (call $foo) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (br $tryend) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (i32.const 1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (br_if $out + ;; CHECK-NEXT: (i32.const 2) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (local.get $x) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $cannot-push-past-try_table + (local $x i32) + (block $out + ;; This local.set cannot be pushed down, because the exception thrown by + ;; 'call $foo' below may not be caught by 'catch $e'. + (local.set $x (i32.const 1)) + (block $tryend + (drop + (block $catch (result i32) + (try_table (catch $e $catch) + (call $foo) + ) + (br $tryend) + ) + ) + ) + (drop (i32.const 1)) + (br_if $out (i32.const 2)) + (drop (local.get $x)) + ) + ) + + ;; CHECK: (func $cannot-push-past-throw_ref-within-catch (type $0) + ;; CHECK-NEXT: (local $x i32) + ;; CHECK-NEXT: (block $out + ;; CHECK-NEXT: (local.set $x + ;; CHECK-NEXT: (i32.const 1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (block $tryend + ;; CHECK-NEXT: (throw_ref + ;; CHECK-NEXT: (block $catch (result exnref) + ;; CHECK-NEXT: (try_table (catch_all_ref $catch) + ;; CHECK-NEXT: (throw $e + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (br $tryend) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (i32.const 1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (br_if $out + ;; CHECK-NEXT: (i32.const 2) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (local.get $x) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $cannot-push-past-throw_ref-within-catch + (local $x i32) + (block $out + ;; This local.set cannot be pushed down, because there is 'throw_ref' + ;; within the catch handler. + (local.set $x (i32.const 1)) + (block $tryend + (throw_ref + (block $catch (result exnref) + (try_table (catch_all_ref $catch) + (throw $e (i32.const 0)) + ) + (br $tryend) + ) + ) + ) + (drop (i32.const 1)) + (br_if $out (i32.const 2)) + (drop (local.get $x)) + ) + ) + + ;; CHECK: (func $can-push-past-conditional-throw (type $1) (param $param i32) + ;; CHECK-NEXT: (local $x i32) + ;; CHECK-NEXT: (block $block + ;; CHECK-NEXT: (if + ;; CHECK-NEXT: (local.get $param) + ;; CHECK-NEXT: (then + ;; CHECK-NEXT: (throw $e + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.set $x + ;; CHECK-NEXT: (i32.const 1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (local.get $x) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $can-push-past-conditional-throw (param $param i32) + (local $x i32) + (block $block + ;; We can push past an if containing a throw. The if is conditional + ;; control flow, which is what we look for in this optimization, and a + ;; throw is like a break - it will jump out of the current block - so we + ;; can push the set past it, as the set is only needed in this block. + (local.set $x (i32.const 1)) + (if + (local.get $param) + (then + (throw $e (i32.const 0)) + ) + ) + (drop (local.get $x)) + ) + ) + + ;; CHECK: (func $cannot-push-past-conditional-throw-extra-use (type $1) (param $param i32) + ;; CHECK-NEXT: (local $x i32) + ;; CHECK-NEXT: (block $block + ;; CHECK-NEXT: (local.set $x + ;; CHECK-NEXT: (i32.const 1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (if + ;; CHECK-NEXT: (local.get $param) + ;; CHECK-NEXT: (then + ;; CHECK-NEXT: (throw $e + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (local.get $x) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (local.get $x) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $cannot-push-past-conditional-throw-extra-use (param $param i32) + (local $x i32) + ;; As above, but now there is another local.get outside of the block. That + ;; means the local.set cannot be pushed to a place it might not execute. + (block $block + (local.set $x (i32.const 1)) + (if + (local.get $param) + (then + (throw $e (i32.const 0)) + ) + ) + (drop (local.get $x)) + ) + (drop (local.get $x)) + ) +) diff --git a/test/lit/passes/dce-eh-legacy.wast b/test/lit/passes/dce-eh-legacy.wast index 120ec4e11..ef6d569d6 100644 --- a/test/lit/passes/dce-eh-legacy.wast +++ b/test/lit/passes/dce-eh-legacy.wast @@ -79,32 +79,6 @@ (call $foo) ;; should be dce'd ) - ;; CHECK: (func $throw (type $0) - ;; CHECK-NEXT: (block $label$0 - ;; CHECK-NEXT: (block $label$1 - ;; CHECK-NEXT: (throw $e) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: ) - (func $throw - ;; All these wrapping expressions before 'throw' will be dce'd - (drop - (block $label$0 (result externref) - (if - (i32.clz - (block $label$1 (result i32) - (throw $e) - ) - ) - (then - (nop) - ) - ) - (ref.null extern) - ) - ) - ) - ;; CHECK: (func $rethrow (type $0) ;; CHECK-NEXT: (try $l0 ;; CHECK-NEXT: (do diff --git a/test/lit/passes/dce-eh.wast b/test/lit/passes/dce-eh.wast new file mode 100644 index 000000000..413a278d0 --- /dev/null +++ b/test/lit/passes/dce-eh.wast @@ -0,0 +1,145 @@ +;; NOTE: Assertions have been generated by update_lit_checks.py and should not be edited. +;; RUN: wasm-opt %s --dce -all -S -o - | filecheck %s + +;; If either try_table body or any of catch handler is reachable, the whole +;; try_table construct is reachable. +(module + ;; CHECK: (tag $e) + (tag $e) + + ;; CHECK: (tag $e-i32 (param i32)) + (tag $e-i32 (param i32)) + + ;; CHECK: (func $foo (type $0) + ;; CHECK-NEXT: (nop) + ;; CHECK-NEXT: ) + (func $foo) + + ;; CHECK: (func $try_table_unreachable (type $0) + ;; CHECK-NEXT: (block $catch + ;; CHECK-NEXT: (try_table (catch_all $catch) + ;; CHECK-NEXT: (unreachable) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (call $foo) + ;; CHECK-NEXT: ) + (func $try_table_unreachable + (block $catch + (try_table (catch_all $catch) + (unreachable) + ) + ) + (call $foo) ;; shouldn't be dce'd + ) + + ;; CHECK: (func $catch_unreachable (type $0) + ;; CHECK-NEXT: (block $tryend + ;; CHECK-NEXT: (block $catch + ;; CHECK-NEXT: (try_table (catch_all $catch) + ;; CHECK-NEXT: (br $tryend) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (unreachable) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (call $foo) + ;; CHECK-NEXT: ) + (func $catch_unreachable + (block $tryend + (block $catch + (try_table (catch_all $catch) + (br $tryend) + ) + ) + (unreachable) + ) + (call $foo) ;; shouldn't be dce'd + ) + + ;; CHECK: (func $both_unreachable (type $0) + ;; CHECK-NEXT: (block $tryend + ;; CHECK-NEXT: (block $catch + ;; CHECK-NEXT: (try_table (catch_all $catch) + ;; CHECK-NEXT: (unreachable) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (unreachable) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $both_unreachable + (block $tryend + (block $catch + (try_table (catch_all $catch) + (unreachable) + ) + ) + (unreachable) + ) + (call $foo) ;; should be dce'd + ) + + ;; CHECK: (func $throw (type $0) + ;; CHECK-NEXT: (block $label$0 + ;; CHECK-NEXT: (block $label$1 + ;; CHECK-NEXT: (throw $e) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $throw + ;; All these wrapping expressions before 'throw' will be dce'd. + (drop + (block $label$0 (result externref) + (if + (i32.clz + (block $label$1 (result i32) + (throw $e) + ) + ) + (then + (nop) + ) + ) + (ref.null extern) + ) + ) + ) + + ;; CHECK: (func $throw_ref (type $0) + ;; CHECK-NEXT: (local $ex exnref) + ;; CHECK-NEXT: (block $tryend + ;; CHECK-NEXT: (local.set $ex + ;; CHECK-NEXT: (block $catch (result exnref) + ;; CHECK-NEXT: (try_table (catch_all_ref $catch) + ;; CHECK-NEXT: (br $tryend) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (block + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (throw_ref + ;; CHECK-NEXT: (local.get $ex) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $throw_ref + (local $ex exnref) + (block $tryend + (local.set $ex + (block $catch (result exnref) + (try_table (catch_all_ref $catch) + (br $tryend) + ) + ) + ) + (drop + ;; This i32.add will be dce'd. + (i32.add + (i32.const 0) + (throw_ref (local.get $ex)) + ) + ) + ) + ) +) diff --git a/test/lit/passes/global-effects-eh-legacy.wast b/test/lit/passes/global-effects-eh-legacy.wast new file mode 100644 index 000000000..7390f996d --- /dev/null +++ b/test/lit/passes/global-effects-eh-legacy.wast @@ -0,0 +1,507 @@ +;; NOTE: Assertions have been generated by update_lit_checks.py --all-items and should not be edited. + +;; Run without global effects, and run with, and also run with but discard them +;; first (to check that discard works; that should be the same as without). + +;; RUN: foreach %s %t wasm-opt -all --vacuum -S -o - | filecheck %s --check-prefix WITHOUT +;; RUN: foreach %s %t wasm-opt -all --generate-global-effects --vacuum -S -o - | filecheck %s --check-prefix INCLUDE +;; RUN: foreach %s %t wasm-opt -all --generate-global-effects --discard-global-effects --vacuum -S -o - | filecheck %s --check-prefix WITHOUT + +(module + + ;; WITHOUT: (type $void (func)) + ;; INCLUDE: (type $void (func)) + (type $void (func)) + + ;; WITHOUT: (type $1 (func (result i32))) + + ;; WITHOUT: (type $2 (func (param i32))) + + ;; WITHOUT: (import "a" "b" (func $import (type $void))) + ;; INCLUDE: (type $1 (func (result i32))) + + ;; INCLUDE: (type $2 (func (param i32))) + + ;; INCLUDE: (import "a" "b" (func $import (type $void))) + (import "a" "b" (func $import)) + + ;; WITHOUT: (table $t 0 funcref) + ;; INCLUDE: (table $t 0 funcref) + (table $t 0 funcref) + + ;; WITHOUT: (elem declare func $throw) + + ;; WITHOUT: (tag $tag) + ;; INCLUDE: (elem declare func $throw) + + ;; INCLUDE: (tag $tag) + (tag $tag) + + ;; WITHOUT: (func $main (type $void) + ;; WITHOUT-NEXT: (call $nop) + ;; WITHOUT-NEXT: (call $unreachable) + ;; WITHOUT-NEXT: (call $call-nop) + ;; WITHOUT-NEXT: (call $call-unreachable) + ;; WITHOUT-NEXT: (drop + ;; WITHOUT-NEXT: (call $unimportant-effects) + ;; WITHOUT-NEXT: ) + ;; WITHOUT-NEXT: (call $throw) + ;; WITHOUT-NEXT: (call $throw-and-import) + ;; WITHOUT-NEXT: ) + ;; INCLUDE: (func $main (type $void) + ;; INCLUDE-NEXT: (call $unreachable) + ;; INCLUDE-NEXT: (call $call-unreachable) + ;; INCLUDE-NEXT: (call $throw) + ;; INCLUDE-NEXT: (call $throw-and-import) + ;; INCLUDE-NEXT: ) + (func $main + ;; Calling a function with no effects can be optimized away in INCLUDE (but + ;; not WITHOUT or DISCARD, where the global effect info is not available). + (call $nop) + ;; Calling a function with effects cannot. + (call $unreachable) + ;; Calling something that calls something with no effects can be optimized + ;; away, since we compute transitive effects + (call $call-nop) + ;; Calling something that calls something with effects cannot. + (call $call-unreachable) + ;; Calling something that only has unimportant effects can be optimized + ;; (see below for details). + (drop + (call $unimportant-effects) + ) + ;; A throwing function cannot be removed. + (call $throw) + ;; A function that throws and calls an import definitely cannot be removed. + (call $throw-and-import) + ) + + ;; WITHOUT: (func $cycle (type $void) + ;; WITHOUT-NEXT: (call $cycle) + ;; WITHOUT-NEXT: ) + ;; INCLUDE: (func $cycle (type $void) + ;; INCLUDE-NEXT: (call $cycle) + ;; INCLUDE-NEXT: ) + (func $cycle + ;; Calling a function with no effects in a cycle cannot be optimized out - + ;; this must keep hanging forever. + (call $cycle) + ) + + ;; WITHOUT: (func $cycle-1 (type $void) + ;; WITHOUT-NEXT: (call $cycle-2) + ;; WITHOUT-NEXT: ) + ;; INCLUDE: (func $cycle-1 (type $void) + ;; INCLUDE-NEXT: (call $cycle-2) + ;; INCLUDE-NEXT: ) + (func $cycle-1 + ;; $cycle-1 and -2 form a cycle together, in which no call can be removed. + (call $cycle-2) + ) + + ;; WITHOUT: (func $cycle-2 (type $void) + ;; WITHOUT-NEXT: (call $cycle-1) + ;; WITHOUT-NEXT: ) + ;; INCLUDE: (func $cycle-2 (type $void) + ;; INCLUDE-NEXT: (call $cycle-1) + ;; INCLUDE-NEXT: ) + (func $cycle-2 + (call $cycle-1) + ) + + ;; WITHOUT: (func $nop (type $void) + ;; WITHOUT-NEXT: (nop) + ;; WITHOUT-NEXT: ) + ;; INCLUDE: (func $nop (type $void) + ;; INCLUDE-NEXT: (nop) + ;; INCLUDE-NEXT: ) + (func $nop + (nop) + ) + + ;; WITHOUT: (func $unreachable (type $void) + ;; WITHOUT-NEXT: (unreachable) + ;; WITHOUT-NEXT: ) + ;; INCLUDE: (func $unreachable (type $void) + ;; INCLUDE-NEXT: (unreachable) + ;; INCLUDE-NEXT: ) + (func $unreachable + (unreachable) + ) + + ;; WITHOUT: (func $call-nop (type $void) + ;; WITHOUT-NEXT: (call $nop) + ;; WITHOUT-NEXT: ) + ;; INCLUDE: (func $call-nop (type $void) + ;; INCLUDE-NEXT: (nop) + ;; INCLUDE-NEXT: ) + (func $call-nop + ;; This call to a nop can be optimized out, as above, in INCLUDE. + (call $nop) + ) + + ;; WITHOUT: (func $call-unreachable (type $void) + ;; WITHOUT-NEXT: (call $unreachable) + ;; WITHOUT-NEXT: ) + ;; INCLUDE: (func $call-unreachable (type $void) + ;; INCLUDE-NEXT: (call $unreachable) + ;; INCLUDE-NEXT: ) + (func $call-unreachable + (call $unreachable) + ) + + ;; WITHOUT: (func $unimportant-effects (type $1) (result i32) + ;; WITHOUT-NEXT: (local $x i32) + ;; WITHOUT-NEXT: (local.set $x + ;; WITHOUT-NEXT: (i32.const 100) + ;; WITHOUT-NEXT: ) + ;; WITHOUT-NEXT: (return + ;; WITHOUT-NEXT: (local.get $x) + ;; WITHOUT-NEXT: ) + ;; WITHOUT-NEXT: ) + ;; INCLUDE: (func $unimportant-effects (type $1) (result i32) + ;; INCLUDE-NEXT: (local $x i32) + ;; INCLUDE-NEXT: (local.set $x + ;; INCLUDE-NEXT: (i32.const 100) + ;; INCLUDE-NEXT: ) + ;; INCLUDE-NEXT: (return + ;; INCLUDE-NEXT: (local.get $x) + ;; INCLUDE-NEXT: ) + ;; INCLUDE-NEXT: ) + (func $unimportant-effects (result i32) + (local $x i32) + ;; Operations on locals should not prevent optimization, as when we return + ;; from the function they no longer matter. + (local.set $x + (i32.const 100) + ) + ;; A return is an effect that no longer matters once we exit the function. + (return + (local.get $x) + ) + ) + + ;; WITHOUT: (func $call-throw-and-catch (type $void) + ;; WITHOUT-NEXT: (try + ;; WITHOUT-NEXT: (do + ;; WITHOUT-NEXT: (call $throw) + ;; WITHOUT-NEXT: ) + ;; WITHOUT-NEXT: (catch_all + ;; WITHOUT-NEXT: (nop) + ;; WITHOUT-NEXT: ) + ;; WITHOUT-NEXT: ) + ;; WITHOUT-NEXT: (try + ;; WITHOUT-NEXT: (do + ;; WITHOUT-NEXT: (call $throw-and-import) + ;; WITHOUT-NEXT: ) + ;; WITHOUT-NEXT: (catch_all + ;; WITHOUT-NEXT: (nop) + ;; WITHOUT-NEXT: ) + ;; WITHOUT-NEXT: ) + ;; WITHOUT-NEXT: ) + ;; INCLUDE: (func $call-throw-and-catch (type $void) + ;; INCLUDE-NEXT: (try + ;; INCLUDE-NEXT: (do + ;; INCLUDE-NEXT: (call $throw-and-import) + ;; INCLUDE-NEXT: ) + ;; INCLUDE-NEXT: (catch_all + ;; INCLUDE-NEXT: (nop) + ;; INCLUDE-NEXT: ) + ;; INCLUDE-NEXT: ) + ;; INCLUDE-NEXT: ) + (func $call-throw-and-catch + (try + (do + ;; This call cannot be optimized out, as the target throws. However, the + ;; entire try-catch can be, since the call's only effect is to throw, + ;; and the catch_all catches that. + (call $throw) + ) + (catch_all) + ) + (try + (do + ;; This call both throws and calls an import, and cannot be removed. + (call $throw-and-import) + ) + (catch_all) + ) + ) + + ;; WITHOUT: (func $return-call-throw-and-catch (type $void) + ;; WITHOUT-NEXT: (return_call $throw) + ;; WITHOUT-NEXT: ) + ;; INCLUDE: (func $return-call-throw-and-catch (type $void) + ;; INCLUDE-NEXT: (return_call $throw) + ;; INCLUDE-NEXT: ) + (func $return-call-throw-and-catch + (try + (do + ;; This call cannot be optimized out, as the target throws. However, the + ;; surrounding try-catch can be removed even without global effects + ;; because the throw from the return_call is never observed by this + ;; try-catch. + (return_call $throw) + ) + (catch_all) + ) + ) + + ;; WITHOUT: (func $return-call-indirect-throw-and-catch (type $void) + ;; WITHOUT-NEXT: (return_call_indirect $t (type $void) + ;; WITHOUT-NEXT: (i32.const 0) + ;; WITHOUT-NEXT: ) + ;; WITHOUT-NEXT: ) + ;; INCLUDE: (func $return-call-indirect-throw-and-catch (type $void) + ;; INCLUDE-NEXT: (return_call_indirect $t (type $void) + ;; INCLUDE-NEXT: (i32.const 0) + ;; INCLUDE-NEXT: ) + ;; INCLUDE-NEXT: ) + (func $return-call-indirect-throw-and-catch + (try + (do + ;; This call cannot be optimized out, as the target may throw. However, + ;; the surrounding try-catch can be removed even without global effects + ;; because the throw from the return_call is never observed by this + ;; try-catch. + (return_call_indirect + (i32.const 0) + ) + ) + (catch_all) + ) + ) + + ;; WITHOUT: (func $return-call-ref-throw-and-catch (type $void) + ;; WITHOUT-NEXT: (return_call_ref $void + ;; WITHOUT-NEXT: (ref.func $throw) + ;; WITHOUT-NEXT: ) + ;; WITHOUT-NEXT: ) + ;; INCLUDE: (func $return-call-ref-throw-and-catch (type $void) + ;; INCLUDE-NEXT: (return_call_ref $void + ;; INCLUDE-NEXT: (ref.func $throw) + ;; INCLUDE-NEXT: ) + ;; INCLUDE-NEXT: ) + (func $return-call-ref-throw-and-catch + (try + (do + ;; This call cannot be optimized out, as the target may throw. However, + ;; the surrounding try-catch can be removed even without global effects + ;; because the throw from the return_call is never observed by this + ;; try-catch. + (return_call_ref $void + (ref.func $throw) + ) + ) + (catch_all) + ) + ) + + ;; WITHOUT: (func $call-return-call-throw-and-catch (type $void) + ;; WITHOUT-NEXT: (try + ;; WITHOUT-NEXT: (do + ;; WITHOUT-NEXT: (call $return-call-throw-and-catch) + ;; WITHOUT-NEXT: ) + ;; WITHOUT-NEXT: (catch_all + ;; WITHOUT-NEXT: (nop) + ;; WITHOUT-NEXT: ) + ;; WITHOUT-NEXT: ) + ;; WITHOUT-NEXT: (try + ;; WITHOUT-NEXT: (do + ;; WITHOUT-NEXT: (call $return-call-indirect-throw-and-catch) + ;; WITHOUT-NEXT: ) + ;; WITHOUT-NEXT: (catch_all + ;; WITHOUT-NEXT: (nop) + ;; WITHOUT-NEXT: ) + ;; WITHOUT-NEXT: ) + ;; WITHOUT-NEXT: (try + ;; WITHOUT-NEXT: (do + ;; WITHOUT-NEXT: (call $return-call-ref-throw-and-catch) + ;; WITHOUT-NEXT: ) + ;; WITHOUT-NEXT: (catch_all + ;; WITHOUT-NEXT: (nop) + ;; WITHOUT-NEXT: ) + ;; WITHOUT-NEXT: ) + ;; WITHOUT-NEXT: (call $return-call-throw-and-catch) + ;; WITHOUT-NEXT: (call $return-call-indirect-throw-and-catch) + ;; WITHOUT-NEXT: (call $return-call-ref-throw-and-catch) + ;; WITHOUT-NEXT: ) + ;; INCLUDE: (func $call-return-call-throw-and-catch (type $void) + ;; INCLUDE-NEXT: (try + ;; INCLUDE-NEXT: (do + ;; INCLUDE-NEXT: (call $return-call-indirect-throw-and-catch) + ;; INCLUDE-NEXT: ) + ;; INCLUDE-NEXT: (catch_all + ;; INCLUDE-NEXT: (nop) + ;; INCLUDE-NEXT: ) + ;; INCLUDE-NEXT: ) + ;; INCLUDE-NEXT: (try + ;; INCLUDE-NEXT: (do + ;; INCLUDE-NEXT: (call $return-call-ref-throw-and-catch) + ;; INCLUDE-NEXT: ) + ;; INCLUDE-NEXT: (catch_all + ;; INCLUDE-NEXT: (nop) + ;; INCLUDE-NEXT: ) + ;; INCLUDE-NEXT: ) + ;; INCLUDE-NEXT: (call $return-call-throw-and-catch) + ;; INCLUDE-NEXT: (call $return-call-indirect-throw-and-catch) + ;; INCLUDE-NEXT: (call $return-call-ref-throw-and-catch) + ;; INCLUDE-NEXT: ) + (func $call-return-call-throw-and-catch + (try + (do + ;; Even though the body of the previous function is a try-catch_all, the + ;; function still throws because of its return_call, so this cannot be + ;; optimized out, but once again the entire try-catch can be. + (call $return-call-throw-and-catch) + ) + (catch_all) + ) + (try + (do + ;; This would be the same, except since it performs an indirect call, we + ;; conservatively assume it could have any effect, so we can't optimize. + (call $return-call-indirect-throw-and-catch) + ) + (catch_all) + ) + (try + (do + ;; Same here. + (call $return-call-ref-throw-and-catch) + ) + (catch_all) + ) + + ;; These cannot be optimized out at all. + (call $return-call-throw-and-catch) + (call $return-call-indirect-throw-and-catch) + (call $return-call-ref-throw-and-catch) + ) + + ;; WITHOUT: (func $call-unreachable-and-catch (type $void) + ;; WITHOUT-NEXT: (try + ;; WITHOUT-NEXT: (do + ;; WITHOUT-NEXT: (call $unreachable) + ;; WITHOUT-NEXT: ) + ;; WITHOUT-NEXT: (catch_all + ;; WITHOUT-NEXT: (nop) + ;; WITHOUT-NEXT: ) + ;; WITHOUT-NEXT: ) + ;; WITHOUT-NEXT: ) + ;; INCLUDE: (func $call-unreachable-and-catch (type $void) + ;; INCLUDE-NEXT: (call $unreachable) + ;; INCLUDE-NEXT: ) + (func $call-unreachable-and-catch + (try + (do + ;; This call has a non-throw effect. We can optimize away the try-catch + ;; (since no exception can be thrown anyhow), but we must leave the + ;; call. + (call $unreachable) + ) + (catch_all) + ) + ) + + ;; WITHOUT: (func $call-throw-or-unreachable-and-catch (type $2) (param $x i32) + ;; WITHOUT-NEXT: (try + ;; WITHOUT-NEXT: (do + ;; WITHOUT-NEXT: (if + ;; WITHOUT-NEXT: (local.get $x) + ;; WITHOUT-NEXT: (then + ;; WITHOUT-NEXT: (call $throw) + ;; WITHOUT-NEXT: ) + ;; WITHOUT-NEXT: (else + ;; WITHOUT-NEXT: (call $unreachable) + ;; WITHOUT-NEXT: ) + ;; WITHOUT-NEXT: ) + ;; WITHOUT-NEXT: ) + ;; WITHOUT-NEXT: (catch_all + ;; WITHOUT-NEXT: (nop) + ;; WITHOUT-NEXT: ) + ;; WITHOUT-NEXT: ) + ;; WITHOUT-NEXT: ) + ;; INCLUDE: (func $call-throw-or-unreachable-and-catch (type $2) (param $x i32) + ;; INCLUDE-NEXT: (try + ;; INCLUDE-NEXT: (do + ;; INCLUDE-NEXT: (if + ;; INCLUDE-NEXT: (local.get $x) + ;; INCLUDE-NEXT: (then + ;; INCLUDE-NEXT: (call $throw) + ;; INCLUDE-NEXT: ) + ;; INCLUDE-NEXT: (else + ;; INCLUDE-NEXT: (call $unreachable) + ;; INCLUDE-NEXT: ) + ;; INCLUDE-NEXT: ) + ;; INCLUDE-NEXT: ) + ;; INCLUDE-NEXT: (catch_all + ;; INCLUDE-NEXT: (nop) + ;; INCLUDE-NEXT: ) + ;; INCLUDE-NEXT: ) + ;; INCLUDE-NEXT: ) + (func $call-throw-or-unreachable-and-catch (param $x i32) + ;; This try-catch-all's body will either call a throw or an unreachable. + ;; Since we have both possible effects, we cannot optimize anything here. + (try + (do + (if + (local.get $x) + (then + (call $throw) + ) + (else + (call $unreachable) + ) + ) + ) + (catch_all) + ) + ) + + ;; WITHOUT: (func $throw (type $void) + ;; WITHOUT-NEXT: (throw $tag) + ;; WITHOUT-NEXT: ) + ;; INCLUDE: (func $throw (type $void) + ;; INCLUDE-NEXT: (throw $tag) + ;; INCLUDE-NEXT: ) + (func $throw + (throw $tag) + ) + + ;; WITHOUT: (func $throw-and-import (type $void) + ;; WITHOUT-NEXT: (throw $tag) + ;; WITHOUT-NEXT: ) + ;; INCLUDE: (func $throw-and-import (type $void) + ;; INCLUDE-NEXT: (throw $tag) + ;; INCLUDE-NEXT: ) + (func $throw-and-import + (if + (i32.const 1) + (then + (throw $tag) + ) + (else + (call $import) + ) + ) + ) + + ;; WITHOUT: (func $cycle-with-unknown-call (type $void) + ;; WITHOUT-NEXT: (call $cycle-with-unknown-call) + ;; WITHOUT-NEXT: (call $import) + ;; WITHOUT-NEXT: ) + ;; INCLUDE: (func $cycle-with-unknown-call (type $void) + ;; INCLUDE-NEXT: (call $cycle-with-unknown-call) + ;; INCLUDE-NEXT: (call $import) + ;; INCLUDE-NEXT: ) + (func $cycle-with-unknown-call + ;; This function can not only call itself recursively, but also calls an + ;; import. We should not remove anything here, and not error during the + ;; analysis (this guards against a bug where the import would make us toss + ;; away the effects object, and the infinite loop makes us set a property on + ;; that object, so it must check the object still exists). + (call $cycle-with-unknown-call) + (call $import) + ) +) diff --git a/test/lit/passes/global-effects.wast b/test/lit/passes/global-effects.wast index 7390f996d..c3c6d2073 100644 --- a/test/lit/passes/global-effects.wast +++ b/test/lit/passes/global-effects.wast @@ -182,49 +182,44 @@ ) ;; WITHOUT: (func $call-throw-and-catch (type $void) - ;; WITHOUT-NEXT: (try - ;; WITHOUT-NEXT: (do + ;; WITHOUT-NEXT: (block $tryend + ;; WITHOUT-NEXT: (try_table (catch_all $tryend) ;; WITHOUT-NEXT: (call $throw) ;; WITHOUT-NEXT: ) - ;; WITHOUT-NEXT: (catch_all - ;; WITHOUT-NEXT: (nop) - ;; WITHOUT-NEXT: ) ;; WITHOUT-NEXT: ) - ;; WITHOUT-NEXT: (try - ;; WITHOUT-NEXT: (do + ;; WITHOUT-NEXT: (block $tryend0 + ;; WITHOUT-NEXT: (try_table (catch_all $tryend0) ;; WITHOUT-NEXT: (call $throw-and-import) ;; WITHOUT-NEXT: ) - ;; WITHOUT-NEXT: (catch_all - ;; WITHOUT-NEXT: (nop) - ;; WITHOUT-NEXT: ) ;; WITHOUT-NEXT: ) ;; WITHOUT-NEXT: ) ;; INCLUDE: (func $call-throw-and-catch (type $void) - ;; INCLUDE-NEXT: (try - ;; INCLUDE-NEXT: (do - ;; INCLUDE-NEXT: (call $throw-and-import) + ;; INCLUDE-NEXT: (block $tryend + ;; INCLUDE-NEXT: (try_table (catch_all $tryend) + ;; INCLUDE-NEXT: (call $throw) ;; INCLUDE-NEXT: ) - ;; INCLUDE-NEXT: (catch_all - ;; INCLUDE-NEXT: (nop) + ;; INCLUDE-NEXT: ) + ;; INCLUDE-NEXT: (block $tryend0 + ;; INCLUDE-NEXT: (try_table (catch_all $tryend0) + ;; INCLUDE-NEXT: (call $throw-and-import) ;; INCLUDE-NEXT: ) ;; INCLUDE-NEXT: ) ;; INCLUDE-NEXT: ) (func $call-throw-and-catch - (try - (do + (block $tryend + (try_table (catch_all $tryend) ;; This call cannot be optimized out, as the target throws. However, the - ;; entire try-catch can be, since the call's only effect is to throw, - ;; and the catch_all catches that. + ;; entire try_table could be, since the call's only effect is to throw, + ;; and the catch_all catches that. We do this for `try` but not yet for + ;; `try_table`. (call $throw) ) - (catch_all) ) - (try - (do + (block $tryend + (try_table (catch_all $tryend) ;; This call both throws and calls an import, and cannot be removed. (call $throw-and-import) ) - (catch_all) ) ) @@ -235,15 +230,14 @@ ;; INCLUDE-NEXT: (return_call $throw) ;; INCLUDE-NEXT: ) (func $return-call-throw-and-catch - (try - (do + (block $tryend + (try_table (catch_all $tryend) ;; This call cannot be optimized out, as the target throws. However, the - ;; surrounding try-catch can be removed even without global effects + ;; surrounding try_table can be removed even without global effects ;; because the throw from the return_call is never observed by this - ;; try-catch. + ;; try_table. (return_call $throw) ) - (catch_all) ) ) @@ -258,17 +252,16 @@ ;; INCLUDE-NEXT: ) ;; INCLUDE-NEXT: ) (func $return-call-indirect-throw-and-catch - (try - (do + (block $tryend + (try_table (catch_all $tryend) ;; This call cannot be optimized out, as the target may throw. However, - ;; the surrounding try-catch can be removed even without global effects + ;; the surrounding try_table can be removed even without global effects ;; because the throw from the return_call is never observed by this ;; try-catch. (return_call_indirect (i32.const 0) ) ) - (catch_all) ) ) @@ -283,94 +276,81 @@ ;; INCLUDE-NEXT: ) ;; INCLUDE-NEXT: ) (func $return-call-ref-throw-and-catch - (try - (do + (block $tryend + (try_table (catch_all $tryend) ;; This call cannot be optimized out, as the target may throw. However, - ;; the surrounding try-catch can be removed even without global effects + ;; the surrounding try_table can be removed even without global effects ;; because the throw from the return_call is never observed by this ;; try-catch. (return_call_ref $void (ref.func $throw) ) ) - (catch_all) ) ) ;; WITHOUT: (func $call-return-call-throw-and-catch (type $void) - ;; WITHOUT-NEXT: (try - ;; WITHOUT-NEXT: (do + ;; WITHOUT-NEXT: (block $tryend + ;; WITHOUT-NEXT: (try_table (catch_all $tryend) ;; WITHOUT-NEXT: (call $return-call-throw-and-catch) ;; WITHOUT-NEXT: ) - ;; WITHOUT-NEXT: (catch_all - ;; WITHOUT-NEXT: (nop) - ;; WITHOUT-NEXT: ) ;; WITHOUT-NEXT: ) - ;; WITHOUT-NEXT: (try - ;; WITHOUT-NEXT: (do + ;; WITHOUT-NEXT: (block $tryend0 + ;; WITHOUT-NEXT: (try_table (catch_all $tryend0) ;; WITHOUT-NEXT: (call $return-call-indirect-throw-and-catch) ;; WITHOUT-NEXT: ) - ;; WITHOUT-NEXT: (catch_all - ;; WITHOUT-NEXT: (nop) - ;; WITHOUT-NEXT: ) ;; WITHOUT-NEXT: ) - ;; WITHOUT-NEXT: (try - ;; WITHOUT-NEXT: (do + ;; WITHOUT-NEXT: (block $tryend1 + ;; WITHOUT-NEXT: (try_table (catch_all $tryend1) ;; WITHOUT-NEXT: (call $return-call-ref-throw-and-catch) ;; WITHOUT-NEXT: ) - ;; WITHOUT-NEXT: (catch_all - ;; WITHOUT-NEXT: (nop) - ;; WITHOUT-NEXT: ) ;; WITHOUT-NEXT: ) ;; WITHOUT-NEXT: (call $return-call-throw-and-catch) ;; WITHOUT-NEXT: (call $return-call-indirect-throw-and-catch) ;; WITHOUT-NEXT: (call $return-call-ref-throw-and-catch) ;; WITHOUT-NEXT: ) ;; INCLUDE: (func $call-return-call-throw-and-catch (type $void) - ;; INCLUDE-NEXT: (try - ;; INCLUDE-NEXT: (do - ;; INCLUDE-NEXT: (call $return-call-indirect-throw-and-catch) + ;; INCLUDE-NEXT: (block $tryend + ;; INCLUDE-NEXT: (try_table (catch_all $tryend) + ;; INCLUDE-NEXT: (call $return-call-throw-and-catch) ;; INCLUDE-NEXT: ) - ;; INCLUDE-NEXT: (catch_all - ;; INCLUDE-NEXT: (nop) + ;; INCLUDE-NEXT: ) + ;; INCLUDE-NEXT: (block $tryend0 + ;; INCLUDE-NEXT: (try_table (catch_all $tryend0) + ;; INCLUDE-NEXT: (call $return-call-indirect-throw-and-catch) ;; INCLUDE-NEXT: ) ;; INCLUDE-NEXT: ) - ;; INCLUDE-NEXT: (try - ;; INCLUDE-NEXT: (do + ;; INCLUDE-NEXT: (block $tryend1 + ;; INCLUDE-NEXT: (try_table (catch_all $tryend1) ;; INCLUDE-NEXT: (call $return-call-ref-throw-and-catch) ;; INCLUDE-NEXT: ) - ;; INCLUDE-NEXT: (catch_all - ;; INCLUDE-NEXT: (nop) - ;; INCLUDE-NEXT: ) ;; INCLUDE-NEXT: ) ;; INCLUDE-NEXT: (call $return-call-throw-and-catch) ;; INCLUDE-NEXT: (call $return-call-indirect-throw-and-catch) ;; INCLUDE-NEXT: (call $return-call-ref-throw-and-catch) ;; INCLUDE-NEXT: ) (func $call-return-call-throw-and-catch - (try - (do - ;; Even though the body of the previous function is a try-catch_all, the + (block $tryend + (try_table (catch_all $tryend) + ;; Even though the body of the previous function has a catch_all, the ;; function still throws because of its return_call, so this cannot be - ;; optimized out, but once again the entire try-catch can be. + ;; optimized out, but once again the entire try_table could be. Again, + ;; this is something we do for `try` for not yet for `try_table`. (call $return-call-throw-and-catch) ) - (catch_all) ) - (try - (do + (block $tryend + (try_table (catch_all $tryend) ;; This would be the same, except since it performs an indirect call, we ;; conservatively assume it could have any effect, so we can't optimize. (call $return-call-indirect-throw-and-catch) ) - (catch_all) ) - (try - (do + (block $tryend + (try_table (catch_all $tryend) ;; Same here. (call $return-call-ref-throw-and-catch) ) - (catch_all) ) ;; These cannot be optimized out at all. @@ -380,33 +360,29 @@ ) ;; WITHOUT: (func $call-unreachable-and-catch (type $void) - ;; WITHOUT-NEXT: (try - ;; WITHOUT-NEXT: (do + ;; WITHOUT-NEXT: (block $tryend + ;; WITHOUT-NEXT: (try_table (catch_all $tryend) ;; WITHOUT-NEXT: (call $unreachable) ;; WITHOUT-NEXT: ) - ;; WITHOUT-NEXT: (catch_all - ;; WITHOUT-NEXT: (nop) - ;; WITHOUT-NEXT: ) ;; WITHOUT-NEXT: ) ;; WITHOUT-NEXT: ) ;; INCLUDE: (func $call-unreachable-and-catch (type $void) ;; INCLUDE-NEXT: (call $unreachable) ;; INCLUDE-NEXT: ) (func $call-unreachable-and-catch - (try - (do + (block $tryend + (try_table (catch_all $tryend) ;; This call has a non-throw effect. We can optimize away the try-catch ;; (since no exception can be thrown anyhow), but we must leave the ;; call. (call $unreachable) ) - (catch_all) ) ) ;; WITHOUT: (func $call-throw-or-unreachable-and-catch (type $2) (param $x i32) - ;; WITHOUT-NEXT: (try - ;; WITHOUT-NEXT: (do + ;; WITHOUT-NEXT: (block $tryend + ;; WITHOUT-NEXT: (try_table (catch_all $tryend) ;; WITHOUT-NEXT: (if ;; WITHOUT-NEXT: (local.get $x) ;; WITHOUT-NEXT: (then @@ -417,14 +393,11 @@ ;; WITHOUT-NEXT: ) ;; WITHOUT-NEXT: ) ;; WITHOUT-NEXT: ) - ;; WITHOUT-NEXT: (catch_all - ;; WITHOUT-NEXT: (nop) - ;; WITHOUT-NEXT: ) ;; WITHOUT-NEXT: ) ;; WITHOUT-NEXT: ) ;; INCLUDE: (func $call-throw-or-unreachable-and-catch (type $2) (param $x i32) - ;; INCLUDE-NEXT: (try - ;; INCLUDE-NEXT: (do + ;; INCLUDE-NEXT: (block $tryend + ;; INCLUDE-NEXT: (try_table (catch_all $tryend) ;; INCLUDE-NEXT: (if ;; INCLUDE-NEXT: (local.get $x) ;; INCLUDE-NEXT: (then @@ -435,16 +408,13 @@ ;; INCLUDE-NEXT: ) ;; INCLUDE-NEXT: ) ;; INCLUDE-NEXT: ) - ;; INCLUDE-NEXT: (catch_all - ;; INCLUDE-NEXT: (nop) - ;; INCLUDE-NEXT: ) ;; INCLUDE-NEXT: ) ;; INCLUDE-NEXT: ) (func $call-throw-or-unreachable-and-catch (param $x i32) - ;; This try-catch-all's body will either call a throw or an unreachable. + ;; This try_table's body will either call a throw or an unreachable. ;; Since we have both possible effects, we cannot optimize anything here. - (try - (do + (block $tryend + (try_table (catch_all $tryend) (if (local.get $x) (then @@ -455,7 +425,6 @@ ) ) ) - (catch_all) ) ) diff --git a/test/lit/passes/gufa-eh.wast b/test/lit/passes/gufa-eh.wast new file mode 100644 index 000000000..79265a318 --- /dev/null +++ b/test/lit/passes/gufa-eh.wast @@ -0,0 +1,60 @@ +;; NOTE: Assertions have been generated by update_lit_checks.py --all-items and should not be edited. + +;; RUN: foreach %s %t wasm-opt -all --gufa -S -o - | filecheck %s + +(module + ;; CHECK: (type $0 (func (param i32))) + + ;; CHECK: (type $1 (func (result i32))) + + ;; CHECK: (type $2 (func)) + + ;; CHECK: (tag $e (param i32)) + (tag $e (param i32)) + + ;; CHECK: (func $try_table-target-block-is-not-unreachable (type $1) (result i32) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (block $catch (result i32) + ;; CHECK-NEXT: (try_table (catch $e $catch) + ;; CHECK-NEXT: (throw $e + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + (func $try_table-target-block-is-not-unreachable (result i32) + ;; Ensure that try_table connects caught tags with their branch targets. + (block $catch (result i32) + (try_table (catch $e $catch) + (throw $e (i32.const 0)) + ) + ) + ) + + ;; CHECK: (func $try_table-materializes-exnref (type $2) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (block $catch (result exnref) + ;; CHECK-NEXT: (try_table (catch_all_ref $catch) + ;; CHECK-NEXT: (throw $e + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $try_table-materializes-exnref + ;; Ensure that catch_all_ref materializes a non-null exnref value. If we do + ;; not connect a non-null exnref value to the branch target, GUFA will think + ;; no value can possibly get out of that block, and will insert an + ;; unreachable instruction after the block. + (drop + (block $catch (result exnref) + (try_table (catch_all_ref $catch) + (throw $e (i32.const 0)) + ) + ) + ) + ) +) diff --git a/test/lit/passes/local-subtyping.wast b/test/lit/passes/local-subtyping.wast index 582c24f23..2985102e1 100644 --- a/test/lit/passes/local-subtyping.wast +++ b/test/lit/passes/local-subtyping.wast @@ -24,6 +24,9 @@ ;; CHECK: (import "out" "i64" (func $i64 (type $6) (result i64))) (import "out" "i64" (func $i64 (result i64))) + ;; CHECK: (tag $e-anyref (param anyref)) + (tag $e-anyref (param anyref)) + ;; Refinalization can find a more specific type, where the declared type was ;; not the optimal LUB. ;; CHECK: (func $refinalize (type $2) (param $x i32) @@ -80,7 +83,7 @@ ;; A simple case where a local has a single assignment that we can use as a ;; more specific type. A similar thing with a parameter, however, is not a ;; thing we can optimize. Also, ignore a local with zero assignments. - ;; CHECK: (func $simple-local-but-not-param (type $7) (param $x funcref) + ;; CHECK: (func $simple-local-but-not-param (type $8) (param $x funcref) ;; CHECK-NEXT: (local $y (ref $1)) ;; CHECK-NEXT: (local $unused funcref) ;; CHECK-NEXT: (local.set $x @@ -101,7 +104,7 @@ ) ) - ;; CHECK: (func $locals-with-multiple-assignments (type $8) (param $struct structref) + ;; CHECK: (func $locals-with-multiple-assignments (type $9) (param $struct structref) ;; CHECK-NEXT: (local $x eqref) ;; CHECK-NEXT: (local $y (ref i31)) ;; CHECK-NEXT: (local $z structref) @@ -568,4 +571,49 @@ (local.get $x) ) ) + + ;; CHECK: (func $try_table-catch-result (type $0) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (block $catch (result anyref) + ;; CHECK-NEXT: (try_table (catch $e-anyref $catch) + ;; CHECK-NEXT: (nop) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (ref.null none) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $try_table-catch-result + (drop + ;; Must not be refined to (result nullref). + (block $catch (result anyref) + (try_table (catch $e-anyref $catch) + (nop) + ) + (ref.null none) + ) + ) + ) + + ;; CHECK: (func $try_table-ref (type $0) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (block $catch (result exnref) + ;; CHECK-NEXT: (try_table (catch_all_ref $catch) + ;; CHECK-NEXT: (nop) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (ref.null noexn) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $try_table-ref + (drop + ;; Must not be refined to nullexnref. + ;; An exnref comes from the catch_all_ref. + (block $catch (result exnref) + (try_table (catch_all_ref $catch) + (nop) + ) + (ref.null exn) + ) + ) + ) ) diff --git a/test/lit/passes/simplify-locals-eh-legacy.wast b/test/lit/passes/simplify-locals-eh-legacy.wast index 7b48707d4..d7fb75776 100644 --- a/test/lit/passes/simplify-locals-eh-legacy.wast +++ b/test/lit/passes/simplify-locals-eh-legacy.wast @@ -4,7 +4,7 @@ (module ;; CHECK: (tag $e-i32 (param i32)) (tag $e-i32 (param i32)) - ;; CHECK: (func $foo (type $2) (param $0 i32) (param $1 i32) + ;; CHECK: (func $foo (type $3) (param $0 i32) (param $1 i32) ;; CHECK-NEXT: (nop) ;; CHECK-NEXT: ) (func $foo (param i32 i32)) @@ -83,7 +83,7 @@ ) ) - ;; CHECK: (func $bar (type $3) (result i32) + ;; CHECK: (func $bar (type $1) (result i32) ;; CHECK-NEXT: (i32.const 3) ;; CHECK-NEXT: ) (func $bar (result i32) (i32.const 3)) @@ -152,7 +152,7 @@ ) ) - ;; CHECK: (func $return-call-can-be-sinked-into-try (type $3) (result i32) + ;; CHECK: (func $return-call-can-be-sinked-into-try (type $1) (result i32) ;; CHECK-NEXT: (local $0 i32) ;; CHECK-NEXT: (nop) ;; CHECK-NEXT: (try (result i32) @@ -202,103 +202,4 @@ ) ) ) - - ;; CHECK: (func $equivalent-set-removal-call (type $1) (param $0 i32) - ;; CHECK-NEXT: (local $1 i32) - ;; CHECK-NEXT: (nop) - ;; CHECK-NEXT: (drop - ;; CHECK-NEXT: (local.get $0) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: (nop) - ;; CHECK-NEXT: (call $equivalent-set-removal-call - ;; CHECK-NEXT: (i32.const 2) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: (drop - ;; CHECK-NEXT: (local.get $0) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: (drop - ;; CHECK-NEXT: (local.get $0) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: ) - (func $equivalent-set-removal-call (param $0 i32) - (local $1 i32) - (local.set $1 (local.get $0)) - (drop (local.get $0)) - (drop (local.get $1)) - ;; Even with EH enabled we can look past the call and optimize the final 1 - ;; to a 0, since they contain the same (and while the call might branch, - ;; such a branch does not cause a problem here, as if we branch we just - ;; don't reach the change later down). - (call $equivalent-set-removal-call - (i32.const 2) - ) - (drop (local.get $0)) - (drop (local.get $1)) - ) - - ;; CHECK: (func $equivalent-set-removal-if (type $2) (param $p i32) (param $0 i32) - ;; CHECK-NEXT: (local $1 i32) - ;; CHECK-NEXT: (nop) - ;; CHECK-NEXT: (drop - ;; CHECK-NEXT: (local.get $0) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: (local.set $1 - ;; CHECK-NEXT: (local.get $0) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: (if - ;; CHECK-NEXT: (local.get $p) - ;; CHECK-NEXT: (then - ;; CHECK-NEXT: (drop - ;; CHECK-NEXT: (local.get $0) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: (drop - ;; CHECK-NEXT: (local.get $0) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: (else - ;; CHECK-NEXT: (drop - ;; CHECK-NEXT: (local.get $0) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: (drop - ;; CHECK-NEXT: (local.get $1) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: (drop - ;; CHECK-NEXT: (local.get $0) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: (drop - ;; CHECK-NEXT: (local.get $1) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: ) - (func $equivalent-set-removal-if (param $p i32) (param $0 i32) - (local $1 i32) - (local.set $1 (local.get $0)) - (drop (local.get $0)) - ;; This local.get of 1 can be of 0. - (drop (local.get $1)) - (if - (local.get $p) - (then - (block - ;; We also optimize in this block, which is adjacent to the code before - ;; us. It is valid to optimize the 1 to a 0 here, as it is dominated by - ;; the code earlier. - (drop (local.get $0)) - (drop (local.get $1)) - ) - ) - (else - (block - ;; We could also optimize here, but atm just look at code adjacent to - ;; its dominator. TODO - (drop (local.get $0)) - (drop (local.get $1)) - ) - ) - ) - ;; As in the else, this could be optimized. TODO - (drop (local.get $0)) - (drop (local.get $1)) - ) ) diff --git a/test/lit/passes/simplify-locals-eh.wast b/test/lit/passes/simplify-locals-eh.wast new file mode 100644 index 000000000..f455bd4e5 --- /dev/null +++ b/test/lit/passes/simplify-locals-eh.wast @@ -0,0 +1,226 @@ +;; NOTE: Assertions have been generated by update_lit_checks.py and should not be edited. +;; RUN: wasm-opt %s --simplify-locals -all -S -o - | filecheck %s + +(module + ;; CHECK: (tag $e-i32 (param i32)) + (tag $e-i32 (param i32)) + + ;; CHECK: (func $bar (type $1) (result i32) + ;; CHECK-NEXT: (i32.const 3) + ;; CHECK-NEXT: ) + (func $bar (result i32) (i32.const 3)) + + ;; CHECK: (func $call-cannot-be-sinked-into-try_table (type $2) + ;; CHECK-NEXT: (local $0 i32) + ;; CHECK-NEXT: (local.set $0 + ;; CHECK-NEXT: (call $bar) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (block $tryend + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (block $catch (result i32) + ;; CHECK-NEXT: (try_table (catch $e-i32 $catch) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (local.get $0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (br $tryend) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $call-cannot-be-sinked-into-try_table (local $0 i32) + (drop + ;; This local.tee should NOT be sinked into 'try_table' below, because it + ;; may throw + (local.tee $0 (call $bar)) + ) + (block $tryend + (drop + (block $catch (result i32) + (try_table (catch $e-i32 $catch) + (drop (local.get $0)) + ) + (br $tryend) + ) + ) + ) + ) + + ;; CHECK: (func $non-call-can-be-sinked-into-try_table (type $2) + ;; CHECK-NEXT: (local $0 i32) + ;; CHECK-NEXT: (nop) + ;; CHECK-NEXT: (block $tryend + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (block $catch (result i32) + ;; CHECK-NEXT: (try_table (catch $e-i32 $catch) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (i32.const 3) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (br $tryend) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $non-call-can-be-sinked-into-try_table (local $0 i32) + (drop + ;; This local.tee can be sinked into 'try_table' below, because it cannot + ;; throw + (local.tee $0 (i32.const 3)) + ) + (block $tryend + (drop + (block $catch (result i32) + (try_table (catch $e-i32 $catch) + (drop (local.get $0)) + ) + (br $tryend) + ) + ) + ) + ) + + ;; CHECK: (func $return-call-can-be-sinked-into-try_table (type $1) (result i32) + ;; CHECK-NEXT: (local $0 i32) + ;; CHECK-NEXT: (nop) + ;; CHECK-NEXT: (block $tryend (result i32) + ;; CHECK-NEXT: (try_table (result i32) (catch $e-i32 $tryend) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (if (result i32) + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: (then + ;; CHECK-NEXT: (return_call $return-call-can-be-sinked-into-try_table) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (else + ;; CHECK-NEXT: (i32.const 1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $return-call-can-be-sinked-into-try_table (result i32) + (local $0 i32) + (drop + ;; This cannot throw either, so it can be sunk. Wrap the return_call in an + ;; if so the whole expression does not return unconditionally. + (local.tee $0 + (if (result i32) + (i32.const 0) + (then + (return_call $return-call-can-be-sinked-into-try_table) + ) + (else + (i32.const 1) + ) + ) + ) + ) + (block $tryend (result i32) + (try_table (result i32) (catch $e-i32 $tryend) + (drop (local.get $0)) + (i32.const 0) + ) + ) + ) + + ;; CHECK: (func $equivalent-set-removal-call (type $0) (param $0 i32) + ;; CHECK-NEXT: (local $1 i32) + ;; CHECK-NEXT: (nop) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (local.get $0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (nop) + ;; CHECK-NEXT: (call $equivalent-set-removal-call + ;; CHECK-NEXT: (i32.const 2) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (local.get $0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (local.get $0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $equivalent-set-removal-call (param $0 i32) + (local $1 i32) + (local.set $1 (local.get $0)) + (drop (local.get $0)) + (drop (local.get $1)) + ;; Even with EH enabled we can look past the call and optimize the final 1 + ;; to a 0, since they contain the same (and while the call might branch, + ;; such a branch does not cause a problem here, as if we branch we just + ;; don't reach the change later down). + (call $equivalent-set-removal-call + (i32.const 2) + ) + (drop (local.get $0)) + (drop (local.get $1)) + ) + + ;; CHECK: (func $equivalent-set-removal-if (type $3) (param $p i32) (param $0 i32) + ;; CHECK-NEXT: (local $1 i32) + ;; CHECK-NEXT: (nop) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (local.get $0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.set $1 + ;; CHECK-NEXT: (local.get $0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (if + ;; CHECK-NEXT: (local.get $p) + ;; CHECK-NEXT: (then + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (local.get $0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (local.get $0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (else + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (local.get $0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (local.get $1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (local.get $0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (local.get $1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $equivalent-set-removal-if (param $p i32) (param $0 i32) + (local $1 i32) + (local.set $1 (local.get $0)) + (drop (local.get $0)) + ;; This local.get of 1 can be of 0. + (drop (local.get $1)) + (if + (local.get $p) + (then + (block + ;; We also optimize in this block, which is adjacent to the code before + ;; us. It is valid to optimize the 1 to a 0 here, as it is dominated by + ;; the code earlier. + (drop (local.get $0)) + (drop (local.get $1)) + ) + ) + (else + (block + ;; We could also optimize here, but atm just look at code adjacent to + ;; its dominator. TODO + (drop (local.get $0)) + (drop (local.get $1)) + ) + ) + ) + ;; As in the else, this could be optimized. TODO + (drop (local.get $0)) + (drop (local.get $1)) + ) +) diff --git a/test/lit/passes/vacuum-eh-legacy.wast b/test/lit/passes/vacuum-eh-legacy.wast index 125475e26..0eb53bb56 100644 --- a/test/lit/passes/vacuum-eh-legacy.wast +++ b/test/lit/passes/vacuum-eh-legacy.wast @@ -6,14 +6,13 @@ (type $void (func)) ;; CHECK: (table $t 0 funcref) + (table $t 0 funcref) ;; CHECK: (tag $e (param i32)) (tag $e (param i32)) ;; CHECK: (tag $e2 (param i32)) (tag $e2 (param i32)) - (table $t 0 funcref) - ;; CHECK: (func $try-test (type $void) ;; CHECK-NEXT: (nop) ;; CHECK-NEXT: ) diff --git a/test/lit/passes/vacuum-eh.wast b/test/lit/passes/vacuum-eh.wast new file mode 100644 index 000000000..ec62f3c58 --- /dev/null +++ b/test/lit/passes/vacuum-eh.wast @@ -0,0 +1,235 @@ +;; NOTE: Assertions have been generated by update_lit_checks.py and should not be edited. +;; RUN: wasm-opt %s --vacuum -all -S -o - | filecheck %s + +(module + ;; CHECK: (type $void (func)) + (type $void (func)) + + ;; CHECK: (table $t 0 funcref) + (table $t 0 funcref) + + ;; CHECK: (tag $e (param i32)) + (tag $e (param i32)) + ;; CHECK: (tag $e2 (param i32)) + (tag $e2 (param i32)) + + ;; CHECK: (func $try_table-test (type $void) + ;; CHECK-NEXT: (nop) + ;; CHECK-NEXT: ) + (func $try_table-test + ;; When try_table body does not throw, the try_table can be replaced with + ;; its body + (block $tryend + (drop + (block $catch (result i32) + (try_table (catch $e $catch) + (drop (i32.const 0)) + ) + (br $tryend) + ) + ) + ) + ) + + ;; CHECK: (func $inner-try_table-catch_all-test (type $2) (result i32) + ;; CHECK-NEXT: (local $0 i32) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (block + ;; CHECK-NEXT: (block $inner-catch + ;; CHECK-NEXT: (try_table (catch_all $inner-catch) + ;; CHECK-NEXT: (throw $e + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (return + ;; CHECK-NEXT: (i32.const 1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (i32.const 2) + ;; CHECK-NEXT: ) + (func $inner-try_table-catch_all-test (result i32) + (local $0 i32) + ;; The exception thrown in the inner try_table is caught by the inner + ;; catch_all, so the outer try_table body does not throw and the outer + ;; try_table can be removed + (block $outer-tryend + (drop + (block $outer-catch (result i32) + (try_table (catch $e $outer-catch) + (block $inner-catch + (try_table (catch_all $inner-catch) + (throw $e (i32.const 0)) + ) + (unreachable) + ) + (return (i32.const 1)) + ) + (br $outer-tryend) + ) + ) + ) + (i32.const 2) + ) + + ;; CHECK: (func $inner-try_table-catch-test (type $void) + ;; CHECK-NEXT: (local $0 i32) + ;; CHECK-NEXT: (block $outer-tryend + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (block $outer-catch (result i32) + ;; CHECK-NEXT: (try_table (catch $e $outer-catch) + ;; CHECK-NEXT: (block $inner-tryend + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (block $inner-catch (result i32) + ;; CHECK-NEXT: (try_table (catch $e $inner-catch) + ;; CHECK-NEXT: (throw $e2 + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.set $0 + ;; CHECK-NEXT: (i32.const 1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (br $outer-tryend) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $inner-try_table-catch-test (local $0 i32) + ;; The exception thrown in the inner try_table will not be caught by the + ;; inner catch, so the outer try_table cannot be removed. + (block $outer-tryend + (drop + (block $outer-catch (result i32) + (try_table (catch $e $outer-catch) + (block $inner-tryend + (drop + (block $inner-catch (result i32) + (try_table (catch $e $inner-catch) + (throw $e2 (i32.const 0)) + ) + (br $inner-tryend) + ) + ) + (local.set $0 (i32.const 1)) + ) + ) + (br $outer-tryend) + ) + ) + ) + ) + + ;; CHECK: (func $trivial-catch-all-of-throw (type $void) + ;; CHECK-NEXT: (local $0 i32) + ;; CHECK-NEXT: (block $catch + ;; CHECK-NEXT: (try_table (catch_all $catch) + ;; CHECK-NEXT: (throw $e + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (block $catch0 + ;; CHECK-NEXT: (try_table (catch_all $catch0) + ;; CHECK-NEXT: (if + ;; CHECK-NEXT: (local.get $0) + ;; CHECK-NEXT: (then + ;; CHECK-NEXT: (throw $e + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (else + ;; CHECK-NEXT: (unreachable) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $trivial-catch-all-of-throw (local $0 i32) + ;; This try_table's body throws, but the catch_all catches it, so the entire + ;; try_table could be optimized out. We do this for `try` but not yet for + ;; `try_table`. + (block $catch + (try_table (catch_all $catch) + (throw $e (i32.const 0)) + ) + ) + ;; Here we also have a possible trap, so we can't do it. + (block $catch + (try_table (catch_all $catch) + (if + (local.get $0) + (then + (throw $e (i32.const 0)) + ) + (else + (unreachable) + ) + ) + ) + ) + ) + + ;; CHECK: (func $throw (type $void) + ;; CHECK-NEXT: (throw $e + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $throw + ;; Helper for the tail call tests below. + (throw $e + (i32.const 0) + ) + ) + + ;; CHECK: (func $return-call-catch (type $void) + ;; CHECK-NEXT: (return_call $throw) + ;; CHECK-NEXT: ) + (func $return-call-catch + (block $catch + (try_table (catch_all $catch) + ;; This returns before it throws, so we can optimize out the surrounding + ;; try_table. + (return_call $throw) + ) + ) + ) + + ;; CHECK: (func $return-call-indirect-catch (type $void) + ;; CHECK-NEXT: (return_call_indirect $t (type $void) + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $return-call-indirect-catch + (block $catch + (try_table (catch_all $catch) + ;; This returns before it throws, so we can optimize out the surrounding + ;; try_table. + (return_call_indirect + (i32.const 0) + ) + ) + ) + ) + + ;; CHECK: (func $return-call-ref-catch (type $void) + ;; CHECK-NEXT: (return_call_ref $void + ;; CHECK-NEXT: (ref.func $throw) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $return-call-ref-catch + (block $catch + (try_table (catch_all $catch) + ;; This returns before it throws, so we can optimize out the surrounding + ;; try_table. + (return_call_ref $void + (ref.func $throw) + ) + ) + ) + ) +) diff --git a/test/spec/exception-handling.wast b/test/spec/exception-handling.wast index 95350b77b..c6da12eef 100644 --- a/test/spec/exception-handling.wast +++ b/test/spec/exception-handling.wast @@ -1,3 +1,153 @@ +(module + (tag $e-v) + (tag $e-i32 (param i32)) + (tag $e-f32 (param f32)) + (tag $e-i32-f32 (param i32 f32)) + + (func $throw_single_value (export "throw_single_value") + (throw $e-i32 (i32.const 5)) + ) + + (func (export "throw_multiple_values") + (throw $e-i32-f32 (i32.const 3) (f32.const 3.5)) + ) + + (func (export "try_table_nothrow") (result i32) + (block $tryend (result i32) + (drop + (block $catch (result i32) + (br $tryend + (try_table (result i32) (catch $e-i32 $catch) + (i32.const 3) + ) + ) + ) + ) + (i32.const 0) + ) + ) + + (func (export "try_table_throw_catch") (result i32) + (block $tryend (result i32) + (drop + (block $catch (result i32) + (br $tryend + (try_table (result i32) (catch $e-i32 $catch) + (throw $e-i32 (i32.const 5)) + ) + ) + ) + ) + (i32.const 3) + ) + ) + + (func (export "try_table_throw_nocatch") (result i32) + (block $tryend (result i32) + (drop + (block $catch (result f32) + (br $tryend + (try_table (result i32) (catch $e-f32 $catch) + (throw $e-i32 (i32.const 5)) + ) + ) + ) + ) + (i32.const 3) + ) + ) + + (func (export "try_table_throw_catchall") (result i32) + (block $tryend (result i32) + (block $catch-all + (drop + (block $catch-f32 (result f32) + (br $tryend + (try_table (result i32) (catch $e-f32 $catch-f32) (catch_all $catch-all) + (throw $e-i32 (i32.const 5)) + ) + ) + ) + ) + (br $tryend (i32.const 4)) + ) + (i32.const 3) + ) + ) + + (func (export "try_table_call_catch") (result i32) + (block $tryend (result i32) + (block $catch (result i32) + (br $tryend + (try_table (result i32) (catch $e-i32 $catch) + (call $throw_single_value) + (unreachable) + ) + ) + ) + ) + ) + + (func (export "try_table_throw_multivalue_catch") (result i32) (local $x (tuple i32 f32)) + (block $tryend (result i32) + (local.set $x + (block $catch (result i32) (result f32) + (br $tryend + (try_table (result i32) (catch $e-i32-f32 $catch) + (throw $e-i32-f32 (i32.const 5) (f32.const 1.5)) + ) + ) + ) + ) + (tuple.extract 2 0 + (local.get $x) + ) + ) + ) + + (func (export "try_table_throw_throw_ref") (local $x (tuple i32 exnref)) + (block $tryend + (local.set $x + (block $catch (result i32) (result exnref) + (try_table (catch_ref $e-i32 $catch) + (throw $e-i32 (i32.const 5)) + ) + (br $tryend) + ) + ) + (throw_ref + (tuple.extract 2 1 + (local.get $x) + ) + ) + ) + ) + + (func (export "try_table_call_throw_ref") + (block $tryend + (throw_ref + (block $catch (result exnref) + (try_table (catch_all_ref $catch) + (call $throw_single_value) + ) + (br $tryend) + ) + ) + ) + ) +) + +(assert_exception (invoke "throw_single_value")) +(assert_exception (invoke "throw_multiple_values")) +(assert_return (invoke "try_table_nothrow") (i32.const 3)) +(assert_return (invoke "try_table_throw_catch") (i32.const 3)) +(assert_exception (invoke "try_table_throw_nocatch")) +(assert_return (invoke "try_table_throw_catchall") (i32.const 3)) +(assert_return (invoke "try_table_call_catch") (i32.const 5)) +(assert_return (invoke "try_table_throw_multivalue_catch") (i32.const 5)) +(assert_exception (invoke "try_table_throw_throw_ref")) +(assert_exception (invoke "try_table_call_throw_ref")) + (assert_invalid (module (tag $e-i32 (param i32)) diff --git a/test/spec/return_call_eh-legacy.wast b/test/spec/return_call_eh-legacy.wast new file mode 100644 index 000000000..8b0b54ee0 --- /dev/null +++ b/test/spec/return_call_eh-legacy.wast @@ -0,0 +1,35 @@ +;; Test the combination of 'return_call' with legacy exception handling. + +(module + (tag $t) + + (func $test (export "test") (result i32) + (try (result i32) + (do + (call $return-call-in-try) + ) + (catch_all + ;; Catch the exception thrown from $return-callee. + (i32.const 42) + ) + ) + + ) + + (func $return-call-in-try (result i32) + (try (result i32) + (do + (return_call $return-callee) + ) + (catch_all + (unreachable) + ) + ) + ) + + (func $return-callee (result i32) + (throw $t) + ) +) + +(assert_return (invoke "test") (i32.const 42)) diff --git a/test/spec/return_call_eh.wast b/test/spec/return_call_eh.wast index e85fdf7c6..11950c4b4 100644 --- a/test/spec/return_call_eh.wast +++ b/test/spec/return_call_eh.wast @@ -1,34 +1,31 @@ -;; Test the combination of 'return_call' with exception handling +;; Test the combination of 'return_call' with exception handling. (module (tag $t) (func $test (export "test") (result i32) - (try (result i32) - (do - (call $return-call-in-try) - ) - (catch_all - ;; Catch the exception thrown from $return-callee - (i32.const 42) + (block $catch + (return + (try_table (result i32) (catch_all $catch) + (call $return-call-in-try_table) + ) ) ) - + ;; Catch the exception thrown from $return-callee. + (i32.const 42) ) - (func $return-call-in-try (result i32) - (try (result i32) - (do + (func $return-call-in-try_table (result i32) + (block $catch + (try_table (catch_all $catch) (return_call $return-callee) ) - (catch_all - (unreachable) - ) ) + (unreachable) ) (func $return-callee (result i32) - (throw $t) + (throw $t) ) ) |