From 4d3c936439cd6fb454ee34744096b006999c5f27 Mon Sep 17 00:00:00 2001 From: NellowTCS Date: Fri, 25 Sep 2026 23:28:33 -0600 Subject: [PATCH 1/5] spill reference locals to growable tables --- src/passes/Asyncify.cpp | 312 ++++++++++++++---- test/lit/passes/asyncify-reference-types.wast | 43 +++ 2 files changed, 287 insertions(+), 68 deletions(-) create mode 100644 test/lit/passes/asyncify-reference-types.wast diff --git a/src/passes/Asyncify.cpp b/src/passes/Asyncify.cpp index feea9adf7c2..a36dc73e2aa 100644 --- a/src/passes/Asyncify.cpp +++ b/src/passes/Asyncify.cpp @@ -342,6 +342,9 @@ // of their original range. // +#include +#include + #include "asmjs/shared-constants.h" #include "cfg/liveness-traversal.h" #include "ir/effects.h" @@ -387,6 +390,18 @@ enum class DataOffset { BStackPos = 0, BStackEnd = 4, BStackEnd64 = 8 }; const auto STACK_ALIGN = 4; +// Reference spill tables start at the acyclic per-function sum and grow on +// unwind (for recursion); overflow traps on the out-of-bounds table.set. +const uint32_t REF_TABLE_MAX = 1 << 26; + +static Name refTableName(HeapType ht) { + return Name(std::string("__asyncify_ref_table_") + ht.toString()); +} + +static Name refCursorName(HeapType ht) { + return Name(std::string("__asyncify_ref_pos_") + ht.toString()); +} + // 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 @@ -875,6 +890,15 @@ static bool doesCall(Expression* curr) { return curr->is() || curr->is(); } +// Interned types (Type, HeapType) carry no ordering operators, but their +// canonical ids give a stable, deterministic order for map iteration. +struct HeapTypeIdLess { + bool operator()(HeapType a, HeapType b) const { + return a.getID() < b.getID(); + } +}; +using RefCountMap = std::map; + class AsyncifyBuilder : public Builder { public: Module& wasm; @@ -885,6 +909,51 @@ class AsyncifyBuilder : public Builder { : Builder(wasm), wasm(wasm), pointerType(pointerType), asyncifyMemory(asyncifyMemory) {} + Name getRefTable(HeapType heapType) { return refTableName(heapType); } + + Expression* makeGetRefPos(HeapType heapType) { + return makeGlobalGet(refCursorName(heapType), pointerType); + } + + // The table index for the `offset`th spilled slot of this heap type within + // the current frame, i.e. the cursor global advanced by the slot offset. + Expression* makeRefIndex(HeapType heapType, Index offset) { + return makeBinary(Abstract::getBinary(pointerType, Abstract::Add), + makeGetRefPos(heapType), + makeConst(Literal::makeFromInt64(offset, pointerType))); + } + + Expression* makeIncRefPos(HeapType heapType, int32_t by) { + if (by == 0) { + return makeNop(); + } + return makeGlobalSet( + refCursorName(heapType), + makeBinary(Abstract::getBinary(pointerType, Abstract::Add), + makeGetRefPos(heapType), + makeConst(Literal::makeFromInt64(by, pointerType)))); + } + + // Ensure the table has room for `count` more slots at the cursor, so that + // recursive callers (live on the stack more than once) still fit. + Expression* makeEnsureRefCapacity(HeapType heapType, uint32_t count) { + if (count == 0) { + return makeNop(); + } + auto table = getRefTable(heapType); + auto need = makeRefIndex(heapType, count); + auto deficit = makeBinary(Abstract::getBinary(pointerType, Abstract::Sub), + need, + makeTableSize(table)); + return makeIf(makeBinary(Abstract::getBinary(pointerType, Abstract::GtU), + need, + makeTableSize(table)), + makeDrop(makeTableGrow( + table, + LiteralUtils::makeZero(Type(heapType, Nullable), wasm), + deficit))); + } + Expression* makeGetStackPos() { return makeLoad(pointerType.getByteSize(), false, @@ -1443,6 +1512,40 @@ struct AsyncifyAssertUnwindCorrectness : Pass { }; // Instrument local saving/restoring. +// TODO: look more precisely inside basic blocks +std::set computeRelevantLiveLocals(Function* func, Module* module) { + struct RelevantLiveLocalsWalker + : public LivenessWalker> { + // Basic blocks that have a possible unwind/rewind in them. + std::set relevantBasicBlocks; + + void visitCall(Call* curr) { + if (!currBasicBlock) { + return; + } + // Blocks where we might unwind/rewind, each marked by a call to + // ASYNCIFY_CHECK_CALL_INDEX right before the real call. + if (curr->target == ASYNCIFY_CHECK_CALL_INDEX) { + relevantBasicBlocks.insert(currBasicBlock); + } + } + }; + + RelevantLiveLocalsWalker walker; + walker.setFunction(func); + walker.walkFunctionInModule(func, module); + std::set relevant; + for (auto* block : walker.liveBlocks) { + if (walker.relevantBasicBlocks.contains(block)) { + for (auto local : block->contents.start) { + relevant.insert(local); + } + } + } + return relevant; +} + struct AsyncifyLocals : public WalkerPass> { bool isFunctionParallel() override { return true; } @@ -1568,66 +1671,50 @@ struct AsyncifyLocals : public WalkerPass> { std::set relevantLiveLocals; void findRelevantLiveLocals(Function* func) { - struct RelevantLiveLocalsWalker - : public LivenessWalker> { - // Basic blocks that have a possible unwind/rewind in them. - std::set relevantBasicBlocks; + relevantLiveLocals = computeRelevantLiveLocals(func, getModule()); + } - void visitCall(Call* curr) { - if (!currBasicBlock) { - return; - } - // Note blocks where we might unwind/rewind, all of which have a - // possible call to ASYNCIFY_CHECK_CALL_INDEX emitted right before the - // actual call. - // Note that each relevant original call was turned into a sequence of - // instructions, one of which is an if and then a call to this special - // intrinsic. We rely on the fact that if a local was live at the - // original call, it also would be in all that sequence of instructions, - // and in particular at the call we look for here (which is right before - // the call, and so anything that has its final use at the call is still - // live here). - if (curr->target == ASYNCIFY_CHECK_CALL_INDEX) { - relevantBasicBlocks.insert(currBasicBlock); - } - } - }; + struct SpillLayout { + Index memTotal = 0; + RefCountMap refTotals; + }; - RelevantLiveLocalsWalker walker; - walker.setFunction(func); - walker.walkFunctionInModule(func, getModule()); - // The relevant live locals are ones that are alive at an unwind/rewind - // location. TODO look more precisely inside basic blocks, as one might stop - // being live in the middle - for (auto* block : walker.liveBlocks) { - if (walker.relevantBasicBlocks.contains(block)) { - for (auto local : block->contents.start) { - relevantLiveLocals.insert(local); + SpillLayout computeSpillLayout() { + SpillLayout layout; + auto* func = getFunction(); + for (Index i = 0; i < func->getNumLocals(); i++) { + if (!relevantLiveLocals.contains(i)) { + continue; + } + for (const auto& type : func->getLocalType(i)) { + if (type.isRef()) { + layout.refTotals[type.getHeapType()] += 1; + } else { + layout.memTotal += getByteSize(type); } } } + return layout; } Expression* makeLocalLoading() { if (relevantLiveLocals.empty()) { return builder->makeNop(); } + auto layout = computeSpillLayout(); auto* func = getFunction(); auto numLocals = func->getNumLocals(); - Index total = 0; - for (Index i = 0; i < numLocals; i++) { - if (!relevantLiveLocals.contains(i)) { - continue; - } - total += getByteSize(func->getLocalType(i)); - } auto* block = builder->makeBlock(); - block->list.push_back(builder->makeIncStackPos(-total)); + + block->list.push_back(builder->makeIncStackPos(-layout.memTotal)); + for (const auto& [heapType, count] : layout.refTotals) { + block->list.push_back(builder->makeIncRefPos(heapType, -int32_t(count))); + } auto tempIndex = builder->addVar(func, builder->pointerType); block->list.push_back( builder->makeLocalSet(tempIndex, builder->makeGetStackPos())); Index offset = 0; + RefCountMap refOffsets; for (Index i = 0; i < numLocals; i++) { if (!relevantLiveLocals.contains(i)) { continue; @@ -1635,18 +1722,33 @@ struct AsyncifyLocals : public WalkerPass> { auto localType = func->getLocalType(i); SmallVector loads; for (const auto& type : localType) { - auto size = getByteSize(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)); - offset += size; + if (type.isRef()) { + auto heapType = type.getHeapType(); + auto& refOffset = refOffsets[heapType]; + // The table is nullable, so a non-nullable local needs a cast back. + Expression* load = + builder->makeTableGet(builder->getRefTable(heapType), + builder->makeRefIndex(heapType, refOffset), + Type(heapType, Nullable)); + if (!type.isNullable()) { + load = builder->makeRefAs(RefAsNonNull, load); + } + loads.push_back(load); + refOffset += 1; + } else { + auto size = getByteSize(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)); + offset += size; + } } Expression* load; if (loads.size() == 1) { @@ -1666,13 +1768,19 @@ struct AsyncifyLocals : public WalkerPass> { if (relevantLiveLocals.empty()) { return builder->makeNop(); } + auto layout = computeSpillLayout(); auto* func = getFunction(); auto numLocals = func->getNumLocals(); auto* block = builder->makeBlock(); auto tempIndex = builder->addVar(func, builder->pointerType); block->list.push_back( builder->makeLocalSet(tempIndex, builder->makeGetStackPos())); + // Ensure room for this frame's reference spill before writing any of it + for (const auto& [heapType, total] : layout.refTotals) { + block->list.push_back(builder->makeEnsureRefCapacity(heapType, total)); + } Index offset = 0; + RefCountMap refOffsets; for (Index i = 0; i < numLocals; i++) { if (!relevantLiveLocals.contains(i)) { continue; @@ -1680,26 +1788,39 @@ struct AsyncifyLocals : public WalkerPass> { auto localType = func->getLocalType(i); size_t j = 0; for (const auto& type : localType) { - auto size = getByteSize(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)); - offset += size; + if (type.isRef()) { + auto heapType = type.getHeapType(); + auto& refOffset = refOffsets[heapType]; + block->list.push_back( + builder->makeTableSet(builder->getRefTable(heapType), + builder->makeRefIndex(heapType, refOffset), + localGet)); + refOffset += 1; + } else { + auto size = getByteSize(type); + 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)); + offset += size; + } ++j; } } block->list.push_back(builder->makeIncStackPos(offset)); + for (const auto& [heapType, total] : refOffsets) { + block->list.push_back(builder->makeIncRefPos(heapType, int32_t(total))); + } block->finalize(); return block; } @@ -1717,10 +1838,12 @@ struct AsyncifyLocals : public WalkerPass> { builder->makeIncStackPos(4)); } + // Only called for non-reference types unsigned getByteSize(Type type) { if (!type.hasByteSize()) { - Fatal() << "Asyncify does not yet support non-number types, like " - "references (see " + Fatal() << "Asyncify cannot spill a value of type " << type + << " into the linear-memory spill stack, as it has no linear " + "memory representation (see " "https://github.com/WebAssembly/binaryen/issues/3739)"; } return type.getByteSize(); @@ -1885,6 +2008,7 @@ struct Asyncify : public Pass { runner.setValidateGlobally(false); runner.run(); } + setupRefSpill(module, instrumentedFuncs); if (asserts) { // Add asserts in non-instrumented code. Note we do not use an // instrumented pass runner here as we do want to run on all functions. @@ -1956,6 +2080,44 @@ struct Asyncify : public Pass { } } + // Reference locals have no linear-memory representation, so they are spilled + // into pass-created tables + void setupRefSpill(Module* module, + const PassUtils::FuncSet& instrumentedFuncs) { + Builder builder(*module); + RefCountMap counts; + for (auto* func : instrumentedFuncs) { + if (!func->body) { + continue; + } + for (auto local : computeRelevantLiveLocals(func, module)) { + for (const auto& type : func->getLocalType(local)) { + if (type.isRef()) { + counts[type.getHeapType()] += 1; + } + } + } + } + for (const auto& [heapType, count] : counts) { + if (count == 0) { + continue; + } + + auto table = Builder::makeTable(refTableName(heapType), + Type(heapType, Nullable), + count, // initial size + REF_TABLE_MAX, // max size (growable) + pointerType, // address type + nullptr); // init: default null + module->addTable(std::move(table)); + module->addGlobal(builder.makeGlobal(refCursorName(heapType), + pointerType, + builder.makeConst(pointerType), + Builder::Mutable)); + refHeapTypes.insert(heapType); + } + } + void addFunctions(Module* module) { Builder builder(*module); auto makeFunction = [&](Name name, bool setData, State state) { @@ -1970,6 +2132,19 @@ struct Asyncify : public Pass { body->list.push_back(builder.makeGlobalSet( ASYNCIFY_DATA, builder.makeLocalGet(0, pointerType))); } + // Asyncify only supports one pause at a time. + if (name == ASYNCIFY_START_UNWIND) { + for (auto heapType : refHeapTypes) { + body->list.push_back(builder.makeIf( + builder.makeBinary( + Abstract::getBinary(pointerType, Abstract::Ne), + builder.makeGlobalGet(refCursorName(heapType), pointerType), + builder.makeConst(pointerType)), + builder.makeUnreachable())); + body->list.push_back(builder.makeGlobalSet( + refCursorName(heapType), builder.makeConst(pointerType))); + } + } // Verify the data is valid. auto* stackPos = builder.makeLoad(pointerType.getByteSize(), @@ -2023,6 +2198,7 @@ struct Asyncify : public Pass { Type pointerType; Name asyncifyMemory; + std::set refHeapTypes; }; Pass* createAsyncifyPass() { return new Asyncify(); } diff --git a/test/lit/passes/asyncify-reference-types.wast b/test/lit/passes/asyncify-reference-types.wast new file mode 100644 index 00000000000..18db15ff78f --- /dev/null +++ b/test/lit/passes/asyncify-reference-types.wast @@ -0,0 +1,43 @@ +;; Asyncify spills reference-typed locals (externref/funcref) into growable +;; per-heap-type tables. + +;; RUN: foreach %s %t wasm-opt --enable-reference-types --asyncify -S -o - | filecheck %s + +(module + (import "env" "unwind" (func $unwind)) + (memory 1) + (func (export "run") (param $r externref) (result i32) + (local $x i32) + (call $unwind) + (drop (ref.is_null (local.get $r))) + (local.get $x) + ) +) + +;; CHECK: (global $__asyncify_ref_pos_extern (mut i32) (i32.const 0)) +;; CHECK: (table $__asyncify_ref_table_extern 1 67108864 externref) + +;; On rewind, the externref local is restored from the table at the cursor. +;; CHECK: (global.set $__asyncify_ref_pos_extern +;; CHECK: (local.set $r +;; CHECK: (table.get $__asyncify_ref_table_extern +;; CHECK: (i32.add +;; CHECK: (global.get $__asyncify_ref_pos_extern) + +;; On unwind, the table is grown if the cursor has reached the current size, and +;; the local is spilled to the table. Growth is what makes recursion safe. +;; CHECK: (table.size $__asyncify_ref_table_extern) +;; CHECK: (table.grow $__asyncify_ref_table_extern +;; CHECK: (table.set $__asyncify_ref_table_extern +;; CHECK: (global.get $__asyncify_ref_pos_extern) + +;; The cursor is reset at the start of each unwind. +;; A nonzero cursor at the start of an unwind means two pauses overlap, which +;; asyncify does not support, so it traps before resetting the cursor. +;; CHECK: (func $asyncify_start_unwind +;; CHECK: (i32.ne +;; CHECK: (global.get $__asyncify_ref_pos_extern) +;; CHECK: (i32.const 0) +;; CHECK: (unreachable) +;; CHECK: (global.set $__asyncify_ref_pos_extern +;; CHECK: (i32.const 0) From 8612f9f6f6d7153fa20d153cc29cdb1a47cd1d56 Mon Sep 17 00:00:00 2001 From: NellowTCS Date: Thu, 1 Oct 2026 00:52:33 -0600 Subject: [PATCH 2/5] Extract ref-spill helpers and fix duplicate ref index in table growth --- src/passes/Asyncify.cpp | 146 +++++++++++++++++++++++----------------- 1 file changed, 86 insertions(+), 60 deletions(-) diff --git a/src/passes/Asyncify.cpp b/src/passes/Asyncify.cpp index a36dc73e2aa..989e7b29d31 100644 --- a/src/passes/Asyncify.cpp +++ b/src/passes/Asyncify.cpp @@ -941,13 +941,15 @@ class AsyncifyBuilder : public Builder { return makeNop(); } auto table = getRefTable(heapType); - auto need = makeRefIndex(heapType, count); + // Each operand gets its own copy: the IR is a tree, so an Expression* + // cannot be shared between the condition and the grow. auto deficit = makeBinary(Abstract::getBinary(pointerType, Abstract::Sub), - need, + makeRefIndex(heapType, count), makeTableSize(table)); - return makeIf(makeBinary(Abstract::getBinary(pointerType, Abstract::GtU), - need, - makeTableSize(table)), + auto full = makeBinary(Abstract::getBinary(pointerType, Abstract::GtU), + makeRefIndex(heapType, count), + makeTableSize(table)); + return makeIf(full, makeDrop(makeTableGrow( table, LiteralUtils::makeZero(Type(heapType, Nullable), wasm), @@ -1674,6 +1676,78 @@ struct AsyncifyLocals : public WalkerPass> { relevantLiveLocals = computeRelevantLiveLocals(func, getModule()); } + // Next slot for a local: a table slot for reference components, a byte + // range in linear memory for everything else + struct SpillCursors { + AsyncifyBuilder& builder; + RefCountMap refOffsets; + Index memOffset = 0; + + SpillCursors(AsyncifyBuilder& builder) : builder(builder) {} + + // Table slot for the next reference + Expression* nextRef(HeapType heapType) { + return builder.makeRefIndex(heapType, refOffsets[heapType]++); + } + + // Byte offset of the next non-reference component + Index takeMem(Index size) { + auto offset = memOffset; + memOffset += size; + return offset; + } + }; + + // Reads one component back from its spill slot + Expression* + makeComponentLoading(Type type, Index tempIndex, SpillCursors& cursors) { + if (type.isRef()) { + auto heapType = type.getHeapType(); + // The table is nullable, so a non-nullable local needs a cast back. + Expression* load = builder->makeTableGet(builder->getRefTable(heapType), + cursors.nextRef(heapType), + Type(heapType, Nullable)); + if (!type.isNullable()) { + load = builder->makeRefAs(RefAsNonNull, load); + } + return load; + } + auto size = getByteSize(type); + assert(size % STACK_ALIGN == 0); + // TODO: higher alignment? + return builder->makeLoad( + size, + true, + cursors.takeMem(size), + STACK_ALIGN, + builder->makeLocalGet(tempIndex, builder->pointerType), + type, + asyncifyMemory); + } + + // Writes one component to its spill slot + Expression* makeComponentSaving(Type type, + Expression* value, + Index tempIndex, + SpillCursors& cursors) { + if (type.isRef()) { + return builder->makeTableSet(builder->getRefTable(type.getHeapType()), + cursors.nextRef(type.getHeapType()), + value); + } + auto size = getByteSize(type); + assert(size % STACK_ALIGN == 0); + // TODO: higher alignment? + return builder->makeStore( + size, + cursors.takeMem(size), + STACK_ALIGN, + builder->makeLocalGet(tempIndex, builder->pointerType), + value, + type, + asyncifyMemory); + } + struct SpillLayout { Index memTotal = 0; RefCountMap refTotals; @@ -1713,8 +1787,7 @@ struct AsyncifyLocals : public WalkerPass> { auto tempIndex = builder->addVar(func, builder->pointerType); block->list.push_back( builder->makeLocalSet(tempIndex, builder->makeGetStackPos())); - Index offset = 0; - RefCountMap refOffsets; + SpillCursors cursors(*builder); for (Index i = 0; i < numLocals; i++) { if (!relevantLiveLocals.contains(i)) { continue; @@ -1722,33 +1795,7 @@ struct AsyncifyLocals : public WalkerPass> { auto localType = func->getLocalType(i); SmallVector loads; for (const auto& type : localType) { - if (type.isRef()) { - auto heapType = type.getHeapType(); - auto& refOffset = refOffsets[heapType]; - // The table is nullable, so a non-nullable local needs a cast back. - Expression* load = - builder->makeTableGet(builder->getRefTable(heapType), - builder->makeRefIndex(heapType, refOffset), - Type(heapType, Nullable)); - if (!type.isNullable()) { - load = builder->makeRefAs(RefAsNonNull, load); - } - loads.push_back(load); - refOffset += 1; - } else { - auto size = getByteSize(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)); - offset += size; - } + loads.push_back(makeComponentLoading(type, tempIndex, cursors)); } Expression* load; if (loads.size() == 1) { @@ -1779,8 +1826,7 @@ struct AsyncifyLocals : public WalkerPass> { for (const auto& [heapType, total] : layout.refTotals) { block->list.push_back(builder->makeEnsureRefCapacity(heapType, total)); } - Index offset = 0; - RefCountMap refOffsets; + SpillCursors cursors(*builder); for (Index i = 0; i < numLocals; i++) { if (!relevantLiveLocals.contains(i)) { continue; @@ -1792,33 +1838,13 @@ struct AsyncifyLocals : public WalkerPass> { if (localType.size() > 1) { localGet = builder->makeTupleExtract(localGet, j); } - if (type.isRef()) { - auto heapType = type.getHeapType(); - auto& refOffset = refOffsets[heapType]; - block->list.push_back( - builder->makeTableSet(builder->getRefTable(heapType), - builder->makeRefIndex(heapType, refOffset), - localGet)); - refOffset += 1; - } else { - auto size = getByteSize(type); - 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)); - offset += size; - } + block->list.push_back( + makeComponentSaving(type, localGet, tempIndex, cursors)); ++j; } } - block->list.push_back(builder->makeIncStackPos(offset)); - for (const auto& [heapType, total] : refOffsets) { + block->list.push_back(builder->makeIncStackPos(cursors.memOffset)); + for (const auto& [heapType, total] : cursors.refOffsets) { block->list.push_back(builder->makeIncRefPos(heapType, int32_t(total))); } block->finalize(); From 009621376a7d9648d006bf98955c174f31026dc3 Mon Sep 17 00:00:00 2001 From: NellowTCS Date: Thu, 1 Oct 2026 03:02:20 -0600 Subject: [PATCH 3/5] Clear restored ref slots after asyncify rewind --- src/passes/Asyncify.cpp | 15 +++++++++++++++ test/lit/passes/asyncify-reference-types.wast | 6 ++++++ 2 files changed, 21 insertions(+) diff --git a/src/passes/Asyncify.cpp b/src/passes/Asyncify.cpp index 989e7b29d31..164d1d58fc7 100644 --- a/src/passes/Asyncify.cpp +++ b/src/passes/Asyncify.cpp @@ -923,6 +923,14 @@ class AsyncifyBuilder : public Builder { makeConst(Literal::makeFromInt64(offset, pointerType))); } + // A spill slot is a GC root, so once a reference has been restored into its + // local the slot must be cleared to stop the table retaining the object. + Expression* makeClearRefSlot(HeapType heapType, Index offset) { + return makeTableSet(getRefTable(heapType), + makeRefIndex(heapType, offset), + LiteralUtils::makeZero(Type(heapType, Nullable), wasm)); + } + Expression* makeIncRefPos(HeapType heapType, int32_t by) { if (by == 0) { return makeNop(); @@ -1807,6 +1815,13 @@ struct AsyncifyLocals : public WalkerPass> { } block->list.push_back(builder->makeLocalSet(i, load)); } + // Release the slots just restored, so the table does not keep the + // references alive for the life of the instance. + for (const auto& [heapType, count] : layout.refTotals) { + for (Index offset = 0; offset < count; offset++) { + block->list.push_back(builder->makeClearRefSlot(heapType, offset)); + } + } block->finalize(); return block; } diff --git a/test/lit/passes/asyncify-reference-types.wast b/test/lit/passes/asyncify-reference-types.wast index 18db15ff78f..525b690158a 100644 --- a/test/lit/passes/asyncify-reference-types.wast +++ b/test/lit/passes/asyncify-reference-types.wast @@ -24,6 +24,12 @@ ;; CHECK: (i32.add ;; CHECK: (global.get $__asyncify_ref_pos_extern) +;; The restored slot is then cleared: a table is a GC root, so leaving the +;; reference there would retain the object for the life of the instance. +;; CHECK: (table.set $__asyncify_ref_table_extern +;; CHECK: (global.get $__asyncify_ref_pos_extern) +;; CHECK: (ref.null noextern) + ;; On unwind, the table is grown if the cursor has reached the current size, and ;; the local is spilled to the table. Growth is what makes recursion safe. ;; CHECK: (table.size $__asyncify_ref_table_extern) From 81d6095ec1f9695eff087bbb39abbc7d73844378 Mon Sep 17 00:00:00 2001 From: NellowTCS Date: Sun, 8 Nov 2026 22:16:26 -0700 Subject: [PATCH 4/5] Review comments --- src/ir/type-updating.cpp | 14 +- src/ir/type-updating.h | 10 +- src/passes/Asyncify.cpp | 258 ++- .../asyncify-reference-types-heap2local.wast | 292 +++ test/lit/passes/asyncify-reference-types.wast | 1703 ++++++++++++++++- 5 files changed, 2146 insertions(+), 131 deletions(-) create mode 100644 test/lit/passes/asyncify-reference-types-heap2local.wast diff --git a/src/ir/type-updating.cpp b/src/ir/type-updating.cpp index 423e7cd9791..6e41fb24e60 100644 --- a/src/ir/type-updating.cpp +++ b/src/ir/type-updating.cpp @@ -498,11 +498,22 @@ Type GlobalTypeRewriter::getTempTupleType(Tuple tuple) { namespace TypeUpdating { -void handleNonDefaultableLocals(Function* func, Module& wasm) { +void handleNonDefaultableLocals(Function* func, + Module& wasm, + const std::unordered_set* forced) { if (!wasm.features.hasReferenceTypes()) { // No references, so no non-nullable ones at all. return; } + std::unordered_set badIndexes; + if (forced) { + for (auto index : *forced) { + assert(!func->isParam(index)); + if (!func->getLocalType(index).isDefaultable()) { + badIndexes.insert(index); + } + } + } bool hasNonNullable = false; for (auto varType : func->vars) { for (auto type : varType) { @@ -522,7 +533,6 @@ void handleNonDefaultableLocals(Function* func, Module& wasm) { // the wasm spec. We do not need to modify such locals. LocalStructuralDominance info( func, wasm, LocalStructuralDominance::NonNullableOnly); - std::unordered_set badIndexes; for (auto index : info.nonDominatingIndices) { badIndexes.insert(index); diff --git a/src/ir/type-updating.h b/src/ir/type-updating.h index 7aaf02a8234..ab4e59b9ecf 100644 --- a/src/ir/type-updating.h +++ b/src/ir/type-updating.h @@ -17,6 +17,8 @@ #ifndef wasm_ir_type_updating_h #define wasm_ir_type_updating_h +#include + #include "ir/branch-utils.h" #include "ir/module-utils.h" #include "support/insert_ordered.h" @@ -564,7 +566,13 @@ namespace TypeUpdating { // them. Atm this turns them into nullable ones, and adds ref.as_non_null on // their uses (which keeps the type of the users identical). // This may also handle other types of nondefaultable locals in the future. -void handleNonDefaultableLocals(Function* func, Module& wasm); +// +// If `forced` is provided, the locals at those indexes are made nullable as +// well, even if they validate as they are. +void handleNonDefaultableLocals( + Function* func, + Module& wasm, + const std::unordered_set* forced = nullptr); // Returns the type that a local should be, after handling of non- // defaultability. diff --git a/src/passes/Asyncify.cpp b/src/passes/Asyncify.cpp index 79fdae3de4b..07cfc0c3fed 100644 --- a/src/passes/Asyncify.cpp +++ b/src/passes/Asyncify.cpp @@ -354,10 +354,12 @@ #include "ir/memory-utils.h" #include "ir/module-utils.h" #include "ir/names.h" +#include "ir/type-updating.h" #include "ir/utils.h" #include "pass.h" #include "passes/pass-utils.h" #include "support/file.h" +#include "support/insert_ordered.h" #include "support/string.h" #include "wasm-builder.h" #include "wasm.h" @@ -390,16 +392,36 @@ enum class DataOffset { BStackPos = 0, BStackEnd = 4, BStackEnd64 = 8 }; const auto STACK_ALIGN = 4; -// Reference spill tables start at the acyclic per-function sum and grow on -// unwind (for recursion); overflow traps on the out-of-bounds table.set. +// Reference locals are spilled into one table per heap type/exactness, with a +// global holding the current slot position (like the Asyncify stack pointer). +// The saved region grows on unwind and shrinks on rewind (which clears slots). +// Initial size covers live ref locals of that type; recursion grows as needed, +// and overflow traps on the out-of-bounds table.set. const uint32_t REF_TABLE_MAX = 1 << 26; -static Name refTableName(HeapType ht) { - return Name(std::string("__asyncify_ref_table_") + ht.toString()); +static Name refName(Module& module, Type type, const char* prefix) { + std::string name = prefix; + auto heapType = type.getHeapType(); + if (auto it = module.typeNames.find(heapType); + it != module.typeNames.end() && it->second.name.is()) { + name += it->second.name.toString(); + } else { + // Basic types and unnamed types have no name in the module + name += heapType.toString(); + } + if (type.isExact()) { + // Exact references need their own tables + name += "_exact"; + } + return Name(name); } -static Name refCursorName(HeapType ht) { - return Name(std::string("__asyncify_ref_pos_") + ht.toString()); +static Name refTableName(Module& module, Type type) { + return refName(module, type, "__asyncify_ref_table_"); +} + +static Name refCursorName(Module& module, Type type) { + return refName(module, type, "__asyncify_ref_pos_"); } // A helper class for managing fake global names. Creates the globals and @@ -413,29 +435,42 @@ static Name refCursorName(HeapType ht) { // i.e., we don't store to the target local if not running normally). class FakeGlobalHelper { Module& module; + std::unordered_set storageTypes; public: FakeGlobalHelper(Module& module) : module(module) { - Builder builder(module); - std::string prefix = "asyncify_fake_call_global_"; + // Store the types in their nullable versions so that makeZero can + // initialize the globals (non-defaultable types have no zero) for (auto type : collectTypes()) { - auto global = prefix + Type(type).toString(); - map[type] = global; - rev[global] = type; + storageTypes.insert(getStorageType(type)); + } + Builder builder(module); + for (auto type : storageTypes) { + auto name = getNameForStorageType(type); + rev[name] = type; module.addGlobal(builder.makeGlobal( - global, type, LiteralUtils::makeZero(type, module), Builder::Mutable)); + name, type, LiteralUtils::makeZero(type, module), Builder::Mutable)); } } ~FakeGlobalHelper() { - for (auto& pair : map) { - auto name = pair.second; - module.removeGlobal(name); + for (auto type : storageTypes) { + module.removeGlobal(getNameForStorageType(type)); } } - Name getName(Type type) { return map.at(type); } + // The name of the fake global used to stash a call result of this type + Name getName(Type type) { return getNameForStorageType(getStorageType(type)); } + + // The type a call result is stored as. + // Non-defaultable reference types are stored as their nullable versions, + // and readers cast back with ref.as_non_null + Type getStorageType(Type type) { + return TypeUpdating::getValidLocalType(type, module.features); + } + // The storage type of the fake global with this name, or none if this is not + // a fake global. Type getTypeOrNone(Name name) { auto iter = rev.find(name); if (iter != rev.end()) { @@ -445,9 +480,12 @@ class FakeGlobalHelper { } private: - std::unordered_map map; std::unordered_map rev; + Name getNameForStorageType(Type type) { + return Name(std::string("asyncify_fake_call_global_") + type.toString()); + } + // Collect the types returned from all calls for which call support globals // may need to be generated. using Types = std::unordered_set; @@ -887,14 +925,8 @@ static bool doesCall(Expression* curr) { return curr->is() || curr->is(); } -// Interned types (Type, HeapType) carry no ordering operators, but their -// canonical ids give a stable, deterministic order for map iteration. -struct HeapTypeIdLess { - bool operator()(HeapType a, HeapType b) const { - return a.getID() < b.getID(); - } -}; -using RefCountMap = std::map; +// Counts of spill slots per reference type +using RefCountMap = InsertOrderedMap; class AsyncifyBuilder : public Builder { public: @@ -906,59 +938,58 @@ class AsyncifyBuilder : public Builder { : Builder(wasm), wasm(wasm), pointerType(pointerType), asyncifyMemory(asyncifyMemory) {} - Name getRefTable(HeapType heapType) { return refTableName(heapType); } + Name getRefTable(Type type) { return refTableName(wasm, type); } - Expression* makeGetRefPos(HeapType heapType) { - return makeGlobalGet(refCursorName(heapType), pointerType); + Expression* makeGetRefPos(Type type) { + return makeGlobalGet(refCursorName(wasm, type), pointerType); } - // The table index for the `offset`th spilled slot of this heap type within - // the current frame, i.e. the cursor global advanced by the slot offset. - Expression* makeRefIndex(HeapType heapType, Index offset) { + // The table index for the `offset`th spilled slot of this reference type + // within the current frame, i.e. the cursor global advanced by the slot + // offset. + Expression* makeRefIndex(Type type, Index offset) { return makeBinary(Abstract::getBinary(pointerType, Abstract::Add), - makeGetRefPos(heapType), + makeGetRefPos(type), makeConst(Literal::makeFromInt64(offset, pointerType))); } // A spill slot is a GC root, so once a reference has been restored into its // local the slot must be cleared to stop the table retaining the object. - Expression* makeClearRefSlot(HeapType heapType, Index offset) { - return makeTableSet(getRefTable(heapType), - makeRefIndex(heapType, offset), - LiteralUtils::makeZero(Type(heapType, Nullable), wasm)); + Expression* makeClearRefSlot(Type type, Index offset) { + return makeTableSet( + getRefTable(type), makeRefIndex(type, offset), + LiteralUtils::makeZero(type, wasm)); } - Expression* makeIncRefPos(HeapType heapType, int32_t by) { + Expression* makeIncRefPos(Type type, int32_t by) { if (by == 0) { return makeNop(); } return makeGlobalSet( - refCursorName(heapType), + refCursorName(wasm, type), makeBinary(Abstract::getBinary(pointerType, Abstract::Add), - makeGetRefPos(heapType), + makeGetRefPos(type), makeConst(Literal::makeFromInt64(by, pointerType)))); } // Ensure the table has room for `count` more slots at the cursor, so that // recursive callers (live on the stack more than once) still fit. - Expression* makeEnsureRefCapacity(HeapType heapType, uint32_t count) { + Expression* makeEnsureRefCapacity(Type type, uint32_t count) { if (count == 0) { return makeNop(); } - auto table = getRefTable(heapType); + auto table = getRefTable(type); // Each operand gets its own copy: the IR is a tree, so an Expression* // cannot be shared between the condition and the grow. auto deficit = makeBinary(Abstract::getBinary(pointerType, Abstract::Sub), - makeRefIndex(heapType, count), + makeRefIndex(type, count), makeTableSize(table)); auto full = makeBinary(Abstract::getBinary(pointerType, Abstract::GtU), - makeRefIndex(heapType, count), + makeRefIndex(type, count), makeTableSize(table)); return makeIf(full, makeDrop(makeTableGrow( - table, - LiteralUtils::makeZero(Type(heapType, Nullable), wasm), - deficit))); + table, LiteralUtils::makeZero(type, wasm), deficit))); } Expression* makeGetStackPos() { @@ -1269,6 +1300,32 @@ struct AsyncifyFlow : public Pass { return builder->makeIf(builder->makeStateCheck(State::Normal), curr); } + // Reads back a fake call-result global, restoring the call result's type. + // The global stores a nullable version of that type, so non-nullable + // references need ref.as_non_null. + Expression* makeFakeGlobalGet(Name name, Type type) { + auto storageType = analyzer->fakeGlobals.getStorageType(type); + if (storageType == type) { + return builder->makeGlobalGet(name, type); + } + if (!type.isTuple()) { + return builder->makeRefAs(RefAsNonNull, + builder->makeGlobalGet(name, storageType)); + } + std::vector elems(type.size()); + for (Index i = 0, size = type.size(); i < size; ++i) { + // Each component needs its own global.get: the IR is a tree, so an + // Expression* cannot be shared. + Expression* elem = builder->makeTupleExtract( + builder->makeGlobalGet(name, storageType), i); + if (type[i].isNonNullableRef()) { + elem = builder->makeRefAs(RefAsNonNull, elem); + } + elems[i] = elem; + } + return builder->makeTupleMake(std::move(elems)); + } + Expression* makeCallSupport(Expression* curr) { // TODO: stop doing this after code can no longer reach a call that may // change the state @@ -1288,9 +1345,10 @@ struct AsyncifyFlow : public Pass { // AsyncifyLocals locals adds local saving/restoring. auto* set = curr->dynCast(); if (set) { - auto name = analyzer->fakeGlobals.getName(set->value->type); + auto type = set->value->type; + auto name = analyzer->fakeGlobals.getName(type); curr = builder->makeGlobalSet(name, set->value); - set->value = builder->makeGlobalGet(name, set->value->type); + set->value = makeFakeGlobalGet(name, type); } // Instrument the call itself (or, if a drop, the drop+call). auto index = callIndex++; @@ -1626,6 +1684,9 @@ struct AsyncifyLocals : public WalkerPass> { // Find the locals that we actually need to load and save: any local that is // alive at a relevant call site must be handled, but others can be ignored. findRelevantLiveLocals(func); + // A spilled local may not be initialized before its first call, so make + // non-nullable ones nullable. Reads/writes are cast back with ref.as_non_null. + makeRelevantLocalsDefaultable(func); // The new function body has a prelude to load locals if rewinding, // then the actual main body, which does all its unwindings by breaking // to the unwind block, which then handles pushing the call index, as @@ -1681,6 +1742,19 @@ struct AsyncifyLocals : public WalkerPass> { relevantLiveLocals = computeRelevantLiveLocals(func, getModule()); } + // Make the non-defaultable locals that we spill nullable + void makeRelevantLocalsDefaultable(Function* func) { + std::unordered_set forced; + for (auto index : relevantLiveLocals) { + if (!func->isParam(index) && !func->getLocalType(index).isDefaultable()) { + forced.insert(index); + } + } + if (!forced.empty()) { + TypeUpdating::handleNonDefaultableLocals(func, *getModule(), &forced); + } + } + // Next slot for a local: a table slot for reference components, a byte // range in linear memory for everything else struct SpillCursors { @@ -1691,8 +1765,8 @@ struct AsyncifyLocals : public WalkerPass> { SpillCursors(AsyncifyBuilder& builder) : builder(builder) {} // Table slot for the next reference - Expression* nextRef(HeapType heapType) { - return builder.makeRefIndex(heapType, refOffsets[heapType]++); + Expression* nextRef(Type type) { + return builder.makeRefIndex(type, refOffsets[type]++); } // Byte offset of the next non-reference component @@ -1707,12 +1781,13 @@ struct AsyncifyLocals : public WalkerPass> { Expression* makeComponentLoading(Type type, Index tempIndex, SpillCursors& cursors) { if (type.isRef()) { - auto heapType = type.getHeapType(); // The table is nullable, so a non-nullable local needs a cast back. - Expression* load = builder->makeTableGet(builder->getRefTable(heapType), - cursors.nextRef(heapType), - Type(heapType, Nullable)); - if (!type.isNullable()) { + auto storageType = type.with(Nullable); + Expression* load = builder->makeTableGet( + builder->getRefTable(storageType), + cursors.nextRef(storageType), + storageType); + if (type.isNonNullableRef()) { load = builder->makeRefAs(RefAsNonNull, load); } return load; @@ -1736,8 +1811,9 @@ struct AsyncifyLocals : public WalkerPass> { Index tempIndex, SpillCursors& cursors) { if (type.isRef()) { - return builder->makeTableSet(builder->getRefTable(type.getHeapType()), - cursors.nextRef(type.getHeapType()), + auto storageType = type.with(Nullable); + return builder->makeTableSet(builder->getRefTable(storageType), + cursors.nextRef(storageType), value); } auto size = getByteSize(type); @@ -1767,7 +1843,7 @@ struct AsyncifyLocals : public WalkerPass> { } for (const auto& type : func->getLocalType(i)) { if (type.isRef()) { - layout.refTotals[type.getHeapType()] += 1; + layout.refTotals[type.with(Nullable)] += 1; } else { layout.memTotal += getByteSize(type); } @@ -1786,8 +1862,8 @@ struct AsyncifyLocals : public WalkerPass> { auto* block = builder->makeBlock(); block->list.push_back(builder->makeIncStackPos(-layout.memTotal)); - for (const auto& [heapType, count] : layout.refTotals) { - block->list.push_back(builder->makeIncRefPos(heapType, -int32_t(count))); + for (const auto& [type, count] : layout.refTotals) { + block->list.push_back(builder->makeIncRefPos(type, -int32_t(count))); } auto tempIndex = builder->addVar(func, builder->pointerType); block->list.push_back( @@ -1814,9 +1890,9 @@ struct AsyncifyLocals : public WalkerPass> { } // Release the slots just restored, so the table does not keep the // references alive for the life of the instance. - for (const auto& [heapType, count] : layout.refTotals) { + for (const auto& [type, count] : layout.refTotals) { for (Index offset = 0; offset < count; offset++) { - block->list.push_back(builder->makeClearRefSlot(heapType, offset)); + block->list.push_back(builder->makeClearRefSlot(type, offset)); } } block->finalize(); @@ -1835,8 +1911,8 @@ struct AsyncifyLocals : public WalkerPass> { block->list.push_back( builder->makeLocalSet(tempIndex, builder->makeGetStackPos())); // Ensure room for this frame's reference spill before writing any of it - for (const auto& [heapType, total] : layout.refTotals) { - block->list.push_back(builder->makeEnsureRefCapacity(heapType, total)); + for (const auto& [type, total] : layout.refTotals) { + block->list.push_back(builder->makeEnsureRefCapacity(type, total)); } SpillCursors cursors(*builder); for (Index i = 0; i < numLocals; i++) { @@ -1856,8 +1932,8 @@ struct AsyncifyLocals : public WalkerPass> { } } block->list.push_back(builder->makeIncStackPos(cursors.memOffset)); - for (const auto& [heapType, total] : cursors.refOffsets) { - block->list.push_back(builder->makeIncRefPos(heapType, int32_t(total))); + for (const auto& [type, total] : cursors.refOffsets) { + block->list.push_back(builder->makeIncRefPos(type, int32_t(total))); } block->finalize(); return block; @@ -2046,7 +2122,6 @@ struct Asyncify : public Pass { runner.setValidateGlobally(false); runner.run(); } - setupRefSpill(module, instrumentedFuncs); if (asserts) { // Add asserts in non-instrumented code. Note we do not use an // instrumented pass runner here as we do want to run on all functions. @@ -2059,16 +2134,22 @@ struct Asyncify : public Pass { runner.setValidateGlobally(false); runner.run(); } - // Next, add local saving/restoring logic. We optimize before doing this, - // to undo the extra code generated by flattening, and to arrive at the - // minimal amount of locals (which is important as we must save and - // restore those locals). We also and optimize after as well to simplify - // the code as much as possible. + // Optimize before adding local saving/restoring, to undo flattening and + // reach the minimal set of locals to save/restore. This must run before + // setting up the spill tables, since optimization can add/remove ref locals + // (e.g. heap2local) that the tables must match. + if (optimize) { + PassUtils::FilteredPassRunner runner(module, instrumentedFuncs); + runner.addDefaultFunctionOptimizationPasses(); + runner.setIsNested(true); + runner.setValidateGlobally(false); + runner.run(); + } + setupRefSpill(module, instrumentedFuncs); + // Next, add local saving/restoring logic. We optimize after as well to + // simplify the code as much as possible. { PassUtils::FilteredPassRunner runner(module, instrumentedFuncs); - if (optimize) { - runner.addDefaultFunctionOptimizationPasses(); - } runner.add(std::make_unique( &analyzer, pointerType, asyncifyMemory)); if (optimize) { @@ -2124,35 +2205,36 @@ struct Asyncify : public Pass { const PassUtils::FuncSet& instrumentedFuncs) { Builder builder(*module); RefCountMap counts; - for (auto* func : instrumentedFuncs) { - if (!func->body) { + // Iterate in module order + for (auto& func : module->functions) { + if (!instrumentedFuncs.contains(func.get()) || !func->body) { continue; } - for (auto local : computeRelevantLiveLocals(func, module)) { + for (auto local : computeRelevantLiveLocals(func.get(), module)) { for (const auto& type : func->getLocalType(local)) { if (type.isRef()) { - counts[type.getHeapType()] += 1; + counts[type.with(Nullable)] += 1; } } } } - for (const auto& [heapType, count] : counts) { + for (const auto& [type, count] : counts) { if (count == 0) { continue; } - auto table = Builder::makeTable(refTableName(heapType), - Type(heapType, Nullable), + auto table = Builder::makeTable(refTableName(*module, type), + type, // nullable table type count, // initial size REF_TABLE_MAX, // max size (growable) pointerType, // address type nullptr); // init: default null module->addTable(std::move(table)); - module->addGlobal(builder.makeGlobal(refCursorName(heapType), + module->addGlobal(builder.makeGlobal(refCursorName(*module, type), pointerType, builder.makeConst(pointerType), Builder::Mutable)); - refHeapTypes.insert(heapType); + refTypes.insert(type); } } @@ -2172,15 +2254,15 @@ struct Asyncify : public Pass { } // Asyncify only supports one pause at a time. if (name == ASYNCIFY_START_UNWIND) { - for (auto heapType : refHeapTypes) { + for (auto type : refTypes) { body->list.push_back(builder.makeIf( builder.makeBinary( Abstract::getBinary(pointerType, Abstract::Ne), - builder.makeGlobalGet(refCursorName(heapType), pointerType), + builder.makeGlobalGet(refCursorName(*module, type), pointerType), builder.makeConst(pointerType)), builder.makeUnreachable())); body->list.push_back(builder.makeGlobalSet( - refCursorName(heapType), builder.makeConst(pointerType))); + refCursorName(*module, type), builder.makeConst(pointerType))); } } // Verify the data is valid. @@ -2236,7 +2318,7 @@ struct Asyncify : public Pass { Type pointerType; Name asyncifyMemory; - std::set refHeapTypes; + InsertOrderedSet refTypes; }; Pass* createAsyncifyPass() { return new Asyncify(); } diff --git a/test/lit/passes/asyncify-reference-types-heap2local.wast b/test/lit/passes/asyncify-reference-types-heap2local.wast new file mode 100644 index 00000000000..0eadf721dcc --- /dev/null +++ b/test/lit/passes/asyncify-reference-types-heap2local.wast @@ -0,0 +1,292 @@ +;; NOTE: Assertions have been generated by update_lit_checks.py --all-items and should not be edited. + +;; RUN: wasm-opt %s --asyncify -O2 --all-features -S -o - | filecheck %s + +;; Optimizations run inside Asyncify can create new reference locals, so the +;; spill tables must be set up after them. Here Heap2Local unboxes the struct +;; into locals, and the externref field becomes a local that is spilled across +;; the call. +(module + ;; CHECK: (type $A (struct (field externref) (field i32))) + + ;; CHECK: (type $1 (func)) + + ;; CHECK: (type $2 (func (param i32))) + + ;; CHECK: (type $3 (func (param externref) (result i32))) + + ;; CHECK: (type $4 (func (result i32))) + + ;; CHECK: (import "env" "unwind" (func $unwind (type $1))) + (import "env" "unwind" (func $unwind)) + (memory 1) + (type $A (struct (field externref) (field i32))) + (func (export "run") (param $e externref) (result i32) + (local $a (ref $A)) + (local.set $a (struct.new $A (local.get $e) (i32.const 1))) + (call $unwind) + (ref.is_null (struct.get $A 0 (local.get $a))) + ) +) +;; CHECK: (global $__asyncify_state (mut i32) (i32.const 0)) + +;; CHECK: (global $__asyncify_data (mut i32) (i32.const 0)) + +;; CHECK: (global $__asyncify_ref_pos_A (mut i32) (i32.const 0)) + +;; CHECK: (memory $0 1) + +;; CHECK: (table $__asyncify_ref_table_A 1 67108864 (ref null $A)) + +;; CHECK: (export "run" (func $0)) + +;; CHECK: (export "asyncify_start_unwind" (func $asyncify_start_unwind)) + +;; CHECK: (export "asyncify_stop_unwind" (func $asyncify_stop_unwind)) + +;; CHECK: (export "asyncify_start_rewind" (func $asyncify_start_rewind)) + +;; CHECK: (export "asyncify_stop_rewind" (func $asyncify_stop_unwind)) + +;; CHECK: (export "asyncify_get_state" (func $asyncify_get_state)) + +;; CHECK: (func $0 (type $3) (param $0 externref) (result i32) +;; CHECK-NEXT: (local $1 (ref null $A)) +;; CHECK-NEXT: (local $2 i32) +;; CHECK-NEXT: (if +;; CHECK-NEXT: (i32.eq +;; CHECK-NEXT: (global.get $__asyncify_state) +;; CHECK-NEXT: (i32.const 2) +;; CHECK-NEXT: ) +;; CHECK-NEXT: (then +;; CHECK-NEXT: (global.set $__asyncify_ref_pos_A +;; CHECK-NEXT: (i32.sub +;; CHECK-NEXT: (global.get $__asyncify_ref_pos_A) +;; CHECK-NEXT: (i32.const 1) +;; CHECK-NEXT: ) +;; CHECK-NEXT: ) +;; CHECK-NEXT: (drop +;; CHECK-NEXT: (i32.load +;; CHECK-NEXT: (global.get $__asyncify_data) +;; CHECK-NEXT: ) +;; CHECK-NEXT: ) +;; CHECK-NEXT: (local.set $1 +;; CHECK-NEXT: (table.get $__asyncify_ref_table_A +;; CHECK-NEXT: (global.get $__asyncify_ref_pos_A) +;; CHECK-NEXT: ) +;; CHECK-NEXT: ) +;; CHECK-NEXT: (table.set $__asyncify_ref_table_A +;; CHECK-NEXT: (global.get $__asyncify_ref_pos_A) +;; CHECK-NEXT: (ref.null none) +;; CHECK-NEXT: ) +;; CHECK-NEXT: ) +;; CHECK-NEXT: ) +;; CHECK-NEXT: (local.set $2 +;; CHECK-NEXT: (block $__asyncify_unwind (result i32) +;; CHECK-NEXT: (if +;; CHECK-NEXT: (i32.eq +;; CHECK-NEXT: (global.get $__asyncify_state) +;; CHECK-NEXT: (i32.const 2) +;; CHECK-NEXT: ) +;; CHECK-NEXT: (then +;; CHECK-NEXT: (i32.store +;; CHECK-NEXT: (global.get $__asyncify_data) +;; CHECK-NEXT: (i32.sub +;; CHECK-NEXT: (i32.load +;; CHECK-NEXT: (global.get $__asyncify_data) +;; CHECK-NEXT: ) +;; CHECK-NEXT: (i32.const 4) +;; CHECK-NEXT: ) +;; CHECK-NEXT: ) +;; CHECK-NEXT: (local.set $2 +;; CHECK-NEXT: (i32.load +;; CHECK-NEXT: (i32.load +;; CHECK-NEXT: (global.get $__asyncify_data) +;; CHECK-NEXT: ) +;; CHECK-NEXT: ) +;; CHECK-NEXT: ) +;; CHECK-NEXT: ) +;; CHECK-NEXT: ) +;; CHECK-NEXT: (if +;; CHECK-NEXT: (i32.eqz +;; CHECK-NEXT: (global.get $__asyncify_state) +;; CHECK-NEXT: ) +;; CHECK-NEXT: (then +;; CHECK-NEXT: (local.set $1 +;; CHECK-NEXT: (struct.new $A +;; CHECK-NEXT: (local.get $0) +;; CHECK-NEXT: (i32.const 1) +;; CHECK-NEXT: ) +;; CHECK-NEXT: ) +;; CHECK-NEXT: ) +;; CHECK-NEXT: ) +;; CHECK-NEXT: (if +;; CHECK-NEXT: (i32.or +;; CHECK-NEXT: (i32.eqz +;; CHECK-NEXT: (global.get $__asyncify_state) +;; CHECK-NEXT: ) +;; CHECK-NEXT: (i32.eqz +;; CHECK-NEXT: (local.get $2) +;; CHECK-NEXT: ) +;; CHECK-NEXT: ) +;; CHECK-NEXT: (then +;; CHECK-NEXT: (call $unwind) +;; CHECK-NEXT: (drop +;; CHECK-NEXT: (br_if $__asyncify_unwind +;; CHECK-NEXT: (i32.const 0) +;; CHECK-NEXT: (i32.eq +;; CHECK-NEXT: (global.get $__asyncify_state) +;; CHECK-NEXT: (i32.const 1) +;; CHECK-NEXT: ) +;; CHECK-NEXT: ) +;; CHECK-NEXT: ) +;; CHECK-NEXT: ) +;; CHECK-NEXT: ) +;; CHECK-NEXT: (if +;; CHECK-NEXT: (i32.eqz +;; CHECK-NEXT: (global.get $__asyncify_state) +;; CHECK-NEXT: ) +;; CHECK-NEXT: (then +;; CHECK-NEXT: (return +;; CHECK-NEXT: (ref.is_null +;; CHECK-NEXT: (struct.get $A 0 +;; CHECK-NEXT: (local.get $1) +;; CHECK-NEXT: ) +;; CHECK-NEXT: ) +;; CHECK-NEXT: ) +;; CHECK-NEXT: ) +;; CHECK-NEXT: ) +;; CHECK-NEXT: (unreachable) +;; CHECK-NEXT: ) +;; CHECK-NEXT: ) +;; CHECK-NEXT: (i32.store +;; CHECK-NEXT: (i32.load +;; CHECK-NEXT: (global.get $__asyncify_data) +;; CHECK-NEXT: ) +;; CHECK-NEXT: (local.get $2) +;; CHECK-NEXT: ) +;; CHECK-NEXT: (i32.store +;; CHECK-NEXT: (global.get $__asyncify_data) +;; CHECK-NEXT: (i32.add +;; CHECK-NEXT: (i32.load +;; CHECK-NEXT: (global.get $__asyncify_data) +;; CHECK-NEXT: ) +;; CHECK-NEXT: (i32.const 4) +;; CHECK-NEXT: ) +;; CHECK-NEXT: ) +;; CHECK-NEXT: (drop +;; CHECK-NEXT: (i32.load +;; CHECK-NEXT: (global.get $__asyncify_data) +;; CHECK-NEXT: ) +;; CHECK-NEXT: ) +;; CHECK-NEXT: (if +;; CHECK-NEXT: (i32.gt_u +;; CHECK-NEXT: (i32.add +;; CHECK-NEXT: (global.get $__asyncify_ref_pos_A) +;; CHECK-NEXT: (i32.const 1) +;; CHECK-NEXT: ) +;; CHECK-NEXT: (table.size $__asyncify_ref_table_A) +;; CHECK-NEXT: ) +;; CHECK-NEXT: (then +;; CHECK-NEXT: (drop +;; CHECK-NEXT: (table.grow $__asyncify_ref_table_A +;; CHECK-NEXT: (ref.null none) +;; CHECK-NEXT: (i32.sub +;; CHECK-NEXT: (i32.add +;; CHECK-NEXT: (global.get $__asyncify_ref_pos_A) +;; CHECK-NEXT: (i32.const 1) +;; CHECK-NEXT: ) +;; CHECK-NEXT: (table.size $__asyncify_ref_table_A) +;; CHECK-NEXT: ) +;; CHECK-NEXT: ) +;; CHECK-NEXT: ) +;; CHECK-NEXT: ) +;; CHECK-NEXT: ) +;; CHECK-NEXT: (table.set $__asyncify_ref_table_A +;; CHECK-NEXT: (global.get $__asyncify_ref_pos_A) +;; CHECK-NEXT: (local.get $1) +;; CHECK-NEXT: ) +;; CHECK-NEXT: (global.set $__asyncify_ref_pos_A +;; CHECK-NEXT: (i32.add +;; CHECK-NEXT: (global.get $__asyncify_ref_pos_A) +;; CHECK-NEXT: (i32.const 1) +;; CHECK-NEXT: ) +;; CHECK-NEXT: ) +;; CHECK-NEXT: (i32.const 0) +;; CHECK-NEXT: ) + +;; CHECK: (func $asyncify_start_unwind (type $2) (param $0 i32) +;; CHECK-NEXT: (global.set $__asyncify_state +;; CHECK-NEXT: (i32.const 1) +;; CHECK-NEXT: ) +;; CHECK-NEXT: (global.set $__asyncify_data +;; CHECK-NEXT: (local.get $0) +;; CHECK-NEXT: ) +;; CHECK-NEXT: (if +;; CHECK-NEXT: (global.get $__asyncify_ref_pos_A) +;; CHECK-NEXT: (then +;; CHECK-NEXT: (unreachable) +;; CHECK-NEXT: ) +;; CHECK-NEXT: ) +;; CHECK-NEXT: (global.set $__asyncify_ref_pos_A +;; CHECK-NEXT: (i32.const 0) +;; CHECK-NEXT: ) +;; CHECK-NEXT: (if +;; CHECK-NEXT: (i32.gt_u +;; CHECK-NEXT: (i32.load +;; CHECK-NEXT: (global.get $__asyncify_data) +;; CHECK-NEXT: ) +;; CHECK-NEXT: (i32.load offset=4 +;; CHECK-NEXT: (global.get $__asyncify_data) +;; CHECK-NEXT: ) +;; CHECK-NEXT: ) +;; CHECK-NEXT: (then +;; CHECK-NEXT: (unreachable) +;; CHECK-NEXT: ) +;; CHECK-NEXT: ) +;; CHECK-NEXT: ) + +;; CHECK: (func $asyncify_stop_unwind (type $1) +;; CHECK-NEXT: (global.set $__asyncify_state +;; CHECK-NEXT: (i32.const 0) +;; CHECK-NEXT: ) +;; CHECK-NEXT: (if +;; CHECK-NEXT: (i32.gt_u +;; CHECK-NEXT: (i32.load +;; CHECK-NEXT: (global.get $__asyncify_data) +;; CHECK-NEXT: ) +;; CHECK-NEXT: (i32.load offset=4 +;; CHECK-NEXT: (global.get $__asyncify_data) +;; CHECK-NEXT: ) +;; CHECK-NEXT: ) +;; CHECK-NEXT: (then +;; CHECK-NEXT: (unreachable) +;; CHECK-NEXT: ) +;; CHECK-NEXT: ) +;; CHECK-NEXT: ) + +;; CHECK: (func $asyncify_start_rewind (type $2) (param $0 i32) +;; CHECK-NEXT: (global.set $__asyncify_state +;; CHECK-NEXT: (i32.const 2) +;; CHECK-NEXT: ) +;; CHECK-NEXT: (global.set $__asyncify_data +;; CHECK-NEXT: (local.get $0) +;; CHECK-NEXT: ) +;; CHECK-NEXT: (if +;; CHECK-NEXT: (i32.gt_u +;; CHECK-NEXT: (i32.load +;; CHECK-NEXT: (global.get $__asyncify_data) +;; CHECK-NEXT: ) +;; CHECK-NEXT: (i32.load offset=4 +;; CHECK-NEXT: (global.get $__asyncify_data) +;; CHECK-NEXT: ) +;; CHECK-NEXT: ) +;; CHECK-NEXT: (then +;; CHECK-NEXT: (unreachable) +;; CHECK-NEXT: ) +;; CHECK-NEXT: ) +;; CHECK-NEXT: ) + +;; CHECK: (func $asyncify_get_state (type $4) (result i32) +;; CHECK-NEXT: (global.get $__asyncify_state) +;; CHECK-NEXT: ) diff --git a/test/lit/passes/asyncify-reference-types.wast b/test/lit/passes/asyncify-reference-types.wast index 525b690158a..ac5f09c3ffb 100644 --- a/test/lit/passes/asyncify-reference-types.wast +++ b/test/lit/passes/asyncify-reference-types.wast @@ -1,49 +1,1672 @@ -;; Asyncify spills reference-typed locals (externref/funcref) into growable -;; per-heap-type tables. +;; NOTE: Assertions have been generated by update_lit_checks.py --all-items and should not be edited. -;; RUN: foreach %s %t wasm-opt --enable-reference-types --asyncify -S -o - | filecheck %s +;; RUN: foreach %s %t wasm-opt --asyncify --all-features -S -o - | filecheck %s +;; Reference locals are spilled to one table per heap type. The initial size +;; covers the reference locals of that type that are live across calls, and the +;; table grows on unwind. (module + ;; CHECK: (type $A (struct)) + + ;; CHECK: (type $1 (func)) + + ;; CHECK: (type $2 (func (result i32))) + + ;; CHECK: (type $3 (func (param i32))) + + ;; CHECK: (type $4 (func (result (ref func)))) + + ;; CHECK: (type $5 (func (param (ref func)))) + + ;; CHECK: (type $6 (func (param externref) (result i32))) + + ;; CHECK: (type $7 (func (param funcref) (result i32))) + + ;; CHECK: (type $8 (func (param (ref (exact $A)) (ref $A)) (result i32))) + + ;; CHECK: (import "env" "unwind" (func $unwind (type $1))) (import "env" "unwind" (func $unwind)) + ;; CHECK: (import "env" "get_func" (func $get_func (type $4) (result (ref func)))) + (import "env" "get_func" (func $get_func (result (ref func)))) + ;; CHECK: (import "env" "use" (func $use (type $5) (param (ref func)))) + (import "env" "use" (func $use (param (ref func)))) + (type $A (struct)) (memory 1) - (func (export "run") (param $r externref) (result i32) - (local $x i32) + ;; CHECK: (global $__asyncify_state (mut i32) (i32.const 0)) + + ;; CHECK: (global $__asyncify_data (mut i32) (i32.const 0)) + + ;; CHECK: (global $__asyncify_ref_pos_extern (mut i32) (i32.const 0)) + + ;; CHECK: (global $__asyncify_ref_pos_func (mut i32) (i32.const 0)) + + ;; CHECK: (global $__asyncify_ref_pos_A_exact (mut i32) (i32.const 0)) + + ;; CHECK: (global $__asyncify_ref_pos_A (mut i32) (i32.const 0)) + + ;; CHECK: (memory $0 1) + + ;; CHECK: (table $__asyncify_ref_table_extern 2 67108864 externref) + + ;; CHECK: (table $__asyncify_ref_table_func 4 67108864 funcref) + + ;; CHECK: (table $__asyncify_ref_table_A_exact 1 67108864 (ref null (exact $A))) + + ;; CHECK: (table $__asyncify_ref_table_A 1 67108864 (ref null $A)) + + ;; CHECK: (elem declare func $target) + + ;; CHECK: (export "reference-locals" (func $reference-locals)) + + ;; CHECK: (export "funcref-local" (func $funcref-local)) + + ;; CHECK: (export "call-result" (func $call-result)) + + ;; CHECK: (export "uninitialized-local" (func $uninitialized-local)) + + ;; CHECK: (export "exact" (func $exact)) + + ;; CHECK: (export "asyncify_start_unwind" (func $asyncify_start_unwind)) + + ;; CHECK: (export "asyncify_stop_unwind" (func $asyncify_stop_unwind)) + + ;; CHECK: (export "asyncify_start_rewind" (func $asyncify_start_rewind)) + + ;; CHECK: (export "asyncify_stop_rewind" (func $asyncify_stop_rewind)) + + ;; CHECK: (export "asyncify_get_state" (func $asyncify_get_state)) + + ;; CHECK: (func $target (type $1) + ;; CHECK-NEXT: ) + (func $target) + + ;; CHECK: (func $reference-locals (type $6) (param $e externref) (result i32) + ;; CHECK-NEXT: (local $e2 externref) + ;; CHECK-NEXT: (local $e3 externref) + ;; CHECK-NEXT: (local $3 externref) + ;; CHECK-NEXT: (local $4 externref) + ;; CHECK-NEXT: (local $5 i32) + ;; CHECK-NEXT: (local $6 externref) + ;; CHECK-NEXT: (local $7 i32) + ;; CHECK-NEXT: (local $8 i32) + ;; CHECK-NEXT: (local $9 i32) + ;; CHECK-NEXT: (local $10 i32) + ;; CHECK-NEXT: (local $11 i32) + ;; CHECK-NEXT: (local $12 i32) + ;; CHECK-NEXT: (local $13 i32) + ;; CHECK-NEXT: (local $14 i32) + ;; CHECK-NEXT: (if + ;; CHECK-NEXT: (i32.eq + ;; CHECK-NEXT: (global.get $__asyncify_state) + ;; CHECK-NEXT: (i32.const 2) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (then + ;; CHECK-NEXT: (i32.store + ;; CHECK-NEXT: (global.get $__asyncify_data) + ;; CHECK-NEXT: (i32.add + ;; CHECK-NEXT: (i32.load + ;; CHECK-NEXT: (global.get $__asyncify_data) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (i32.const -4) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (global.set $__asyncify_ref_pos_extern + ;; CHECK-NEXT: (i32.add + ;; CHECK-NEXT: (global.get $__asyncify_ref_pos_extern) + ;; CHECK-NEXT: (i32.const -2) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.set $13 + ;; CHECK-NEXT: (i32.load + ;; CHECK-NEXT: (global.get $__asyncify_data) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.set $e2 + ;; CHECK-NEXT: (table.get $__asyncify_ref_table_extern + ;; CHECK-NEXT: (i32.add + ;; CHECK-NEXT: (global.get $__asyncify_ref_pos_extern) + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.set $e3 + ;; CHECK-NEXT: (table.get $__asyncify_ref_table_extern + ;; CHECK-NEXT: (i32.add + ;; CHECK-NEXT: (global.get $__asyncify_ref_pos_extern) + ;; CHECK-NEXT: (i32.const 1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.set $9 + ;; CHECK-NEXT: (i32.load + ;; CHECK-NEXT: (local.get $13) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (table.set $__asyncify_ref_table_extern + ;; CHECK-NEXT: (i32.add + ;; CHECK-NEXT: (global.get $__asyncify_ref_pos_extern) + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (ref.null noextern) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (table.set $__asyncify_ref_table_extern + ;; CHECK-NEXT: (i32.add + ;; CHECK-NEXT: (global.get $__asyncify_ref_pos_extern) + ;; CHECK-NEXT: (i32.const 1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (ref.null noextern) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.set $11 + ;; CHECK-NEXT: (block $__asyncify_unwind (result i32) + ;; CHECK-NEXT: (block + ;; CHECK-NEXT: (block + ;; CHECK-NEXT: (if + ;; CHECK-NEXT: (i32.eq + ;; CHECK-NEXT: (global.get $__asyncify_state) + ;; CHECK-NEXT: (i32.const 2) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (then + ;; CHECK-NEXT: (i32.store + ;; CHECK-NEXT: (global.get $__asyncify_data) + ;; CHECK-NEXT: (i32.add + ;; CHECK-NEXT: (i32.load + ;; CHECK-NEXT: (global.get $__asyncify_data) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (i32.const -4) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.set $12 + ;; CHECK-NEXT: (i32.load + ;; CHECK-NEXT: (i32.load + ;; CHECK-NEXT: (global.get $__asyncify_data) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (block + ;; CHECK-NEXT: (block + ;; CHECK-NEXT: (if + ;; CHECK-NEXT: (i32.eq + ;; CHECK-NEXT: (global.get $__asyncify_state) + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (then + ;; CHECK-NEXT: (local.set $3 + ;; CHECK-NEXT: (local.get $e) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.set $e2 + ;; CHECK-NEXT: (local.get $3) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.set $e3 + ;; CHECK-NEXT: (ref.null noextern) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (nop) + ;; CHECK-NEXT: (nop) + ;; CHECK-NEXT: (if + ;; CHECK-NEXT: (i32.or + ;; CHECK-NEXT: (i32.eq + ;; CHECK-NEXT: (global.get $__asyncify_state) + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (i32.eq + ;; CHECK-NEXT: (local.get $12) + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (then + ;; CHECK-NEXT: (call $unwind) + ;; CHECK-NEXT: (if + ;; CHECK-NEXT: (i32.eq + ;; CHECK-NEXT: (global.get $__asyncify_state) + ;; CHECK-NEXT: (i32.const 1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (then + ;; CHECK-NEXT: (br $__asyncify_unwind + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (if + ;; CHECK-NEXT: (i32.eq + ;; CHECK-NEXT: (global.get $__asyncify_state) + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (then + ;; CHECK-NEXT: (local.set $4 + ;; CHECK-NEXT: (local.get $e2) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.set $5 + ;; CHECK-NEXT: (ref.is_null + ;; CHECK-NEXT: (local.get $4) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.set $6 + ;; CHECK-NEXT: (local.get $e3) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.set $7 + ;; CHECK-NEXT: (ref.is_null + ;; CHECK-NEXT: (local.get $6) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.set $8 + ;; CHECK-NEXT: (i32.and + ;; CHECK-NEXT: (local.get $5) + ;; CHECK-NEXT: (local.get $7) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.set $9 + ;; CHECK-NEXT: (local.get $8) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (nop) + ;; CHECK-NEXT: (nop) + ;; CHECK-NEXT: (nop) + ;; CHECK-NEXT: (nop) + ;; CHECK-NEXT: (nop) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (if + ;; CHECK-NEXT: (i32.eq + ;; CHECK-NEXT: (global.get $__asyncify_state) + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (then + ;; CHECK-NEXT: (local.set $10 + ;; CHECK-NEXT: (local.get $9) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (return + ;; CHECK-NEXT: (local.get $10) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (nop) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (unreachable) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (unreachable) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (block + ;; CHECK-NEXT: (i32.store + ;; CHECK-NEXT: (i32.load + ;; CHECK-NEXT: (global.get $__asyncify_data) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.get $11) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (i32.store + ;; CHECK-NEXT: (global.get $__asyncify_data) + ;; CHECK-NEXT: (i32.add + ;; CHECK-NEXT: (i32.load + ;; CHECK-NEXT: (global.get $__asyncify_data) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (i32.const 4) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (block + ;; CHECK-NEXT: (local.set $14 + ;; CHECK-NEXT: (i32.load + ;; CHECK-NEXT: (global.get $__asyncify_data) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (if + ;; CHECK-NEXT: (i32.gt_u + ;; CHECK-NEXT: (i32.add + ;; CHECK-NEXT: (global.get $__asyncify_ref_pos_extern) + ;; CHECK-NEXT: (i32.const 2) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (table.size $__asyncify_ref_table_extern) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (then + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (table.grow $__asyncify_ref_table_extern + ;; CHECK-NEXT: (ref.null noextern) + ;; CHECK-NEXT: (i32.sub + ;; CHECK-NEXT: (i32.add + ;; CHECK-NEXT: (global.get $__asyncify_ref_pos_extern) + ;; CHECK-NEXT: (i32.const 2) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (table.size $__asyncify_ref_table_extern) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (table.set $__asyncify_ref_table_extern + ;; CHECK-NEXT: (i32.add + ;; CHECK-NEXT: (global.get $__asyncify_ref_pos_extern) + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.get $e2) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (table.set $__asyncify_ref_table_extern + ;; CHECK-NEXT: (i32.add + ;; CHECK-NEXT: (global.get $__asyncify_ref_pos_extern) + ;; CHECK-NEXT: (i32.const 1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.get $e3) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (i32.store + ;; CHECK-NEXT: (local.get $14) + ;; CHECK-NEXT: (local.get $9) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (i32.store + ;; CHECK-NEXT: (global.get $__asyncify_data) + ;; CHECK-NEXT: (i32.add + ;; CHECK-NEXT: (i32.load + ;; CHECK-NEXT: (global.get $__asyncify_data) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (i32.const 4) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (global.set $__asyncify_ref_pos_extern + ;; CHECK-NEXT: (i32.add + ;; CHECK-NEXT: (global.get $__asyncify_ref_pos_extern) + ;; CHECK-NEXT: (i32.const 2) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + (func $reference-locals (export "reference-locals") + (param $e externref) + (result i32) + (local $e2 externref) + (local $e3 externref) + (local.set $e2 (local.get $e)) + (local.set $e3 (ref.null extern)) + (call $unwind) + (i32.and (ref.is_null (local.get $e2)) (ref.is_null (local.get $e3))) + ) + + ;; CHECK: (func $funcref-local (type $7) (param $f funcref) (result i32) + ;; CHECK-NEXT: (local $f2 funcref) + ;; CHECK-NEXT: (local $2 funcref) + ;; CHECK-NEXT: (local $3 funcref) + ;; CHECK-NEXT: (local $4 i32) + ;; CHECK-NEXT: (local $5 i32) + ;; CHECK-NEXT: (local $6 i32) + ;; CHECK-NEXT: (local $7 i32) + ;; CHECK-NEXT: (local $8 i32) + ;; CHECK-NEXT: (local $9 i32) + ;; CHECK-NEXT: (local $10 i32) + ;; CHECK-NEXT: (if + ;; CHECK-NEXT: (i32.eq + ;; CHECK-NEXT: (global.get $__asyncify_state) + ;; CHECK-NEXT: (i32.const 2) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (then + ;; CHECK-NEXT: (i32.store + ;; CHECK-NEXT: (global.get $__asyncify_data) + ;; CHECK-NEXT: (i32.add + ;; CHECK-NEXT: (i32.load + ;; CHECK-NEXT: (global.get $__asyncify_data) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (i32.const -4) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (global.set $__asyncify_ref_pos_func + ;; CHECK-NEXT: (i32.add + ;; CHECK-NEXT: (global.get $__asyncify_ref_pos_func) + ;; CHECK-NEXT: (i32.const -1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.set $9 + ;; CHECK-NEXT: (i32.load + ;; CHECK-NEXT: (global.get $__asyncify_data) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.set $f2 + ;; CHECK-NEXT: (table.get $__asyncify_ref_table_func + ;; CHECK-NEXT: (i32.add + ;; CHECK-NEXT: (global.get $__asyncify_ref_pos_func) + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.set $5 + ;; CHECK-NEXT: (i32.load + ;; CHECK-NEXT: (local.get $9) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (table.set $__asyncify_ref_table_func + ;; CHECK-NEXT: (i32.add + ;; CHECK-NEXT: (global.get $__asyncify_ref_pos_func) + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (ref.null nofunc) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.set $7 + ;; CHECK-NEXT: (block $__asyncify_unwind (result i32) + ;; CHECK-NEXT: (block + ;; CHECK-NEXT: (block + ;; CHECK-NEXT: (if + ;; CHECK-NEXT: (i32.eq + ;; CHECK-NEXT: (global.get $__asyncify_state) + ;; CHECK-NEXT: (i32.const 2) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (then + ;; CHECK-NEXT: (i32.store + ;; CHECK-NEXT: (global.get $__asyncify_data) + ;; CHECK-NEXT: (i32.add + ;; CHECK-NEXT: (i32.load + ;; CHECK-NEXT: (global.get $__asyncify_data) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (i32.const -4) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.set $8 + ;; CHECK-NEXT: (i32.load + ;; CHECK-NEXT: (i32.load + ;; CHECK-NEXT: (global.get $__asyncify_data) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (block + ;; CHECK-NEXT: (block + ;; CHECK-NEXT: (if + ;; CHECK-NEXT: (i32.eq + ;; CHECK-NEXT: (global.get $__asyncify_state) + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (then + ;; CHECK-NEXT: (local.set $2 + ;; CHECK-NEXT: (local.get $f) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.set $f2 + ;; CHECK-NEXT: (local.get $2) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (nop) + ;; CHECK-NEXT: (if + ;; CHECK-NEXT: (i32.or + ;; CHECK-NEXT: (i32.eq + ;; CHECK-NEXT: (global.get $__asyncify_state) + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (i32.eq + ;; CHECK-NEXT: (local.get $8) + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (then + ;; CHECK-NEXT: (call $unwind) + ;; CHECK-NEXT: (if + ;; CHECK-NEXT: (i32.eq + ;; CHECK-NEXT: (global.get $__asyncify_state) + ;; CHECK-NEXT: (i32.const 1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (then + ;; CHECK-NEXT: (br $__asyncify_unwind + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (if + ;; CHECK-NEXT: (i32.eq + ;; CHECK-NEXT: (global.get $__asyncify_state) + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (then + ;; CHECK-NEXT: (local.set $3 + ;; CHECK-NEXT: (local.get $f2) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.set $4 + ;; CHECK-NEXT: (ref.is_null + ;; CHECK-NEXT: (local.get $3) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.set $5 + ;; CHECK-NEXT: (local.get $4) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (nop) + ;; CHECK-NEXT: (nop) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (if + ;; CHECK-NEXT: (i32.eq + ;; CHECK-NEXT: (global.get $__asyncify_state) + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (then + ;; CHECK-NEXT: (local.set $6 + ;; CHECK-NEXT: (local.get $5) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (return + ;; CHECK-NEXT: (local.get $6) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (nop) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (unreachable) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (unreachable) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (block + ;; CHECK-NEXT: (i32.store + ;; CHECK-NEXT: (i32.load + ;; CHECK-NEXT: (global.get $__asyncify_data) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.get $7) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (i32.store + ;; CHECK-NEXT: (global.get $__asyncify_data) + ;; CHECK-NEXT: (i32.add + ;; CHECK-NEXT: (i32.load + ;; CHECK-NEXT: (global.get $__asyncify_data) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (i32.const 4) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (block + ;; CHECK-NEXT: (local.set $10 + ;; CHECK-NEXT: (i32.load + ;; CHECK-NEXT: (global.get $__asyncify_data) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (if + ;; CHECK-NEXT: (i32.gt_u + ;; CHECK-NEXT: (i32.add + ;; CHECK-NEXT: (global.get $__asyncify_ref_pos_func) + ;; CHECK-NEXT: (i32.const 1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (table.size $__asyncify_ref_table_func) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (then + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (table.grow $__asyncify_ref_table_func + ;; CHECK-NEXT: (ref.null nofunc) + ;; CHECK-NEXT: (i32.sub + ;; CHECK-NEXT: (i32.add + ;; CHECK-NEXT: (global.get $__asyncify_ref_pos_func) + ;; CHECK-NEXT: (i32.const 1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (table.size $__asyncify_ref_table_func) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (table.set $__asyncify_ref_table_func + ;; CHECK-NEXT: (i32.add + ;; CHECK-NEXT: (global.get $__asyncify_ref_pos_func) + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.get $f2) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (i32.store + ;; CHECK-NEXT: (local.get $10) + ;; CHECK-NEXT: (local.get $5) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (i32.store + ;; CHECK-NEXT: (global.get $__asyncify_data) + ;; CHECK-NEXT: (i32.add + ;; CHECK-NEXT: (i32.load + ;; CHECK-NEXT: (global.get $__asyncify_data) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (i32.const 4) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (global.set $__asyncify_ref_pos_func + ;; CHECK-NEXT: (i32.add + ;; CHECK-NEXT: (global.get $__asyncify_ref_pos_func) + ;; CHECK-NEXT: (i32.const 1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + (func $funcref-local (export "funcref-local") + (param $f funcref) + (result i32) + (local $f2 funcref) + (local.set $f2 (local.get $f)) (call $unwind) - (drop (ref.is_null (local.get $r))) - (local.get $x) + (ref.is_null (local.get $f2)) + ) + + ;; A non-nullable call result is stashed in a nullable fake global, so the + ;; read-back needs ref.as_non_null. + ;; CHECK: (func $call-result (type $2) (result i32) + ;; CHECK-NEXT: (local $0 funcref) + ;; CHECK-NEXT: (local $1 i32) + ;; CHECK-NEXT: (local $2 i32) + ;; CHECK-NEXT: (local $3 i32) + ;; CHECK-NEXT: (local $4 funcref) + ;; CHECK-NEXT: (local $5 i32) + ;; CHECK-NEXT: (local $6 i32) + ;; CHECK-NEXT: (if + ;; CHECK-NEXT: (i32.eq + ;; CHECK-NEXT: (global.get $__asyncify_state) + ;; CHECK-NEXT: (i32.const 2) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (then + ;; CHECK-NEXT: (nop) + ;; CHECK-NEXT: (global.set $__asyncify_ref_pos_func + ;; CHECK-NEXT: (i32.add + ;; CHECK-NEXT: (global.get $__asyncify_ref_pos_func) + ;; CHECK-NEXT: (i32.const -1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.set $5 + ;; CHECK-NEXT: (i32.load + ;; CHECK-NEXT: (global.get $__asyncify_data) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.set $0 + ;; CHECK-NEXT: (table.get $__asyncify_ref_table_func + ;; CHECK-NEXT: (i32.add + ;; CHECK-NEXT: (global.get $__asyncify_ref_pos_func) + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (table.set $__asyncify_ref_table_func + ;; CHECK-NEXT: (i32.add + ;; CHECK-NEXT: (global.get $__asyncify_ref_pos_func) + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (ref.null nofunc) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.set $2 + ;; CHECK-NEXT: (block $__asyncify_unwind (result i32) + ;; CHECK-NEXT: (block + ;; CHECK-NEXT: (block + ;; CHECK-NEXT: (if + ;; CHECK-NEXT: (i32.eq + ;; CHECK-NEXT: (global.get $__asyncify_state) + ;; CHECK-NEXT: (i32.const 2) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (then + ;; CHECK-NEXT: (i32.store + ;; CHECK-NEXT: (global.get $__asyncify_data) + ;; CHECK-NEXT: (i32.add + ;; CHECK-NEXT: (i32.load + ;; CHECK-NEXT: (global.get $__asyncify_data) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (i32.const -4) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.set $3 + ;; CHECK-NEXT: (i32.load + ;; CHECK-NEXT: (i32.load + ;; CHECK-NEXT: (global.get $__asyncify_data) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (block + ;; CHECK-NEXT: (if + ;; CHECK-NEXT: (i32.or + ;; CHECK-NEXT: (i32.eq + ;; CHECK-NEXT: (global.get $__asyncify_state) + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (i32.eq + ;; CHECK-NEXT: (local.get $3) + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (then + ;; CHECK-NEXT: (local.set $4 + ;; CHECK-NEXT: (call $get_func) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (if + ;; CHECK-NEXT: (i32.eq + ;; CHECK-NEXT: (global.get $__asyncify_state) + ;; CHECK-NEXT: (i32.const 1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (then + ;; CHECK-NEXT: (br $__asyncify_unwind + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (else + ;; CHECK-NEXT: (local.set $0 + ;; CHECK-NEXT: (ref.as_non_null + ;; CHECK-NEXT: (local.get $4) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (if + ;; CHECK-NEXT: (i32.eq + ;; CHECK-NEXT: (global.get $__asyncify_state) + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (then + ;; CHECK-NEXT: (local.set $1 + ;; CHECK-NEXT: (ref.is_null + ;; CHECK-NEXT: (ref.as_non_null + ;; CHECK-NEXT: (local.get $0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (return + ;; CHECK-NEXT: (local.get $1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (nop) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (unreachable) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (unreachable) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (block + ;; CHECK-NEXT: (i32.store + ;; CHECK-NEXT: (i32.load + ;; CHECK-NEXT: (global.get $__asyncify_data) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.get $2) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (i32.store + ;; CHECK-NEXT: (global.get $__asyncify_data) + ;; CHECK-NEXT: (i32.add + ;; CHECK-NEXT: (i32.load + ;; CHECK-NEXT: (global.get $__asyncify_data) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (i32.const 4) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (block + ;; CHECK-NEXT: (local.set $6 + ;; CHECK-NEXT: (i32.load + ;; CHECK-NEXT: (global.get $__asyncify_data) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (if + ;; CHECK-NEXT: (i32.gt_u + ;; CHECK-NEXT: (i32.add + ;; CHECK-NEXT: (global.get $__asyncify_ref_pos_func) + ;; CHECK-NEXT: (i32.const 1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (table.size $__asyncify_ref_table_func) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (then + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (table.grow $__asyncify_ref_table_func + ;; CHECK-NEXT: (ref.null nofunc) + ;; CHECK-NEXT: (i32.sub + ;; CHECK-NEXT: (i32.add + ;; CHECK-NEXT: (global.get $__asyncify_ref_pos_func) + ;; CHECK-NEXT: (i32.const 1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (table.size $__asyncify_ref_table_func) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (table.set $__asyncify_ref_table_func + ;; CHECK-NEXT: (i32.add + ;; CHECK-NEXT: (global.get $__asyncify_ref_pos_func) + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.get $0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (nop) + ;; CHECK-NEXT: (global.set $__asyncify_ref_pos_func + ;; CHECK-NEXT: (i32.add + ;; CHECK-NEXT: (global.get $__asyncify_ref_pos_func) + ;; CHECK-NEXT: (i32.const 1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + (func $call-result (export "call-result") + (result i32) + (ref.is_null (call $get_func)) + ) + + ;; A non-nullable local may be live at a call before it is initialized. It is + ;; made nullable and spilled as null, and its uses are read back with + ;; ref.as_non_null. + ;; CHECK: (func $uninitialized-local (type $2) (result i32) + ;; CHECK-NEXT: (local $f funcref) + ;; CHECK-NEXT: (local $1 funcref) + ;; CHECK-NEXT: (local $2 (ref func)) + ;; CHECK-NEXT: (local $3 i32) + ;; CHECK-NEXT: (local $4 i32) + ;; CHECK-NEXT: (local $5 i32) + ;; CHECK-NEXT: (local $6 i32) + ;; CHECK-NEXT: (local $7 i32) + ;; CHECK-NEXT: (local $8 i32) + ;; CHECK-NEXT: (local $9 i32) + ;; CHECK-NEXT: (if + ;; CHECK-NEXT: (i32.eq + ;; CHECK-NEXT: (global.get $__asyncify_state) + ;; CHECK-NEXT: (i32.const 2) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (then + ;; CHECK-NEXT: (i32.store + ;; CHECK-NEXT: (global.get $__asyncify_data) + ;; CHECK-NEXT: (i32.add + ;; CHECK-NEXT: (i32.load + ;; CHECK-NEXT: (global.get $__asyncify_data) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (i32.const -4) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (global.set $__asyncify_ref_pos_func + ;; CHECK-NEXT: (i32.add + ;; CHECK-NEXT: (global.get $__asyncify_ref_pos_func) + ;; CHECK-NEXT: (i32.const -2) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.set $8 + ;; CHECK-NEXT: (i32.load + ;; CHECK-NEXT: (global.get $__asyncify_data) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.set $f + ;; CHECK-NEXT: (table.get $__asyncify_ref_table_func + ;; CHECK-NEXT: (i32.add + ;; CHECK-NEXT: (global.get $__asyncify_ref_pos_func) + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.set $1 + ;; CHECK-NEXT: (table.get $__asyncify_ref_table_func + ;; CHECK-NEXT: (i32.add + ;; CHECK-NEXT: (global.get $__asyncify_ref_pos_func) + ;; CHECK-NEXT: (i32.const 1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.set $4 + ;; CHECK-NEXT: (i32.load + ;; CHECK-NEXT: (local.get $8) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (table.set $__asyncify_ref_table_func + ;; CHECK-NEXT: (i32.add + ;; CHECK-NEXT: (global.get $__asyncify_ref_pos_func) + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (ref.null nofunc) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (table.set $__asyncify_ref_table_func + ;; CHECK-NEXT: (i32.add + ;; CHECK-NEXT: (global.get $__asyncify_ref_pos_func) + ;; CHECK-NEXT: (i32.const 1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (ref.null nofunc) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.set $6 + ;; CHECK-NEXT: (block $__asyncify_unwind (result i32) + ;; CHECK-NEXT: (block + ;; CHECK-NEXT: (block + ;; CHECK-NEXT: (if + ;; CHECK-NEXT: (i32.eq + ;; CHECK-NEXT: (global.get $__asyncify_state) + ;; CHECK-NEXT: (i32.const 2) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (then + ;; CHECK-NEXT: (i32.store + ;; CHECK-NEXT: (global.get $__asyncify_data) + ;; CHECK-NEXT: (i32.add + ;; CHECK-NEXT: (i32.load + ;; CHECK-NEXT: (global.get $__asyncify_data) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (i32.const -4) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.set $7 + ;; CHECK-NEXT: (i32.load + ;; CHECK-NEXT: (i32.load + ;; CHECK-NEXT: (global.get $__asyncify_data) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (block + ;; CHECK-NEXT: (block + ;; CHECK-NEXT: (if + ;; CHECK-NEXT: (i32.or + ;; CHECK-NEXT: (i32.eq + ;; CHECK-NEXT: (global.get $__asyncify_state) + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (i32.eq + ;; CHECK-NEXT: (local.get $7) + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (then + ;; CHECK-NEXT: (call $unwind) + ;; CHECK-NEXT: (if + ;; CHECK-NEXT: (i32.eq + ;; CHECK-NEXT: (global.get $__asyncify_state) + ;; CHECK-NEXT: (i32.const 1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (then + ;; CHECK-NEXT: (br $__asyncify_unwind + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (if + ;; CHECK-NEXT: (i32.eq + ;; CHECK-NEXT: (global.get $__asyncify_state) + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (then + ;; CHECK-NEXT: (local.set $f + ;; CHECK-NEXT: (ref.func $target) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.set $1 + ;; CHECK-NEXT: (ref.as_non_null + ;; CHECK-NEXT: (local.get $f) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (nop) + ;; CHECK-NEXT: (if + ;; CHECK-NEXT: (i32.or + ;; CHECK-NEXT: (i32.eq + ;; CHECK-NEXT: (global.get $__asyncify_state) + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (i32.eq + ;; CHECK-NEXT: (local.get $7) + ;; CHECK-NEXT: (i32.const 1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (then + ;; CHECK-NEXT: (call $use + ;; CHECK-NEXT: (ref.as_non_null + ;; CHECK-NEXT: (local.get $1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (if + ;; CHECK-NEXT: (i32.eq + ;; CHECK-NEXT: (global.get $__asyncify_state) + ;; CHECK-NEXT: (i32.const 1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (then + ;; CHECK-NEXT: (br $__asyncify_unwind + ;; CHECK-NEXT: (i32.const 1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (if + ;; CHECK-NEXT: (i32.eq + ;; CHECK-NEXT: (global.get $__asyncify_state) + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (then + ;; CHECK-NEXT: (local.set $2 + ;; CHECK-NEXT: (ref.as_non_null + ;; CHECK-NEXT: (local.get $f) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.set $3 + ;; CHECK-NEXT: (ref.is_null + ;; CHECK-NEXT: (local.get $2) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.set $4 + ;; CHECK-NEXT: (local.get $3) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (nop) + ;; CHECK-NEXT: (nop) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (if + ;; CHECK-NEXT: (i32.eq + ;; CHECK-NEXT: (global.get $__asyncify_state) + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (then + ;; CHECK-NEXT: (local.set $5 + ;; CHECK-NEXT: (local.get $4) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (return + ;; CHECK-NEXT: (local.get $5) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (nop) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (unreachable) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (unreachable) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (block + ;; CHECK-NEXT: (i32.store + ;; CHECK-NEXT: (i32.load + ;; CHECK-NEXT: (global.get $__asyncify_data) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.get $6) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (i32.store + ;; CHECK-NEXT: (global.get $__asyncify_data) + ;; CHECK-NEXT: (i32.add + ;; CHECK-NEXT: (i32.load + ;; CHECK-NEXT: (global.get $__asyncify_data) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (i32.const 4) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (block + ;; CHECK-NEXT: (local.set $9 + ;; CHECK-NEXT: (i32.load + ;; CHECK-NEXT: (global.get $__asyncify_data) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (if + ;; CHECK-NEXT: (i32.gt_u + ;; CHECK-NEXT: (i32.add + ;; CHECK-NEXT: (global.get $__asyncify_ref_pos_func) + ;; CHECK-NEXT: (i32.const 2) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (table.size $__asyncify_ref_table_func) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (then + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (table.grow $__asyncify_ref_table_func + ;; CHECK-NEXT: (ref.null nofunc) + ;; CHECK-NEXT: (i32.sub + ;; CHECK-NEXT: (i32.add + ;; CHECK-NEXT: (global.get $__asyncify_ref_pos_func) + ;; CHECK-NEXT: (i32.const 2) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (table.size $__asyncify_ref_table_func) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (table.set $__asyncify_ref_table_func + ;; CHECK-NEXT: (i32.add + ;; CHECK-NEXT: (global.get $__asyncify_ref_pos_func) + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.get $f) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (table.set $__asyncify_ref_table_func + ;; CHECK-NEXT: (i32.add + ;; CHECK-NEXT: (global.get $__asyncify_ref_pos_func) + ;; CHECK-NEXT: (i32.const 1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.get $1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (i32.store + ;; CHECK-NEXT: (local.get $9) + ;; CHECK-NEXT: (local.get $4) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (i32.store + ;; CHECK-NEXT: (global.get $__asyncify_data) + ;; CHECK-NEXT: (i32.add + ;; CHECK-NEXT: (i32.load + ;; CHECK-NEXT: (global.get $__asyncify_data) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (i32.const 4) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (global.set $__asyncify_ref_pos_func + ;; CHECK-NEXT: (i32.add + ;; CHECK-NEXT: (global.get $__asyncify_ref_pos_func) + ;; CHECK-NEXT: (i32.const 2) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + (func $uninitialized-local (export "uninitialized-local") + (result i32) + (local $f (ref func)) + (call $unwind) + (local.set $f (ref.func $target)) + (call $use (local.get $f)) + (ref.is_null (local.get $f)) + ) + + ;; Exact references need their own tables, since the table element type must + ;; be exact as well. + ;; CHECK: (func $exact (type $8) (param $exact (ref (exact $A))) (param $inexact (ref $A)) (result i32) + ;; CHECK-NEXT: (local $e (ref null (exact $A))) + ;; CHECK-NEXT: (local $i (ref null $A)) + ;; CHECK-NEXT: (local $4 (ref (exact $A))) + ;; CHECK-NEXT: (local $5 (ref $A)) + ;; CHECK-NEXT: (local $6 (ref (exact $A))) + ;; CHECK-NEXT: (local $7 (ref (exact $A))) + ;; CHECK-NEXT: (local $8 i32) + ;; CHECK-NEXT: (local $9 (ref $A)) + ;; CHECK-NEXT: (local $10 (ref $A)) + ;; CHECK-NEXT: (local $11 i32) + ;; CHECK-NEXT: (local $12 i32) + ;; CHECK-NEXT: (local $13 i32) + ;; CHECK-NEXT: (local $14 i32) + ;; CHECK-NEXT: (local $15 i32) + ;; CHECK-NEXT: (local $16 i32) + ;; CHECK-NEXT: (local $17 i32) + ;; CHECK-NEXT: (local $18 i32) + ;; CHECK-NEXT: (if + ;; CHECK-NEXT: (i32.eq + ;; CHECK-NEXT: (global.get $__asyncify_state) + ;; CHECK-NEXT: (i32.const 2) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (then + ;; CHECK-NEXT: (i32.store + ;; CHECK-NEXT: (global.get $__asyncify_data) + ;; CHECK-NEXT: (i32.add + ;; CHECK-NEXT: (i32.load + ;; CHECK-NEXT: (global.get $__asyncify_data) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (i32.const -4) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (global.set $__asyncify_ref_pos_A_exact + ;; CHECK-NEXT: (i32.add + ;; CHECK-NEXT: (global.get $__asyncify_ref_pos_A_exact) + ;; CHECK-NEXT: (i32.const -1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (global.set $__asyncify_ref_pos_A + ;; CHECK-NEXT: (i32.add + ;; CHECK-NEXT: (global.get $__asyncify_ref_pos_A) + ;; CHECK-NEXT: (i32.const -1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.set $17 + ;; CHECK-NEXT: (i32.load + ;; CHECK-NEXT: (global.get $__asyncify_data) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.set $e + ;; CHECK-NEXT: (table.get $__asyncify_ref_table_A_exact + ;; CHECK-NEXT: (i32.add + ;; CHECK-NEXT: (global.get $__asyncify_ref_pos_A_exact) + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.set $i + ;; CHECK-NEXT: (table.get $__asyncify_ref_table_A + ;; CHECK-NEXT: (i32.add + ;; CHECK-NEXT: (global.get $__asyncify_ref_pos_A) + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.set $13 + ;; CHECK-NEXT: (i32.load + ;; CHECK-NEXT: (local.get $17) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (table.set $__asyncify_ref_table_A_exact + ;; CHECK-NEXT: (i32.add + ;; CHECK-NEXT: (global.get $__asyncify_ref_pos_A_exact) + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (ref.null none) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (table.set $__asyncify_ref_table_A + ;; CHECK-NEXT: (i32.add + ;; CHECK-NEXT: (global.get $__asyncify_ref_pos_A) + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (ref.null none) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.set $15 + ;; CHECK-NEXT: (block $__asyncify_unwind (result i32) + ;; CHECK-NEXT: (block + ;; CHECK-NEXT: (block + ;; CHECK-NEXT: (if + ;; CHECK-NEXT: (i32.eq + ;; CHECK-NEXT: (global.get $__asyncify_state) + ;; CHECK-NEXT: (i32.const 2) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (then + ;; CHECK-NEXT: (i32.store + ;; CHECK-NEXT: (global.get $__asyncify_data) + ;; CHECK-NEXT: (i32.add + ;; CHECK-NEXT: (i32.load + ;; CHECK-NEXT: (global.get $__asyncify_data) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (i32.const -4) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.set $16 + ;; CHECK-NEXT: (i32.load + ;; CHECK-NEXT: (i32.load + ;; CHECK-NEXT: (global.get $__asyncify_data) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (block + ;; CHECK-NEXT: (block + ;; CHECK-NEXT: (if + ;; CHECK-NEXT: (i32.eq + ;; CHECK-NEXT: (global.get $__asyncify_state) + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (then + ;; CHECK-NEXT: (local.set $4 + ;; CHECK-NEXT: (local.get $exact) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.set $e + ;; CHECK-NEXT: (local.get $4) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.set $5 + ;; CHECK-NEXT: (local.get $inexact) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.set $i + ;; CHECK-NEXT: (local.get $5) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (nop) + ;; CHECK-NEXT: (nop) + ;; CHECK-NEXT: (nop) + ;; CHECK-NEXT: (if + ;; CHECK-NEXT: (i32.or + ;; CHECK-NEXT: (i32.eq + ;; CHECK-NEXT: (global.get $__asyncify_state) + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (i32.eq + ;; CHECK-NEXT: (local.get $16) + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (then + ;; CHECK-NEXT: (call $unwind) + ;; CHECK-NEXT: (if + ;; CHECK-NEXT: (i32.eq + ;; CHECK-NEXT: (global.get $__asyncify_state) + ;; CHECK-NEXT: (i32.const 1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (then + ;; CHECK-NEXT: (br $__asyncify_unwind + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (if + ;; CHECK-NEXT: (i32.eq + ;; CHECK-NEXT: (global.get $__asyncify_state) + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (then + ;; CHECK-NEXT: (local.set $6 + ;; CHECK-NEXT: (ref.as_non_null + ;; CHECK-NEXT: (local.get $e) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.set $7 + ;; CHECK-NEXT: (ref.as_non_null + ;; CHECK-NEXT: (local.get $e) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.set $8 + ;; CHECK-NEXT: (ref.eq + ;; CHECK-NEXT: (local.get $6) + ;; CHECK-NEXT: (local.get $7) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.set $9 + ;; CHECK-NEXT: (ref.as_non_null + ;; CHECK-NEXT: (local.get $i) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.set $10 + ;; CHECK-NEXT: (ref.as_non_null + ;; CHECK-NEXT: (local.get $i) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.set $11 + ;; CHECK-NEXT: (ref.eq + ;; CHECK-NEXT: (local.get $9) + ;; CHECK-NEXT: (local.get $10) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.set $12 + ;; CHECK-NEXT: (i32.and + ;; CHECK-NEXT: (local.get $8) + ;; CHECK-NEXT: (local.get $11) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.set $13 + ;; CHECK-NEXT: (local.get $12) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (nop) + ;; CHECK-NEXT: (nop) + ;; CHECK-NEXT: (nop) + ;; CHECK-NEXT: (nop) + ;; CHECK-NEXT: (nop) + ;; CHECK-NEXT: (nop) + ;; CHECK-NEXT: (nop) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (if + ;; CHECK-NEXT: (i32.eq + ;; CHECK-NEXT: (global.get $__asyncify_state) + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (then + ;; CHECK-NEXT: (local.set $14 + ;; CHECK-NEXT: (local.get $13) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (return + ;; CHECK-NEXT: (local.get $14) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (nop) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (unreachable) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (unreachable) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (block + ;; CHECK-NEXT: (i32.store + ;; CHECK-NEXT: (i32.load + ;; CHECK-NEXT: (global.get $__asyncify_data) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.get $15) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (i32.store + ;; CHECK-NEXT: (global.get $__asyncify_data) + ;; CHECK-NEXT: (i32.add + ;; CHECK-NEXT: (i32.load + ;; CHECK-NEXT: (global.get $__asyncify_data) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (i32.const 4) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (block + ;; CHECK-NEXT: (local.set $18 + ;; CHECK-NEXT: (i32.load + ;; CHECK-NEXT: (global.get $__asyncify_data) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (if + ;; CHECK-NEXT: (i32.gt_u + ;; CHECK-NEXT: (i32.add + ;; CHECK-NEXT: (global.get $__asyncify_ref_pos_A_exact) + ;; CHECK-NEXT: (i32.const 1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (table.size $__asyncify_ref_table_A_exact) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (then + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (table.grow $__asyncify_ref_table_A_exact + ;; CHECK-NEXT: (ref.null none) + ;; CHECK-NEXT: (i32.sub + ;; CHECK-NEXT: (i32.add + ;; CHECK-NEXT: (global.get $__asyncify_ref_pos_A_exact) + ;; CHECK-NEXT: (i32.const 1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (table.size $__asyncify_ref_table_A_exact) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (if + ;; CHECK-NEXT: (i32.gt_u + ;; CHECK-NEXT: (i32.add + ;; CHECK-NEXT: (global.get $__asyncify_ref_pos_A) + ;; CHECK-NEXT: (i32.const 1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (table.size $__asyncify_ref_table_A) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (then + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (table.grow $__asyncify_ref_table_A + ;; CHECK-NEXT: (ref.null none) + ;; CHECK-NEXT: (i32.sub + ;; CHECK-NEXT: (i32.add + ;; CHECK-NEXT: (global.get $__asyncify_ref_pos_A) + ;; CHECK-NEXT: (i32.const 1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (table.size $__asyncify_ref_table_A) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (table.set $__asyncify_ref_table_A_exact + ;; CHECK-NEXT: (i32.add + ;; CHECK-NEXT: (global.get $__asyncify_ref_pos_A_exact) + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.get $e) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (table.set $__asyncify_ref_table_A + ;; CHECK-NEXT: (i32.add + ;; CHECK-NEXT: (global.get $__asyncify_ref_pos_A) + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.get $i) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (i32.store + ;; CHECK-NEXT: (local.get $18) + ;; CHECK-NEXT: (local.get $13) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (i32.store + ;; CHECK-NEXT: (global.get $__asyncify_data) + ;; CHECK-NEXT: (i32.add + ;; CHECK-NEXT: (i32.load + ;; CHECK-NEXT: (global.get $__asyncify_data) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (i32.const 4) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (global.set $__asyncify_ref_pos_A_exact + ;; CHECK-NEXT: (i32.add + ;; CHECK-NEXT: (global.get $__asyncify_ref_pos_A_exact) + ;; CHECK-NEXT: (i32.const 1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (global.set $__asyncify_ref_pos_A + ;; CHECK-NEXT: (i32.add + ;; CHECK-NEXT: (global.get $__asyncify_ref_pos_A) + ;; CHECK-NEXT: (i32.const 1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + (func $exact (export "exact") + (param $exact (ref (exact $A))) + (param $inexact (ref $A)) + (result i32) + (local $e (ref (exact $A))) + (local $i (ref $A)) + (local.set $e (local.get $exact)) + (local.set $i (local.get $inexact)) + (call $unwind) + (i32.and + (ref.eq (local.get $e) (local.get $e)) + (ref.eq (local.get $i) (local.get $i))) ) ) +;; CHECK: (func $asyncify_start_unwind (type $3) (param $0 i32) +;; CHECK-NEXT: (global.set $__asyncify_state +;; CHECK-NEXT: (i32.const 1) +;; CHECK-NEXT: ) +;; CHECK-NEXT: (global.set $__asyncify_data +;; CHECK-NEXT: (local.get $0) +;; CHECK-NEXT: ) +;; CHECK-NEXT: (if +;; CHECK-NEXT: (i32.ne +;; CHECK-NEXT: (global.get $__asyncify_ref_pos_extern) +;; CHECK-NEXT: (i32.const 0) +;; CHECK-NEXT: ) +;; CHECK-NEXT: (then +;; CHECK-NEXT: (unreachable) +;; CHECK-NEXT: ) +;; CHECK-NEXT: ) +;; CHECK-NEXT: (global.set $__asyncify_ref_pos_extern +;; CHECK-NEXT: (i32.const 0) +;; CHECK-NEXT: ) +;; CHECK-NEXT: (if +;; CHECK-NEXT: (i32.ne +;; CHECK-NEXT: (global.get $__asyncify_ref_pos_func) +;; CHECK-NEXT: (i32.const 0) +;; CHECK-NEXT: ) +;; CHECK-NEXT: (then +;; CHECK-NEXT: (unreachable) +;; CHECK-NEXT: ) +;; CHECK-NEXT: ) +;; CHECK-NEXT: (global.set $__asyncify_ref_pos_func +;; CHECK-NEXT: (i32.const 0) +;; CHECK-NEXT: ) +;; CHECK-NEXT: (if +;; CHECK-NEXT: (i32.ne +;; CHECK-NEXT: (global.get $__asyncify_ref_pos_A_exact) +;; CHECK-NEXT: (i32.const 0) +;; CHECK-NEXT: ) +;; CHECK-NEXT: (then +;; CHECK-NEXT: (unreachable) +;; CHECK-NEXT: ) +;; CHECK-NEXT: ) +;; CHECK-NEXT: (global.set $__asyncify_ref_pos_A_exact +;; CHECK-NEXT: (i32.const 0) +;; CHECK-NEXT: ) +;; CHECK-NEXT: (if +;; CHECK-NEXT: (i32.ne +;; CHECK-NEXT: (global.get $__asyncify_ref_pos_A) +;; CHECK-NEXT: (i32.const 0) +;; CHECK-NEXT: ) +;; CHECK-NEXT: (then +;; CHECK-NEXT: (unreachable) +;; CHECK-NEXT: ) +;; CHECK-NEXT: ) +;; CHECK-NEXT: (global.set $__asyncify_ref_pos_A +;; CHECK-NEXT: (i32.const 0) +;; CHECK-NEXT: ) +;; CHECK-NEXT: (if +;; CHECK-NEXT: (i32.gt_u +;; CHECK-NEXT: (i32.load +;; CHECK-NEXT: (global.get $__asyncify_data) +;; CHECK-NEXT: ) +;; CHECK-NEXT: (i32.load offset=4 +;; CHECK-NEXT: (global.get $__asyncify_data) +;; CHECK-NEXT: ) +;; CHECK-NEXT: ) +;; CHECK-NEXT: (then +;; CHECK-NEXT: (unreachable) +;; CHECK-NEXT: ) +;; CHECK-NEXT: ) +;; CHECK-NEXT: ) + +;; CHECK: (func $asyncify_stop_unwind (type $1) +;; CHECK-NEXT: (global.set $__asyncify_state +;; CHECK-NEXT: (i32.const 0) +;; CHECK-NEXT: ) +;; CHECK-NEXT: (if +;; CHECK-NEXT: (i32.gt_u +;; CHECK-NEXT: (i32.load +;; CHECK-NEXT: (global.get $__asyncify_data) +;; CHECK-NEXT: ) +;; CHECK-NEXT: (i32.load offset=4 +;; CHECK-NEXT: (global.get $__asyncify_data) +;; CHECK-NEXT: ) +;; CHECK-NEXT: ) +;; CHECK-NEXT: (then +;; CHECK-NEXT: (unreachable) +;; CHECK-NEXT: ) +;; CHECK-NEXT: ) +;; CHECK-NEXT: ) + +;; CHECK: (func $asyncify_start_rewind (type $3) (param $0 i32) +;; CHECK-NEXT: (global.set $__asyncify_state +;; CHECK-NEXT: (i32.const 2) +;; CHECK-NEXT: ) +;; CHECK-NEXT: (global.set $__asyncify_data +;; CHECK-NEXT: (local.get $0) +;; CHECK-NEXT: ) +;; CHECK-NEXT: (if +;; CHECK-NEXT: (i32.gt_u +;; CHECK-NEXT: (i32.load +;; CHECK-NEXT: (global.get $__asyncify_data) +;; CHECK-NEXT: ) +;; CHECK-NEXT: (i32.load offset=4 +;; CHECK-NEXT: (global.get $__asyncify_data) +;; CHECK-NEXT: ) +;; CHECK-NEXT: ) +;; CHECK-NEXT: (then +;; CHECK-NEXT: (unreachable) +;; CHECK-NEXT: ) +;; CHECK-NEXT: ) +;; CHECK-NEXT: ) + +;; CHECK: (func $asyncify_stop_rewind (type $1) +;; CHECK-NEXT: (global.set $__asyncify_state +;; CHECK-NEXT: (i32.const 0) +;; CHECK-NEXT: ) +;; CHECK-NEXT: (if +;; CHECK-NEXT: (i32.gt_u +;; CHECK-NEXT: (i32.load +;; CHECK-NEXT: (global.get $__asyncify_data) +;; CHECK-NEXT: ) +;; CHECK-NEXT: (i32.load offset=4 +;; CHECK-NEXT: (global.get $__asyncify_data) +;; CHECK-NEXT: ) +;; CHECK-NEXT: ) +;; CHECK-NEXT: (then +;; CHECK-NEXT: (unreachable) +;; CHECK-NEXT: ) +;; CHECK-NEXT: ) +;; CHECK-NEXT: ) -;; CHECK: (global $__asyncify_ref_pos_extern (mut i32) (i32.const 0)) -;; CHECK: (table $__asyncify_ref_table_extern 1 67108864 externref) - -;; On rewind, the externref local is restored from the table at the cursor. -;; CHECK: (global.set $__asyncify_ref_pos_extern -;; CHECK: (local.set $r -;; CHECK: (table.get $__asyncify_ref_table_extern -;; CHECK: (i32.add -;; CHECK: (global.get $__asyncify_ref_pos_extern) - -;; The restored slot is then cleared: a table is a GC root, so leaving the -;; reference there would retain the object for the life of the instance. -;; CHECK: (table.set $__asyncify_ref_table_extern -;; CHECK: (global.get $__asyncify_ref_pos_extern) -;; CHECK: (ref.null noextern) - -;; On unwind, the table is grown if the cursor has reached the current size, and -;; the local is spilled to the table. Growth is what makes recursion safe. -;; CHECK: (table.size $__asyncify_ref_table_extern) -;; CHECK: (table.grow $__asyncify_ref_table_extern -;; CHECK: (table.set $__asyncify_ref_table_extern -;; CHECK: (global.get $__asyncify_ref_pos_extern) - -;; The cursor is reset at the start of each unwind. -;; A nonzero cursor at the start of an unwind means two pauses overlap, which -;; asyncify does not support, so it traps before resetting the cursor. -;; CHECK: (func $asyncify_start_unwind -;; CHECK: (i32.ne -;; CHECK: (global.get $__asyncify_ref_pos_extern) -;; CHECK: (i32.const 0) -;; CHECK: (unreachable) -;; CHECK: (global.set $__asyncify_ref_pos_extern -;; CHECK: (i32.const 0) +;; CHECK: (func $asyncify_get_state (type $2) (result i32) +;; CHECK-NEXT: (global.get $__asyncify_state) +;; CHECK-NEXT: ) From 445ee53bf00b07f29402f80bc215e499c81b5358 Mon Sep 17 00:00:00 2001 From: NellowTCS Date: Sun, 8 Nov 2026 22:17:45 -0700 Subject: [PATCH 5/5] format --- src/passes/Asyncify.cpp | 32 +++++++++++++++++--------------- 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/src/passes/Asyncify.cpp b/src/passes/Asyncify.cpp index 07cfc0c3fed..f3c83e8e266 100644 --- a/src/passes/Asyncify.cpp +++ b/src/passes/Asyncify.cpp @@ -460,10 +460,12 @@ class FakeGlobalHelper { } // The name of the fake global used to stash a call result of this type - Name getName(Type type) { return getNameForStorageType(getStorageType(type)); } + Name getName(Type type) { + return getNameForStorageType(getStorageType(type)); + } // The type a call result is stored as. - // Non-defaultable reference types are stored as their nullable versions, + // Non-defaultable reference types are stored as their nullable versions, // and readers cast back with ref.as_non_null Type getStorageType(Type type) { return TypeUpdating::getValidLocalType(type, module.features); @@ -956,9 +958,9 @@ class AsyncifyBuilder : public Builder { // A spill slot is a GC root, so once a reference has been restored into its // local the slot must be cleared to stop the table retaining the object. Expression* makeClearRefSlot(Type type, Index offset) { - return makeTableSet( - getRefTable(type), makeRefIndex(type, offset), - LiteralUtils::makeZero(type, wasm)); + return makeTableSet(getRefTable(type), + makeRefIndex(type, offset), + LiteralUtils::makeZero(type, wasm)); } Expression* makeIncRefPos(Type type, int32_t by) { @@ -1316,8 +1318,8 @@ struct AsyncifyFlow : public Pass { for (Index i = 0, size = type.size(); i < size; ++i) { // Each component needs its own global.get: the IR is a tree, so an // Expression* cannot be shared. - Expression* elem = builder->makeTupleExtract( - builder->makeGlobalGet(name, storageType), i); + Expression* elem = + builder->makeTupleExtract(builder->makeGlobalGet(name, storageType), i); if (type[i].isNonNullableRef()) { elem = builder->makeRefAs(RefAsNonNull, elem); } @@ -1685,7 +1687,8 @@ struct AsyncifyLocals : public WalkerPass> { // alive at a relevant call site must be handled, but others can be ignored. findRelevantLiveLocals(func); // A spilled local may not be initialized before its first call, so make - // non-nullable ones nullable. Reads/writes are cast back with ref.as_non_null. + // non-nullable ones nullable. Reads/writes are cast back with + // ref.as_non_null. makeRelevantLocalsDefaultable(func); // The new function body has a prelude to load locals if rewinding, // then the actual main body, which does all its unwindings by breaking @@ -1783,10 +1786,10 @@ struct AsyncifyLocals : public WalkerPass> { if (type.isRef()) { // The table is nullable, so a non-nullable local needs a cast back. auto storageType = type.with(Nullable); - Expression* load = builder->makeTableGet( - builder->getRefTable(storageType), - cursors.nextRef(storageType), - storageType); + Expression* load = + builder->makeTableGet(builder->getRefTable(storageType), + cursors.nextRef(storageType), + storageType); if (type.isNonNullableRef()) { load = builder->makeRefAs(RefAsNonNull, load); } @@ -1812,9 +1815,8 @@ struct AsyncifyLocals : public WalkerPass> { SpillCursors& cursors) { if (type.isRef()) { auto storageType = type.with(Nullable); - return builder->makeTableSet(builder->getRefTable(storageType), - cursors.nextRef(storageType), - value); + return builder->makeTableSet( + builder->getRefTable(storageType), cursors.nextRef(storageType), value); } auto size = getByteSize(type); assert(size % STACK_ALIGN == 0);