diff --git a/src/passes/Asyncify.cpp b/src/passes/Asyncify.cpp index 3ea9ca6b40c..b28eb2ee119 100644 --- a/src/passes/Asyncify.cpp +++ b/src/passes/Asyncify.cpp @@ -351,6 +351,10 @@ static const Name START_REWIND = "start_rewind"; static const Name STOP_REWIND = "stop_rewind"; static const Name ASYNCIFY_GET_CALL_INDEX = "__asyncify_get_call_index"; static const Name ASYNCIFY_CHECK_CALL_INDEX = "__asyncify_check_call_index"; +static const Name ASYNCIFY_REF_TABLE = "__asyncify_ref_table"; +static const Name ASYNCIFY_REF_BITMAP_TABLE = "__asyncify_ref_bitmap_table"; +static const Name ASYNCIFY_REF_LOAD_AND_CLEAR = "__asyncify_ref_load_and_clear"; +static const Name ASYNCIFY_REF_STORE = "__asyncify_ref_store"; // TODO: having just normal/unwind_or_rewind would decrease code // size, but make debugging harder @@ -360,6 +364,8 @@ enum class DataOffset { BStackPos = 0, BStackEnd = 4, BStackEnd64 = 8 }; const auto STACK_ALIGN = 4; +const auto MIN_REF_TABLE_SIZE = 256; + // A helper class for managing fake global names. Creates the globals and // provides mappings for using them. // Fake globals are used to stash and then use return values from calls. We need @@ -834,6 +840,7 @@ class ModuleAnalyzer { FakeGlobalHelper fakeGlobals; bool verbose; + bool hasExceptionReferences = false; }; // Checks if something performs a call: either a direct or indirect call, @@ -848,6 +855,15 @@ static bool doesCall(Expression* curr) { return curr->is() || curr->is(); } +// Flat IR exception: local set with a block, we need to handle this separately +static bool isLocalSetWithBlock(Expression* curr) { + if (auto* set = curr->dynCast()) { + curr = set->value; + return curr->is(); + } + return false; +} + class AsyncifyBuilder : public Builder { public: Module& wasm; @@ -1072,6 +1088,7 @@ struct AsyncifyFlow : public Pass { i--; } } + block->finalize(block->type); results.push_back(block); continue; } else if (auto* iff = curr->dynCast()) { @@ -1133,6 +1150,47 @@ struct AsyncifyFlow : public Pass { results.pop_back(); results.push_back(loop); continue; + } else if (auto* try_ = curr->dynCast()) { + if (item.phase == Work::Scan) { + work.push_back(Work{curr, Work::Finish}); + work.push_back(Work{try_->body, Work::Scan}); + continue; + } + try_->body = results.back(); + results.pop_back(); + results.push_back(try_); + continue; + } else if (isLocalSetWithBlock(curr)) { // relaxed flat IR + auto* set = curr->dynCast(); + + if (item.phase == Work::Scan) { + work.push_back(Work{curr, Work::Finish}); + work.push_back(Work{set->value, Work::Scan}); + continue; + } + + auto* blockValue = results.back()->cast(); + results.pop_back(); + // If the block has a type, we need to ensure that when rewinding, + // we still produce a value of that type. + if (blockValue->type.isConcrete()) { + auto type = blockValue->type; + + // If the type is a reference type, we need to ensure it's nullable, + // since the optimization could mark it as non nullable. + if (type.isRef() && type.isNonNullable()) { + type = type.with(wasm::Nullable); + } + + blockValue->list.push_back( + builder->makeLocalGet(set->index, type)); + + blockValue->finalize(type); + } + set->value = blockValue; + + results.push_back(set); + continue; } else if (doesCall(curr)) { // We reach here only in Scan phase, but we in effect "Finish" calls // here as well. @@ -1494,7 +1552,7 @@ struct AsyncifyLocals : public WalkerPass> { if (!relevantLiveLocals.count(i)) { continue; } - total += getByteSize(func->getLocalType(i)); + total += getStoredByteSize(func->getLocalType(i)); } auto* block = builder->makeBlock(); block->list.push_back(builder->makeIncStackPos(-total)); @@ -1509,17 +1567,36 @@ struct AsyncifyLocals : public WalkerPass> { auto localType = func->getLocalType(i); SmallVector loads; for (const auto& type : localType) { - auto size = getByteSize(type); + auto size = getStoredByteSize(type); assert(size % STACK_ALIGN == 0); // TODO: higher alignment? - loads.push_back(builder->makeLoad( - size, - true, - offset, - STACK_ALIGN, - builder->makeLocalGet(tempIndex, builder->pointerType), - type, - asyncifyMemory)); + + if (type.hasByteSize()) { + loads.push_back(builder->makeLoad( + size, + true, + offset, + STACK_ALIGN, + builder->makeLocalGet(tempIndex, builder->pointerType), + type, + asyncifyMemory)); + } else { + analyzer->hasExceptionReferences = true; + + // we load the index and then use it to load the ref + Expression* tableIndex = builder->makeLoad( + size, + true, + offset, + STACK_ALIGN, + builder->makeLocalGet(tempIndex, builder->pointerType), + builder->pointerType, + asyncifyMemory); + + loads.push_back(builder->makeCall( + ASYNCIFY_REF_LOAD_AND_CLEAR, {tableIndex}, Type(HeapType::exn, Nullable))); + } + offset += size; } Expression* load; @@ -1554,21 +1631,38 @@ struct AsyncifyLocals : public WalkerPass> { auto localType = func->getLocalType(i); size_t j = 0; for (const auto& type : localType) { - auto size = getByteSize(type); + auto size = getStoredByteSize(type); Expression* localGet = builder->makeLocalGet(i, localType); if (localType.size() > 1) { localGet = builder->makeTupleExtract(localGet, j); } assert(size % STACK_ALIGN == 0); // TODO: higher alignment? - block->list.push_back(builder->makeStore( - size, - offset, - STACK_ALIGN, - builder->makeLocalGet(tempIndex, builder->pointerType), - localGet, - type, - asyncifyMemory)); + + if (type.hasByteSize()) { + block->list.push_back(builder->makeStore( + size, + offset, + STACK_ALIGN, + builder->makeLocalGet(tempIndex, builder->pointerType), + localGet, + type, + asyncifyMemory)); + } else { + analyzer->hasExceptionReferences = true; + + // the result is the tableIndex as pointerType + // store this into memory + block->list.push_back(builder->makeStore( + size, + offset, + STACK_ALIGN, + builder->makeLocalGet(tempIndex, builder->pointerType), + builder->makeCall(ASYNCIFY_REF_STORE, {localGet}, builder->pointerType), + builder->pointerType, + asyncifyMemory)); + } + offset += size; ++j; } @@ -1591,14 +1685,18 @@ struct AsyncifyLocals : public WalkerPass> { builder->makeIncStackPos(4)); } - unsigned getByteSize(Type type) { - if (!type.hasByteSize()) { - Fatal() << "Asyncify does not yet support non-number types, like " - "references (see " - "https://github.com/WebAssembly/binaryen/issues/3739)"; + unsigned getStoredByteSize(Type type) { + if (type.hasByteSize()) { + return type.getByteSize(); + } + if (type == Type(HeapType::exn, Nullable)) { + return builder->pointerType.getByteSize(); } - return type.getByteSize(); + Fatal() << "Asyncify does not yet support non-number types, like " + "references (see " + "https://github.com/WebAssembly/binaryen/issues/3739)"; } + }; } // anonymous namespace @@ -1731,7 +1829,7 @@ struct Asyncify : public Pass { // anything else. { PassUtils::FilteredPassRunner runner(module, instrumentedFuncs); - runner.add("flatten"); + runner.add("flatten-relaxed"); // Dce is useful here, since AsyncifyFlow makes control flow conditional, // which may make unreachable code look reachable. It also lets us ignore // unreachable code here. @@ -1787,6 +1885,14 @@ struct Asyncify : public Pass { // Finally, add function support (that should not have been seen by // the previous passes). addFunctions(module); + + if (analyzer.hasExceptionReferences) { + // Add tables for saving reference types + addRefTables(module); + + // And functions for saving/loading reference types + addRefFunctions(module); + } } private: @@ -1814,6 +1920,131 @@ struct Asyncify : public Pass { module->addGlobal(std::move(asyncifyData)); } + void addRefTables(Module* module) { + Builder builder(*module); + + auto ref_table = builder.makeTable( + ASYNCIFY_REF_TABLE, Type(HeapType::exn, Nullable), MIN_REF_TABLE_SIZE); + module->addTable(std::move(ref_table)); + + auto ref_bitmap_table = builder.makeTable( + ASYNCIFY_REF_BITMAP_TABLE, Type(HeapType::func, Nullable), MIN_REF_TABLE_SIZE); + module->addTable(std::move(ref_bitmap_table)); + } + + void addRefFunctions(Module* module) { + Builder builder(*module); + + { + // load and clear - load the reference at a given index + // then write ref.null to this index in the bitmap table + auto* body = builder.makeBlock(); + + Index tableIdx = 0; + + body->list.push_back( + builder.makeTableSet(ASYNCIFY_REF_BITMAP_TABLE, + builder.makeLocalGet(tableIdx, pointerType), + builder.makeRefNull(HeapType::func))); + + body->list.push_back(builder.makeReturn( + builder.makeTableGet(ASYNCIFY_REF_TABLE, + builder.makeLocalGet(tableIdx, pointerType), + Type(HeapType::exn, Nullable)))); + body->finalize(); + + module->addFunction( + builder.makeFunction(ASYNCIFY_REF_LOAD_AND_CLEAR, + {{"tableIdx", pointerType}}, + Signature(pointerType, Type(HeapType::exn, Nullable)), + {}, + body)); + } + + { + // store and mark - scan bitmap table to find a free index (a null ref) + // if the index is not found, grow the table + // write the value to the ref table, mark the slot in the bitmap table + + auto* body = builder.makeBlock(); + + Index value = 0, tableSize = 1, foundIndex = 2; + + body->list.push_back(builder.makeLocalSet( + tableSize, builder.makeTableSize(ASYNCIFY_REF_BITMAP_TABLE))); + body->list.push_back(builder.makeLocalSet( + foundIndex, builder.makeConst(Literal::makeFromInt64(0, pointerType)))); + + // foundIndex is 0 and we loop until we reach tableSize or the index + // exists then after the loop if foundIndex is not list.push_back(loop); + + // If no empty slot was found, grow the table + body->list.push_back(builder.makeIf( + builder.makeBinary(Abstract::getBinary(pointerType, Abstract::Eq), + builder.makeLocalGet(foundIndex, pointerType), + builder.makeLocalGet(tableSize, pointerType)), + builder.makeBlock( + {builder.makeDrop(builder.makeTableGrow( + ASYNCIFY_REF_TABLE, + builder.makeRefNull(HeapType::exn), + builder.makeConst(Literal::makeFromInt64(1, pointerType)))), + builder.makeDrop(builder.makeTableGrow( + ASYNCIFY_REF_BITMAP_TABLE, + builder.makeRefNull(HeapType::func), + builder.makeConst(Literal::makeFromInt64(1, pointerType))))}))); + + // Store the value in the ref table + body->list.push_back( + builder.makeTableSet(ASYNCIFY_REF_TABLE, + builder.makeLocalGet(foundIndex, pointerType), + builder.makeLocalGet(value, Type(HeapType::exn, Nullable)))); + + // Mark the slot in the bitmap table as occupied + auto* dummyFunc = module->getFunction(Name("asyncify_start_unwind")); + + body->list.push_back(builder.makeTableSet( + ASYNCIFY_REF_BITMAP_TABLE, + builder.makeLocalGet(foundIndex, pointerType), + builder.makeRefFunc(dummyFunc->name, dummyFunc->type))); + + body->list.push_back( + builder.makeReturn(builder.makeLocalGet(foundIndex, pointerType))); + body->finalize(); + + module->addFunction(builder.makeFunction( + ASYNCIFY_REF_STORE, + {{"value", Type(HeapType::exn, Nullable)}}, + Signature(Type(HeapType::exn, Nullable), pointerType), + {{"tableSize", pointerType}, {"foundIndex", pointerType}}, + body)); + } + } + void addFunctions(Module* module) { Builder builder(*module); auto makeFunction = [&](Name name, bool setData, State state) { diff --git a/src/passes/Flatten.cpp b/src/passes/Flatten.cpp index 1c2cfbcd536..22eb43b7193 100644 --- a/src/passes/Flatten.cpp +++ b/src/passes/Flatten.cpp @@ -74,17 +74,28 @@ struct Flatten // FIXME DWARF updating does not handle local changes yet. bool invalidatesDWARF() override { return true; } + // Whether we support exception handling via try_table. This requires + // relaxing the Flat IR contract by allowing blocks that are catch + // destinations to preserve their return values. + bool relaxed; + + Flatten(bool relaxed) : relaxed(relaxed) {} + std::unique_ptr create() override { - return std::make_unique(); + return std::make_unique(relaxed); } // For each expression, a bunch of expressions that should execute right // before it std::unordered_map> preludes; + // Break values are sent through a temp local std::unordered_map breakTemps; + // These blocks must preserve their return values. + std::unordered_set catchDestBlocks; + void visitExpression(Expression* curr) { std::vector ourPreludes; Builder builder(*getModule()); @@ -116,8 +127,11 @@ struct Flatten newList.push_back(item); } block->list.swap(newList); - // remove a block return value + auto type = block->type; + + bool preserveReturnValue = catchDestBlocks.count(block->name); + if (type.isConcrete()) { // if there is a temp index for breaking to the block, use that Index temp; @@ -127,20 +141,31 @@ struct Flatten } else { temp = builder.addVar(getFunction(), type); } - auto*& last = block->list.back(); - if (last->type.isConcrete()) { - last = builder.makeLocalSet(temp, last); + + if (preserveReturnValue) { + // prelude is just the local set. + ourPreludes.push_back(builder.makeLocalSet(temp, block)); + + // and we leave a get of the value. + replaceCurrent(builder.makeLocalGet(temp, type)); + } else { + // remove a block return value + auto*& last = block->list.back(); + if (last->type.isConcrete()) { + last = builder.makeLocalSet(temp, last); + } + // and we leave just a get of the value + auto* rep = builder.makeLocalGet(temp, type); + replaceCurrent(rep); + // the whole block is now a prelude + ourPreludes.push_back(block); } - block->finalize(Type::none); - // and we leave just a get of the value - auto* rep = builder.makeLocalGet(temp, type); - replaceCurrent(rep); - // the whole block is now a prelude - ourPreludes.push_back(block); } - // the block now has no return value, and may have become unreachable - block->finalize(Type::none); + if (!preserveReturnValue) { + // the block now has no return value, and may have become unreachable + block->finalize(Type::none); + } } else if (auto* iff = curr->dynCast()) { // condition preludes go before the entire if auto* rep = getPreludesWithExpression(iff->condition, iff); @@ -227,6 +252,28 @@ struct Flatten tryy->finalize(); replaceCurrent(rep); + } else if (relaxed && curr->dynCast()) { + auto* tryy = curr->dynCast(); + + // remove a try value + Expression* rep = tryy; + auto* originalBody = tryy->body; + + auto type = tryy->type; + if (type.isConcrete()) { + Index temp = builder.addVar(getFunction(), type); + if (tryy->body->type.isConcrete()) { + tryy->body = builder.makeLocalSet(temp, tryy->body); + } + // and we leave just a get of the value + rep = builder.makeLocalGet(temp, type); + // the whole try is now a prelude + ourPreludes.push_back(tryy); + } + tryy->body = getPreludesWithExpression(originalBody, tryy->body); + tryy->finalize(); + replaceCurrent(rep); + } else { WASM_UNREACHABLE("unexpected expr type"); } @@ -254,7 +301,10 @@ struct Flatten } } else if (auto* br = curr->dynCast()) { - if (br->value) { + // relaxed: if this break has a value and breaks into a catch + // destination, we also need to preserve the break value here + + if (br->value && !catchDestBlocks.count(br->name)) { auto type = br->value->type; if (type.isConcrete()) { // we are sending a value. use a local instead @@ -333,7 +383,7 @@ struct Flatten } } - if (curr->is() || curr->is()) { + if (curr->is() || (!relaxed && curr->is())) { Fatal() << "Unsupported instruction for Flatten: " << getExpressionName(curr); } @@ -368,6 +418,28 @@ struct Flatten } } + void doWalkFunction(Function* func) { + if (relaxed) { + // Find all the catch destination blocks. + struct CatchDestBlockScanner : public PostWalker { + std::unordered_set& catchDestBlocks; + CatchDestBlockScanner(std::unordered_set& catchDestBlocks) + : catchDestBlocks(catchDestBlocks) {} + + void visitTryTable(TryTable* curr) { + for (auto& cd : curr->catchDests) { + catchDestBlocks.insert(cd); + } + } + }; + + CatchDestBlockScanner(catchDestBlocks).walkFunction(func); + } + + Super::doWalkFunction(func); + } + + void visitFunction(Function* curr) { auto* originalBody = curr->body; // if the body is a block with a result, turn that into a return @@ -419,6 +491,7 @@ struct Flatten } }; -Pass* createFlattenPass() { return new Flatten(); } +Pass* createFlattenPass() { return new Flatten(false); } +Pass* createFlattenRelaxedPass() { return new Flatten(true); } } // namespace wasm diff --git a/src/passes/pass.cpp b/src/passes/pass.cpp index 8db59b8ac0f..b2f48f98b50 100644 --- a/src/passes/pass.cpp +++ b/src/passes/pass.cpp @@ -170,6 +170,10 @@ void PassRegistry::registerPasses() { createExtractFunctionIndexPass); registerPass( "flatten", "flattens out code, removing nesting", createFlattenPass); + registerPass( + "flatten-relaxed", + "flattens out code, removing nesting, and supports EH with try_table", + createFlattenRelaxedPass); registerPass("fpcast-emu", "emulates function pointer casts, allowing incorrect indirect " "calls to (sometimes) work", diff --git a/src/passes/passes.h b/src/passes/passes.h index 92dcd3e4eb4..9c2503d6cde 100644 --- a/src/passes/passes.h +++ b/src/passes/passes.h @@ -52,6 +52,7 @@ Pass* createEncloseWorldPass(); Pass* createExtractFunctionPass(); Pass* createExtractFunctionIndexPass(); Pass* createFlattenPass(); +Pass* createFlattenRelaxedPass(); Pass* createFuncCastEmulationPass(); Pass* createFullPrinterPass(); Pass* createFunctionMetricsPass(); diff --git a/test/lit/passes/flatten-eh-relaxed.wast b/test/lit/passes/flatten-eh-relaxed.wast new file mode 100644 index 00000000000..c302fdee530 --- /dev/null +++ b/test/lit/passes/flatten-eh-relaxed.wast @@ -0,0 +1,190 @@ +;; NOTE: Assertions have been generated by update_lit_checks.py and should not be edited. +;; RUN: wasm-opt %s -all --flatten-relaxed -S -o - | filecheck %s + +(module + ;; CHECK: (import "env" "test" (func $test (type $0))) + (import "env" "test" (func $test)) + ;; CHECK: (import "env" "f_i32" (func $f_i32 (type $1) (param i32))) + (import "env" "f_i32" (func $f_i32 (param i32))) + ;; CHECK: (import "env" "f_i32_exnref" (func $f_i32_exnref (type $3) (param i32 exnref))) + (import "env" "f_i32_exnref" (func $f_i32_exnref (param i32 exnref))) + ;; CHECK: (import "env" "f_exnref" (func $f_exnref (type $4) (param exnref))) + (import "env" "f_exnref" (func $f_exnref (param exnref))) + + ;; CHECK: (tag $my_tag (type $1) (param i32)) + (tag $my_tag (param i32)) + + ;; CHECK: (func $thrower (type $1) (param $p i32) + ;; CHECK-NEXT: (local $1 i32) + ;; CHECK-NEXT: (local.set $1 + ;; CHECK-NEXT: (local.get $p) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (throw $my_tag + ;; CHECK-NEXT: (local.get $1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (unreachable) + ;; CHECK-NEXT: ) + (func $thrower (param $p i32) + local.get $p + throw $my_tag + ) + + ;; CHECK: (func $test_catch (type $0) + ;; CHECK-NEXT: (local $0 i32) + ;; CHECK-NEXT: (local $1 i32) + ;; CHECK-NEXT: (block $outer_block + ;; CHECK-NEXT: (local.set $0 + ;; CHECK-NEXT: (block $catch_block (result i32) + ;; CHECK-NEXT: (try_table (catch $my_tag $catch_block) + ;; CHECK-NEXT: (call $thrower + ;; CHECK-NEXT: (i32.const 123) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (br $outer_block) + ;; CHECK-NEXT: (unreachable) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.set $1 + ;; CHECK-NEXT: (local.get $0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (call $f_i32 + ;; CHECK-NEXT: (local.get $1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $test_catch + (block $outer_block + (call $f_i32 + (block $catch_block (result i32) + (try_table + (catch $my_tag $catch_block) + (call $thrower (i32.const 123)) + ) + (br $outer_block) + ) + ) + ) + ) + + ;; CHECK: (func $test_catch_ref (type $0) + ;; CHECK-NEXT: (local $scratch (tuple i32 exnref)) + ;; CHECK-NEXT: (local $1 (tuple i32 exnref)) + ;; CHECK-NEXT: (local $2 (tuple i32 exnref)) + ;; CHECK-NEXT: (local $3 (tuple i32 exnref)) + ;; CHECK-NEXT: (local $4 i32) + ;; CHECK-NEXT: (local $5 (tuple i32 exnref)) + ;; CHECK-NEXT: (local $6 exnref) + ;; CHECK-NEXT: (block $outer_block + ;; CHECK-NEXT: (local.set $1 + ;; CHECK-NEXT: (block $catch_block (type $2) (result i32 exnref) + ;; CHECK-NEXT: (try_table (catch_ref $my_tag $catch_block) + ;; CHECK-NEXT: (call $thrower + ;; CHECK-NEXT: (i32.const 456) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (br $outer_block) + ;; CHECK-NEXT: (unreachable) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.set $2 + ;; CHECK-NEXT: (local.get $1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.set $scratch + ;; CHECK-NEXT: (local.get $2) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.set $3 + ;; CHECK-NEXT: (local.get $scratch) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.set $4 + ;; CHECK-NEXT: (tuple.extract 2 0 + ;; CHECK-NEXT: (local.get $3) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.set $5 + ;; CHECK-NEXT: (local.get $scratch) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.set $6 + ;; CHECK-NEXT: (tuple.extract 2 1 + ;; CHECK-NEXT: (local.get $5) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (call $f_i32_exnref + ;; CHECK-NEXT: (local.get $4) + ;; CHECK-NEXT: (local.get $6) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $test_catch_ref + (block $outer_block + (call $f_i32_exnref + (block $catch_block (result i32 exnref) + (try_table + (catch_ref $my_tag $catch_block) + (call $thrower (i32.const 456)) + ) + (br $outer_block) + ) + ) + ) + ) + + ;; CHECK: (func $test_catch_all_ref (type $0) + ;; CHECK-NEXT: (local $0 exnref) + ;; CHECK-NEXT: (local $1 exnref) + ;; CHECK-NEXT: (block $outer_block + ;; CHECK-NEXT: (local.set $0 + ;; CHECK-NEXT: (block $catch_block (result exnref) + ;; CHECK-NEXT: (try_table (catch_all_ref $catch_block) + ;; CHECK-NEXT: (call $test) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (br $outer_block) + ;; CHECK-NEXT: (unreachable) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.set $1 + ;; CHECK-NEXT: (local.get $0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (call $f_exnref + ;; CHECK-NEXT: (local.get $1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $test_catch_all_ref + (block $outer_block + (call $f_exnref + (block $catch_block (result exnref) + (try_table + (catch_all_ref $catch_block) + (call $test) + ) + (br $outer_block) + ) + ) + ) + ) + + ;; CHECK: (func $test_catch_all (type $0) + ;; CHECK-NEXT: (block $outer_block + ;; CHECK-NEXT: (block $catch_block + ;; CHECK-NEXT: (try_table (catch_all $catch_block) + ;; CHECK-NEXT: (call $test) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (br $outer_block) + ;; CHECK-NEXT: (unreachable) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (call $test) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $test_catch_all + (block $outer_block + (block $catch_block + (try_table + (catch_all $catch_block) + (call $test) + ) + (br $outer_block) + ) + (call $test) + ) + ) +) diff --git a/test/unit/input/asyncify-exception.wat b/test/unit/input/asyncify-exception.wat new file mode 100644 index 00000000000..a2d156afe50 --- /dev/null +++ b/test/unit/input/asyncify-exception.wat @@ -0,0 +1,77 @@ +(module + (type $i32_to_void (func (param i32))) + (type $exnref_to_void (func (param exnref))) + (type $void_to_void (func)) + + (import "env" "maybe_suspend" (func $maybe_suspend (param i32))) + (import "env" "log_caught_tag" (func $log_caught_tag (type $i32_to_void))) + (import "env" "log_caught_exnref" (func $log_caught_exnref (type $exnref_to_void))) + (import "env" "js_thrower" (func $js_thrower (type $void_to_void))) + + (tag $my_tag (param i32)) + + (memory (export "memory") 10) + + (func $suspend_and_maybe_throw (param $p i32) (param $should_throw i32) + (call $maybe_suspend (i32.const 999)) + (if (local.get $should_throw) (then (local.get $p) (throw $my_tag))) + ) + + (func (export "rethrow_for_js") (param $e exnref) + local.get $e + throw_ref + ) + + ;; Simple helper that just throws. + (func $thrower (param $p i32) + (throw $my_tag (local.get $p)) + ) + + ;; Wasm Tagged Exceptions + (func (export "test_wasm_exception") (param $p i32) (param $should_throw i32) (result i32) + (block $catch_handler (result i32) + (try_table + (catch $my_tag 0) + (call $suspend_and_maybe_throw (local.get $p) (local.get $should_throw)) + (return (i32.const -1)) + ) + (unreachable) + ) + (call $log_caught_tag (i32.const 123)) + (return) + ) + + ;; Foreign JS Exceptions, loading/saving exnrefs + (func (export "test_js_exception") (result i32) + (local $ex exnref) + (local.set $ex + (block $catch_handler (result exnref) + (try_table + (catch_all_ref 0) + (return (call $js_thrower (i32.const -1))) + ) + unreachable + ) + ) + + (call $maybe_suspend (i32.const 555)) + (call $log_caught_exnref (local.get $ex)) + (return (i32.const -2)) + ) + + ;; Suspend inside a catch handler + (func (export "test_suspend_in_catch") (param $p i32) (result i32) + (block $catch_handler (result i32) + (try_table + (catch $my_tag 0) + (call $thrower (local.get $p)) + (unreachable) + ) + (unreachable) + ) + (call $maybe_suspend (i32.const 777)) + + (i32.const 100) + (return (i32.add)) + ) +) \ No newline at end of file diff --git a/test/unit/input/asyncify.js b/test/unit/input/asyncify.js index 28e82d296dc..497bf66b3a6 100644 --- a/test/unit/input/asyncify.js +++ b/test/unit/input/asyncify.js @@ -222,7 +222,7 @@ function coroutineTests() { }, values: [], yield: function(value) { - console.log('yield reached', Runtime.rewinding, value); + console.log('yield reached', Runtime.rewinding, value); var coroutine = Runtime.active; if (Runtime.rewinding) { coroutine.stopRewind(); @@ -297,11 +297,125 @@ function stackOverflowAssertTests() { assert(fails == 4, 'all 4 should have failed'); } +function exceptionTests() { + console.log('\nexception tests\n\n'); + + // Get and compile the wasm. + var binary = fs.readFileSync('d.wasm'); + var module = new WebAssembly.Module(binary); + + var DATA_ADDR = 4; + var isSuspending = false; + var shouldSuspend = true; + + var imports = { + env: { + maybe_suspend: function() { + if (!shouldSuspend) { + return; + } + var state = exports.asyncify_get_state(); + if (state === 0 /* Normal */) { + exports.asyncify_start_unwind(DATA_ADDR); + isSuspending = true; + } else if (state === 2 /* Rewinding */) { + exports.asyncify_stop_rewind(); + } + }, + log_caught_tag: function(val) { + // Just a marker for the test - actual logging not needed for assertions + }, + log_caught_exnref: function(exn) { + try { + exports.rethrow_for_js(exn); + } catch (e) { + // Exception re-thrown and caught as expected + } + }, + js_thrower: function() { + throw new Error("Error from JavaScript!"); + } + } + }; + + var instance = new WebAssembly.Instance(module, imports); + var exports = instance.exports; + var view = new Int32Array(exports.memory.buffer); + + // Initialize asyncify stack + var ASYNCIFY_STACK_SIZE = 1024; + view[DATA_ADDR >> 2] = DATA_ADDR + 8; // Stack top + view[(DATA_ADDR + 4) >> 2] = DATA_ADDR + 8 + ASYNCIFY_STACK_SIZE; // Stack end + + function runExceptionTest(name, expectedResult, testFunc) { + console.log('\n==== testing ' + name + ' ===='); + + isSuspending = false; + var result = testFunc(); + + if (isSuspending) { + assert(!result, 'results during exception handling sleep are meaningless, just 0'); + exports.asyncify_stop_unwind(); + + exports.asyncify_start_rewind(DATA_ADDR); + result = testFunc(); + } + + console.log('final result: ' + result); + assert(result == expectedResult, 'bad final result for ' + name); + } + + var testValue = 123; + + // Test with suspension enabled + shouldSuspend = true; + + // Test 1: Async call WITH Wasm exception + runExceptionTest('wasm exception with suspend', testValue, function() { + return exports.test_wasm_exception(testValue, 1); + }); + + // Test 2: Async call WITHOUT Wasm exception + runExceptionTest('wasm exception without suspend', -1, function() { + return exports.test_wasm_exception(testValue, 0); + }); + + // Test 3: Sync call that catches a JS exception + runExceptionTest('js exception handling', -2, function() { + return exports.test_js_exception(); + }); + + // Test 4: Suspend inside a catch handler + runExceptionTest('suspend in catch handler', testValue + 100, function() { + return exports.test_suspend_in_catch(testValue); + }); + + // Test with suspension disabled + shouldSuspend = false; + + runExceptionTest('wasm exception no suspend', testValue, function() { + return exports.test_wasm_exception(testValue, 1); + }); + + runExceptionTest('wasm no exception no suspend', -1, function() { + return exports.test_wasm_exception(testValue, 0); + }); + + runExceptionTest('js exception no suspend', -2, function() { + return exports.test_js_exception(); + }); + + runExceptionTest('catch handler no suspend', testValue + 100, function() { + return exports.test_suspend_in_catch(testValue); + }); +} + // Main sleepTests(); coroutineTests(); stackOverflowAssertTests(); +exceptionTests(); console.log('\ntests completed successfully'); diff --git a/test/unit/test_asyncify.py b/test/unit/test_asyncify.py index 7425173e2ec..36854124970 100644 --- a/test/unit/test_asyncify.py +++ b/test/unit/test_asyncify.py @@ -13,9 +13,10 @@ def test(args): shared.run_process(shared.WASM_OPT + args + [self.input_path('asyncify-sleep.wat'), '--asyncify', '-o', 'a.wasm']) shared.run_process(shared.WASM_OPT + args + [self.input_path('asyncify-coroutine.wat'), '--asyncify', '-o', 'b.wasm']) shared.run_process(shared.WASM_OPT + args + [self.input_path('asyncify-stackOverflow.wat'), '--asyncify', '-o', 'c.wasm']) + shared.run_process(shared.WASM_OPT + args + [self.input_path('asyncify-exception.wat'), '--asyncify', '--enable-exception-handling', '--enable-reference-types', '-o', 'd.wasm']) print(' file size: %d' % os.path.getsize('a.wasm')) if shared.NODEJS: - shared.run_process([shared.NODEJS, self.input_path('asyncify.js')], stdout=subprocess.PIPE, stderr=subprocess.PIPE) + shared.run_process([shared.NODEJS, "--experimental-wasm-exnref", self.input_path('asyncify.js')], stdout=subprocess.PIPE, stderr=subprocess.PIPE) test(['-g']) test([])