From 71572a12027c8cabb5acf7797145839ee102e2d6 Mon Sep 17 00:00:00 2001 From: zb3 Date: Wed, 20 Aug 2025 02:27:28 +0200 Subject: [PATCH 1/4] Flatten: add relaxed mode supporting try_table This introduces a "relaxed" mode to the flatten pass, which allows it to process try_table expressions. In this mode we preserve blocks return values if those blocks are used as catch destinations and we also preserve breaks with values if they target these blocks. To make this useful for asyncify, blocks return values are still saved into locals. --- src/passes/Flatten.cpp | 105 +++++++++++-- src/passes/pass.cpp | 4 + src/passes/passes.h | 1 + test/lit/passes/flatten-eh-relaxed.wast | 190 ++++++++++++++++++++++++ 4 files changed, 284 insertions(+), 16 deletions(-) create mode 100644 test/lit/passes/flatten-eh-relaxed.wast 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) + ) + ) +) From af78742d539134453a7db5178a9eae56e9efe9c1 Mon Sep 17 00:00:00 2001 From: zb3 Date: Wed, 20 Aug 2025 02:53:04 +0200 Subject: [PATCH 2/4] Asyncify: add basic support for try_table This uses the new "relaxed" flat IR which supports try_table. To make it work with asyncify we add support for the new flat IR "local.set with a block" expression, where we need to add a dummy local.get at the end to make the catch block reachable when rewinding. --- src/passes/Asyncify.cpp | 46 ++++++++++++++++++++++++++++++++++++++++- 1 file changed, 45 insertions(+), 1 deletion(-) diff --git a/src/passes/Asyncify.cpp b/src/passes/Asyncify.cpp index 3ea9ca6b40c..4ccb207ae4a 100644 --- a/src/passes/Asyncify.cpp +++ b/src/passes/Asyncify.cpp @@ -848,6 +848,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 +1081,7 @@ struct AsyncifyFlow : public Pass { i--; } } + block->finalize(block->type); results.push_back(block); continue; } else if (auto* iff = curr->dynCast()) { @@ -1133,6 +1143,40 @@ 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; + 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. @@ -1731,7 +1775,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. From cf5162f5ee0cf2701c248ad0476ec63f0c371410 Mon Sep 17 00:00:00 2001 From: zb3 Date: Wed, 20 Aug 2025 03:10:10 +0200 Subject: [PATCH 3/4] Asyncify: add exnref support Asyncify saves and restores locals from memory, but since reference types can't be stored in memory, they need to be stored in tables. What gets saved to memory are their indices, allowing us to continue supporting multiple stacks. The trickier part is how to keep track of free slots in these tables, and while that could be done using extra memory, in order to not depend on multiple memories, a bitmap funcref table is utilized, where a non-null value signals the slot being in use. Notably this doesn't add support for neither externref nor anyref, they would need separate tables. --- src/passes/Asyncify.cpp | 237 +++++++++++++++++++++++++++++++++++----- 1 file changed, 212 insertions(+), 25 deletions(-) diff --git a/src/passes/Asyncify.cpp b/src/passes/Asyncify.cpp index 4ccb207ae4a..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, @@ -1168,6 +1175,13 @@ struct AsyncifyFlow : public Pass { // 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)); @@ -1538,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)); @@ -1553,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; @@ -1598,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; } @@ -1635,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 @@ -1831,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: @@ -1858,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) { From bcf71dc7fbcf4ce2ff764bc1dec45dac55e95142 Mon Sep 17 00:00:00 2001 From: zb3 Date: Wed, 20 Aug 2025 03:12:11 +0200 Subject: [PATCH 4/4] Add tests for exception handling support in asyncify --- test/unit/input/asyncify-exception.wat | 77 ++++++++++++++++ test/unit/input/asyncify.js | 116 ++++++++++++++++++++++++- test/unit/test_asyncify.py | 3 +- 3 files changed, 194 insertions(+), 2 deletions(-) create mode 100644 test/unit/input/asyncify-exception.wat 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([])