From ad26e65ab9c2c19e5dcdab5a87133fbd94050c61 Mon Sep 17 00:00:00 2001 From: Thomas Lively Date: Sat, 31 Oct 2026 22:49:31 -0700 Subject: [PATCH 1/6] [IRBuilder] Use multiple scratch locals for inputs Instead of using a single tuple-typed scratch local for multivalue control flow inputs, allocate one scalar scratch local per input value. This avoids eagerly packaging scalar inputs into a tuple on scope entry. It also avoids allocating extra tuple scratch locals to extract elements when inputs are consumed inside the scope. Scalar input locals do have some downsides. Scopes that pass all of their inputs through untouched must now repackage them with tuple.make. Multivalue loop trampolines in fixLoopWithInput also still need a tuple scratch local to unpack branch values into the scalar input locals. However, real parameterized scopes usually consume their inputs as individual values. Scalar input locals can also be coalesced directly by standard local passes without tuple optimization, and they will allow individual parameters to be tunneled across nested scopes with different arities in a future commit. --- src/wasm-ir-builder.h | 49 +--- src/wasm/wasm-ir-builder.cpp | 99 ++++++-- test/lit/basic/extra-branch-values.wast | 119 +++++----- test/lit/control-flow-input.wast | 298 +++++++++++++++++------- test/lit/control-flow-input.wast.wasm | Bin 793 -> 933 bytes 5 files changed, 368 insertions(+), 197 deletions(-) diff --git a/src/wasm-ir-builder.h b/src/wasm-ir-builder.h index 0cc4e3cb727..3561a40bad6 100644 --- a/src/wasm-ir-builder.h +++ b/src/wasm-ir-builder.h @@ -433,10 +433,10 @@ class IRBuilder : public UnifiedExpressionVisitor> { // function's `explicitLabelNames`. bool labelExplicit = false; - // If the control flow scope has an input type, we need to lower it using a - // scratch local because we cannot represent control flow input in the IR. + // If the control flow scope has an input type, we need to lower it using + // scratch locals because we cannot represent control flow input in the IR. Type inputType; - Index inputLocal = -1; + std::vector inputLocals; // If there are br_on_*, try_table, or resume branches that target this // scope and carry additional values, we need to use a scratch local to @@ -461,13 +461,6 @@ class IRBuilder : public UnifiedExpressionVisitor> { ScopeCtx() : scope(NoScope{}) {} ScopeCtx(Scope scope, Type inputType) : scope(scope), inputType(inputType) {} - ScopeCtx( - Scope scope, Name label, bool labelUsed, Type inputType, Index inputLocal) - : scope(scope), label(label), labelUsed(labelUsed), inputType(inputType), - inputLocal(inputLocal) {} - ScopeCtx(Scope scope, Name label, bool labelUsed, Name branchLabel) - : scope(scope), label(label), branchLabel(branchLabel), - labelUsed(labelUsed) {} static ScopeCtx makeFunc(Function* func) { return ScopeCtx(FuncScope{func}, Type::none); @@ -514,7 +507,7 @@ class IRBuilder : public UnifiedExpressionVisitor> { unreachable = false; if (!keepInput) { inputType = Type::none; - inputLocal = -1; + inputLocals.clear(); } } bool isNone() { return std::get_if(&scope); } @@ -715,35 +708,7 @@ class IRBuilder : public UnifiedExpressionVisitor> { Index blockHint = 0; Index labelHint = 0; - Result<> pushScope(ScopeCtx&& scope) { - if (auto label = scope.getOriginalLabel()) { - // Assign a fresh label to the scope, if necessary. - if (!scope.label) { - scope.label = makeFresh(label); - } - // Record the original label to handle references to it correctly. - labelDepths[label].push_back(scopeStack.size() + 1); - } - if (binaryPos) { - scope.startPos = lastBinaryPos; - lastBinaryPos = *binaryPos; - } - bool hasInput = scope.inputType != Type::none; - Index inputLocal = scope.inputLocal; - if (hasInput && !scope.isDelimiter()) { - if (inputLocal == Index(-1)) { - auto scratch = addScratchLocal(scope.inputType); - CHECK_ERR(scratch); - inputLocal = scope.inputLocal = *scratch; - } - CHECK_ERR(makeLocalSet(inputLocal)); - } - scopeStack.emplace_back(std::move(scope)); - if (hasInput) { - CHECK_ERR(makeLocalGet(inputLocal)); - } - return Ok{}; - } + Result<> pushScope(ScopeCtx&& scope); ScopeCtx& getScope() { if (scopeStack.empty()) { @@ -806,7 +771,9 @@ class IRBuilder : public UnifiedExpressionVisitor> { Result> getExtraOutputLocalAndLabel(Index label, size_t extraArity); Expression* fixExtraOutput(ScopeCtx& scope, Name label, Expression* expr); - void fixLoopWithInput(Loop* loop, Type inputType, Index scratch); + void fixLoopWithInput(Loop* loop, + Type inputType, + const std::vector& scratch); void applyAnnotations(Expression* expr, const CodeAnnotation& annotation); diff --git a/src/wasm/wasm-ir-builder.cpp b/src/wasm/wasm-ir-builder.cpp index 78151f238dd..3535561db2f 100644 --- a/src/wasm/wasm-ir-builder.cpp +++ b/src/wasm/wasm-ir-builder.cpp @@ -778,6 +778,16 @@ struct IRBuilder::ChildPopper ConstraintCollector{builder, children}.visitStackSwitch(curr, ct); return popConstrainedChildren(children); } + + Result<> popInputs(Type inputType, std::vector& inputs) { + inputs.resize(inputType.size()); + std::vector children; + children.reserve(inputType.size()); + for (size_t i = 0; i < inputType.size(); ++i) { + children.push_back({&inputs[i], {inputType[i]}}); + } + return popConstrainedChildren(children); + } }; Result<> IRBuilder::visit(Expression* curr) { @@ -877,6 +887,42 @@ IRBuilder::visitTryTableStart(TryTable* trytable, Name label, Type inputType) { return pushScope(ScopeCtx::makeTryTable(trytable, label, inputType)); } +Result<> IRBuilder::pushScope(ScopeCtx&& scope) { + if (auto label = scope.getOriginalLabel()) { + // Assign a fresh label to the scope, if necessary. + if (!scope.label) { + scope.label = makeFresh(label); + } + // Record the original label to handle references to it correctly. + labelDepths[label].push_back(scopeStack.size() + 1); + } + if (binaryPos) { + scope.startPos = lastBinaryPos; + lastBinaryPos = *binaryPos; + } + bool hasInput = scope.inputType != Type::none; + if (hasInput && !scope.isDelimiter()) { + std::vector inputs; + CHECK_ERR(ChildPopper{*this}.popInputs(scope.inputType, inputs)); + scope.inputLocals.reserve(inputs.size()); + for (size_t i = 0; i < inputs.size(); ++i) { + auto scratch = addScratchLocal(scope.inputType[i]); + CHECK_ERR(scratch); + scope.inputLocals.push_back(*scratch); + pushSynthetic(builder.makeLocalSet(*scratch, inputs[i])); + } + } + scopeStack.emplace_back(std::move(scope)); + if (hasInput) { + auto& newScope = getScope(); + for (size_t i = 0; i < newScope.inputLocals.size(); ++i) { + pushSynthetic( + builder.makeLocalGet(newScope.inputLocals[i], newScope.inputType[i])); + } + } + return Ok{}; +} + Result IRBuilder::finishScope(Block* block) { #if IR_BUILDER_DEBUG if (auto* loc = std::get_if(&debugLoc)) { @@ -1212,8 +1258,8 @@ Result<> IRBuilder::visitEnd() { if (scope.inputType != Type::none && scope.labelUsed) { // Branches to this loop carry values, but Binaryen IR does not support // that. Fix this by trampolining the branches through new code that sets - // the branch value to the appropriate scratch local. - fixLoopWithInput(loop, scope.inputType, scope.inputLocal); + // the branch value to the appropriate scratch locals. + fixLoopWithInput(loop, scope.inputType, scope.inputLocals); } loop->finalize(loop->type); push(loop); @@ -1222,8 +1268,19 @@ Result<> IRBuilder::visitEnd() { if (scope.inputType != Type::none) { // Normally an if without an else must have type none, but if there is an // input parameter, the empty else arm must propagate its value. - // Synthesize an else arm that loads the value from the scratch local. - iff->ifFalse = builder.makeLocalGet(scope.inputLocal, scope.inputType); + // Synthesize an else arm that loads the value from the scratch locals. + if (scope.inputType.isSingle()) { + iff->ifFalse = + builder.makeLocalGet(scope.inputLocals[0], scope.inputType); + } else { + std::vector gets; + gets.reserve(scope.inputLocals.size()); + for (size_t i = 0; i < scope.inputLocals.size(); ++i) { + gets.push_back( + builder.makeLocalGet(scope.inputLocals[i], scope.inputType[i])); + } + iff->ifFalse = builder.makeTupleMake(gets); + } } else { iff->ifFalse = nullptr; } @@ -1368,17 +1425,18 @@ IRBuilder::fixExtraOutput(ScopeCtx& scope, Name label, Expression* curr) { return curr; } -// Branches to this loop need to be trampolined through code that sets the value -// carried by the branch to the appropriate scratch local before branching to -// the loop. Transform this: +// Branches to this loop need to be trampolined through code that sets the +// values carried by the branch to the appropriate scratch locals before +// branching to the loop. Transform this: // // (loop $l (param t1) (result t2) ...) // -// to this: +// to this (or, when t1 is a tuple, using a tuple scratch local to extract each +// element into its corresponding input scratch local): // // (loop $l0 (result t2) // (block $l1 (result t2) -// (local.set $scratch ;; set the branch values to the scratch local +// (local.set $scratch ;; set the branch value to the scratch local // (block $l (result t1) // (br $l1 ;; exit the loop with the fallthrough value, if any. // ... ;; contains branches to $l @@ -1388,7 +1446,9 @@ IRBuilder::fixExtraOutput(ScopeCtx& scope, Name label, Expression* curr) { // (br $l0) ;; continue the loop // ) // ) -void IRBuilder::fixLoopWithInput(Loop* loop, Type inputType, Index scratch) { +void IRBuilder::fixLoopWithInput(Loop* loop, + Type inputType, + const std::vector& scratch) { auto l = loop->name; auto l0 = makeFresh(l, 0); auto l1 = makeFresh(l, 1); @@ -1399,10 +1459,21 @@ void IRBuilder::fixLoopWithInput(Loop* loop, Type inputType, Index scratch) { loop->body, l, builder.makeBreak(l1), inputType) : builder.makeBlock(l, {builder.makeBreak(l1, loop->body)}, inputType); - Block* outer = builder.makeBlock( - l1, - {builder.makeLocalSet(scratch, inner), builder.makeBreak(l0)}, - loop->type); + std::vector list; + if (inputType.isSingle()) { + list.push_back(builder.makeLocalSet(scratch[0], inner)); + } else { + auto tupleScratch = *addScratchLocal(inputType); + list.push_back(builder.makeLocalSet(tupleScratch, inner)); + for (Index i = 0; i < scratch.size(); ++i) { + list.push_back(builder.makeLocalSet( + scratch[i], + builder.makeTupleExtract(builder.makeLocalGet(tupleScratch, inputType), + i))); + } + } + list.push_back(builder.makeBreak(l0)); + Block* outer = builder.makeBlock(l1, list, loop->type); loop->body = outer; loop->name = l0; diff --git a/test/lit/basic/extra-branch-values.wast b/test/lit/basic/extra-branch-values.wast index 953ca7d634f..8f0980c6b9a 100644 --- a/test/lit/basic/extra-branch-values.wast +++ b/test/lit/basic/extra-branch-values.wast @@ -1931,23 +1931,24 @@ ) ;; CHECK: (func $loop (type $24) (param $0 i32) (param $1 anyref) - ;; CHECK-NEXT: (local $scratch (tuple i32 anyref)) - ;; CHECK-NEXT: (local $scratch_3 (tuple i32 anyref)) + ;; CHECK-NEXT: (local $scratch i32) + ;; CHECK-NEXT: (local $scratch_3 anyref) ;; CHECK-NEXT: (local $scratch_4 anyref) ;; CHECK-NEXT: (local $scratch_5 i32) ;; CHECK-NEXT: (local $scratch_6 i32) ;; CHECK-NEXT: (local $scratch_7 (ref any)) ;; CHECK-NEXT: (local $scratch_8 i32) ;; CHECK-NEXT: (local $scratch_9 anyref) + ;; CHECK-NEXT: (local $scratch_10 (tuple i32 anyref)) ;; CHECK-NEXT: (local.set $scratch - ;; CHECK-NEXT: (tuple.make 2 - ;; CHECK-NEXT: (local.get $0) - ;; CHECK-NEXT: (local.get $1) - ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.get $0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.set $scratch_3 + ;; CHECK-NEXT: (local.get $1) ;; CHECK-NEXT: ) ;; CHECK-NEXT: (loop $label2 ;; CHECK-NEXT: (block $label3 - ;; CHECK-NEXT: (local.set $scratch + ;; CHECK-NEXT: (local.set $scratch_10 ;; CHECK-NEXT: (block $label (type $2) (result i32 anyref) ;; CHECK-NEXT: (block $label1 ;; CHECK-NEXT: (local.set $scratch_9 @@ -1955,16 +1956,10 @@ ;; CHECK-NEXT: (local.set $scratch_5 ;; CHECK-NEXT: (block (result i32) ;; CHECK-NEXT: (local.set $scratch_6 - ;; CHECK-NEXT: (tuple.extract 2 0 - ;; CHECK-NEXT: (local.tee $scratch_3 - ;; CHECK-NEXT: (local.get $scratch) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.get $scratch) ;; CHECK-NEXT: ) ;; CHECK-NEXT: (local.set $scratch_4 - ;; CHECK-NEXT: (tuple.extract 2 1 - ;; CHECK-NEXT: (local.get $scratch_3) - ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.get $scratch_3) ;; CHECK-NEXT: ) ;; CHECK-NEXT: (local.get $scratch_6) ;; CHECK-NEXT: ) @@ -1998,6 +1993,16 @@ ;; CHECK-NEXT: (br $label3) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.set $scratch + ;; CHECK-NEXT: (tuple.extract 2 0 + ;; CHECK-NEXT: (local.get $scratch_10) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.set $scratch_3 + ;; CHECK-NEXT: (tuple.extract 2 1 + ;; CHECK-NEXT: (local.get $scratch_10) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) ;; CHECK-NEXT: (br $label2) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) @@ -2033,22 +2038,23 @@ ) ;; CHECK: (func $loop-results (type $8) (param $0 i32) (param $1 anyref) (result i32 anyref) - ;; CHECK-NEXT: (local $scratch (tuple i32 anyref)) - ;; CHECK-NEXT: (local $scratch_3 (tuple i32 anyref)) + ;; CHECK-NEXT: (local $scratch i32) + ;; CHECK-NEXT: (local $scratch_3 anyref) ;; CHECK-NEXT: (local $scratch_4 anyref) ;; CHECK-NEXT: (local $scratch_5 i32) ;; CHECK-NEXT: (local $scratch_6 i32) ;; CHECK-NEXT: (local $scratch_7 (ref any)) ;; CHECK-NEXT: (local $scratch_8 anyref) + ;; CHECK-NEXT: (local $scratch_9 (tuple i32 anyref)) ;; CHECK-NEXT: (local.set $scratch - ;; CHECK-NEXT: (tuple.make 2 - ;; CHECK-NEXT: (local.get $0) - ;; CHECK-NEXT: (local.get $1) - ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.get $0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.set $scratch_3 + ;; CHECK-NEXT: (local.get $1) ;; CHECK-NEXT: ) ;; CHECK-NEXT: (loop $label2 (type $2) (result i32 anyref) ;; CHECK-NEXT: (block $label3 (type $2) (result i32 anyref) - ;; CHECK-NEXT: (local.set $scratch + ;; CHECK-NEXT: (local.set $scratch_9 ;; CHECK-NEXT: (block $label (type $2) (result i32 anyref) ;; CHECK-NEXT: (br $label3 ;; CHECK-NEXT: (block $label1 (type $2) (result i32 anyref) @@ -2057,16 +2063,10 @@ ;; CHECK-NEXT: (local.set $scratch_5 ;; CHECK-NEXT: (block (result i32) ;; CHECK-NEXT: (local.set $scratch_6 - ;; CHECK-NEXT: (tuple.extract 2 0 - ;; CHECK-NEXT: (local.tee $scratch_3 - ;; CHECK-NEXT: (local.get $scratch) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.get $scratch) ;; CHECK-NEXT: ) ;; CHECK-NEXT: (local.set $scratch_4 - ;; CHECK-NEXT: (tuple.extract 2 1 - ;; CHECK-NEXT: (local.get $scratch_3) - ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.get $scratch_3) ;; CHECK-NEXT: ) ;; CHECK-NEXT: (local.get $scratch_6) ;; CHECK-NEXT: ) @@ -2094,53 +2094,56 @@ ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.set $scratch + ;; CHECK-NEXT: (tuple.extract 2 0 + ;; CHECK-NEXT: (local.get $scratch_9) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.set $scratch_3 + ;; CHECK-NEXT: (tuple.extract 2 1 + ;; CHECK-NEXT: (local.get $scratch_9) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) ;; CHECK-NEXT: (br $label2) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) ;; OPT_O: (func $loop-results (type $8) (param $0 i32) (param $1 anyref) (result i32 anyref) ;; OPT_O-NEXT: (local $2 (tuple i32 anyref)) - ;; OPT_O-NEXT: (local $3 eqref) - ;; OPT_O-NEXT: (local.set $2 - ;; OPT_O-NEXT: (tuple.make 2 - ;; OPT_O-NEXT: (local.get $0) - ;; OPT_O-NEXT: (local.get $1) - ;; OPT_O-NEXT: ) - ;; OPT_O-NEXT: ) ;; OPT_O-NEXT: (loop $label2 (type $2) (result i32 (ref any)) ;; OPT_O-NEXT: (block $label3 (type $2) (result i32 (ref any)) - ;; OPT_O-NEXT: (local.set $2 - ;; OPT_O-NEXT: (block $label (type $0) (result i32 eqref) - ;; OPT_O-NEXT: (br $label3 - ;; OPT_O-NEXT: (block $label1 (type $2) (result i32 (ref any)) - ;; OPT_O-NEXT: (local.set $3 - ;; OPT_O-NEXT: (block $label0 (result eqref) - ;; OPT_O-NEXT: (br $label1 + ;; OPT_O-NEXT: (local.set $0 + ;; OPT_O-NEXT: (tuple.extract 2 0 + ;; OPT_O-NEXT: (local.tee $2 + ;; OPT_O-NEXT: (block $label (type $0) (result i32 eqref) + ;; OPT_O-NEXT: (br $label3 + ;; OPT_O-NEXT: (block $label1 (type $2) (result i32 (ref any)) + ;; OPT_O-NEXT: (br $label ;; OPT_O-NEXT: (tuple.make 2 - ;; OPT_O-NEXT: (local.tee $0 - ;; OPT_O-NEXT: (tuple.extract 2 0 - ;; OPT_O-NEXT: (local.get $2) - ;; OPT_O-NEXT: ) - ;; OPT_O-NEXT: ) - ;; OPT_O-NEXT: (br_on_cast $label0 anyref eqref - ;; OPT_O-NEXT: (tuple.extract 2 1 - ;; OPT_O-NEXT: (local.get $2) + ;; OPT_O-NEXT: (local.get $0) + ;; OPT_O-NEXT: (block $label0 (result eqref) + ;; OPT_O-NEXT: (br $label1 + ;; OPT_O-NEXT: (tuple.make 2 + ;; OPT_O-NEXT: (local.get $0) + ;; OPT_O-NEXT: (br_on_cast $label0 anyref eqref + ;; OPT_O-NEXT: (local.get $1) + ;; OPT_O-NEXT: ) + ;; OPT_O-NEXT: ) ;; OPT_O-NEXT: ) ;; OPT_O-NEXT: ) ;; OPT_O-NEXT: ) ;; OPT_O-NEXT: ) ;; OPT_O-NEXT: ) ;; OPT_O-NEXT: ) - ;; OPT_O-NEXT: (br $label - ;; OPT_O-NEXT: (tuple.make 2 - ;; OPT_O-NEXT: (local.get $0) - ;; OPT_O-NEXT: (local.get $3) - ;; OPT_O-NEXT: ) - ;; OPT_O-NEXT: ) ;; OPT_O-NEXT: ) ;; OPT_O-NEXT: ) ;; OPT_O-NEXT: ) ;; OPT_O-NEXT: ) + ;; OPT_O-NEXT: (local.set $1 + ;; OPT_O-NEXT: (tuple.extract 2 1 + ;; OPT_O-NEXT: (local.get $2) + ;; OPT_O-NEXT: ) + ;; OPT_O-NEXT: ) ;; OPT_O-NEXT: (br $label2) ;; OPT_O-NEXT: ) ;; OPT_O-NEXT: ) diff --git a/test/lit/control-flow-input.wast b/test/lit/control-flow-input.wast index 39cb28d9fa1..babf5293d47 100644 --- a/test/lit/control-flow-input.wast +++ b/test/lit/control-flow-input.wast @@ -1,4 +1,4 @@ -;; NOTE: Assertions have been generated by update_lit_checks.py and should not be edited. +;; NOTE: Assertions have been generated by update_lit_checks.py --all-items and should not be edited. ;; Check that control flow input is correctly parsed using scratch locals. The ;; binary input file is generated from this file using WABT's wat2wasm @@ -11,10 +11,22 @@ (module (type $id (func (param i32) (result i32))) + ;; CHECK: (type $0 (func (result i32 i64))) + + ;; CHECK: (type $1 (func (result i32))) + + ;; CHECK: (type $2 (func)) + + ;; CHECK: (type $3 (func (param i32))) + + ;; CHECK: (type $4 (func (result i64))) + + ;; CHECK: (type $5 (func (result f32))) + ;; CHECK: (tag $e (type $3) (param i32)) (tag $e (param i32)) - ;; CHECK: (func $block (type $0) (result i32) + ;; CHECK: (func $block (type $1) (result i32) ;; CHECK-NEXT: (local $scratch i32) ;; CHECK-NEXT: (local.set $scratch ;; CHECK-NEXT: (i32.const 0) @@ -29,17 +41,21 @@ end ) - ;; CHECK: (func $block-multivalue (type $1) (result i32 i64) - ;; CHECK-NEXT: (local $scratch (tuple i32 i64)) + ;; CHECK: (func $block-multivalue (type $0) (result i32 i64) + ;; CHECK-NEXT: (local $scratch i32) + ;; CHECK-NEXT: (local $scratch_1 i64) ;; CHECK-NEXT: (local.set $scratch + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.set $scratch_1 + ;; CHECK-NEXT: (i64.const 1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (block (type $0) (result i32 i64) ;; CHECK-NEXT: (tuple.make 2 - ;; CHECK-NEXT: (i32.const 0) - ;; CHECK-NEXT: (i64.const 1) + ;; CHECK-NEXT: (local.get $scratch) + ;; CHECK-NEXT: (local.get $scratch_1) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) - ;; CHECK-NEXT: (block (type $1) (result i32 i64) - ;; CHECK-NEXT: (local.get $scratch) - ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) (func $block-multivalue (result i32 i64) i32.const 0 @@ -48,6 +64,102 @@ end ) + ;; CHECK: (func $block-multivalue-tuple-input (type $0) (result i32 i64) + ;; CHECK-NEXT: (local $scratch (tuple i32 i64)) + ;; CHECK-NEXT: (local $scratch_1 i32) + ;; CHECK-NEXT: (local $scratch_2 i64) + ;; CHECK-NEXT: (local.set $scratch_1 + ;; CHECK-NEXT: (tuple.extract 2 0 + ;; CHECK-NEXT: (local.tee $scratch + ;; CHECK-NEXT: (block (type $0) (result i32 i64) + ;; CHECK-NEXT: (tuple.make 2 + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: (i64.const 1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.set $scratch_2 + ;; CHECK-NEXT: (tuple.extract 2 1 + ;; CHECK-NEXT: (local.get $scratch) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (block (type $0) (result i32 i64) + ;; CHECK-NEXT: (tuple.make 2 + ;; CHECK-NEXT: (local.get $scratch_1) + ;; CHECK-NEXT: (local.get $scratch_2) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $block-multivalue-tuple-input (result i32 i64) + block (result i32 i64) + i32.const 0 + i64.const 1 + end + block (param i32 i64) (result i32 i64) + end + ) + + ;; CHECK: (func $block-multivalue-nop-input (type $0) (result i32 i64) + ;; CHECK-NEXT: (local $scratch i64) + ;; CHECK-NEXT: (local $scratch_1 i32) + ;; CHECK-NEXT: (local $scratch_2 i64) + ;; CHECK-NEXT: (local.set $scratch_1 + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.set $scratch_2 + ;; CHECK-NEXT: (block (result i64) + ;; CHECK-NEXT: (nop) + ;; CHECK-NEXT: (local.set $scratch + ;; CHECK-NEXT: (i64.const 1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (nop) + ;; CHECK-NEXT: (local.get $scratch) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (block (type $0) (result i32 i64) + ;; CHECK-NEXT: (tuple.make 2 + ;; CHECK-NEXT: (local.get $scratch_1) + ;; CHECK-NEXT: (local.get $scratch_2) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $block-multivalue-nop-input (result i32 i64) + i32.const 0 + nop + i64.const 1 + nop + block (param i32 i64) (result i32 i64) + end + ) + + ;; CHECK: (func $block-multivalue-unreachable-input (type $0) (result i32 i64) + ;; CHECK-NEXT: (local $scratch i32) + ;; CHECK-NEXT: (local $scratch_1 i64) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (f32.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.set $scratch + ;; CHECK-NEXT: (unreachable) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.set $scratch_1 + ;; CHECK-NEXT: (unreachable) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (block (type $0) (result i32 i64) + ;; CHECK-NEXT: (tuple.make 2 + ;; CHECK-NEXT: (local.get $scratch) + ;; CHECK-NEXT: (local.get $scratch_1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $block-multivalue-unreachable-input (result i32 i64) + f32.const 0 + unreachable + block (param i32 i64) (result i32 i64) + end + ) + ;; CHECK: (func $block-drop (type $2) ;; CHECK-NEXT: (local $scratch i32) ;; CHECK-NEXT: (local.set $scratch @@ -67,29 +179,23 @@ ) ;; CHECK: (func $block-multivalue-drop (type $2) - ;; CHECK-NEXT: (local $scratch (tuple i32 i64)) - ;; CHECK-NEXT: (local $scratch_1 (tuple i32 i64)) + ;; CHECK-NEXT: (local $scratch i32) + ;; CHECK-NEXT: (local $scratch_1 i64) ;; CHECK-NEXT: (local $scratch_2 i32) ;; CHECK-NEXT: (local.set $scratch - ;; CHECK-NEXT: (tuple.make 2 - ;; CHECK-NEXT: (i32.const 0) - ;; CHECK-NEXT: (i64.const 1) - ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.set $scratch_1 + ;; CHECK-NEXT: (i64.const 1) ;; CHECK-NEXT: ) ;; CHECK-NEXT: (block ;; CHECK-NEXT: (drop ;; CHECK-NEXT: (block (result i32) ;; CHECK-NEXT: (local.set $scratch_2 - ;; CHECK-NEXT: (tuple.extract 2 0 - ;; CHECK-NEXT: (local.tee $scratch_1 - ;; CHECK-NEXT: (local.get $scratch) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.get $scratch) ;; CHECK-NEXT: ) ;; CHECK-NEXT: (drop - ;; CHECK-NEXT: (tuple.extract 2 1 - ;; CHECK-NEXT: (local.get $scratch_1) - ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.get $scratch_1) ;; CHECK-NEXT: ) ;; CHECK-NEXT: (local.get $scratch_2) ;; CHECK-NEXT: ) @@ -105,7 +211,7 @@ end ) - ;; CHECK: (func $block-passthrough-nop (type $0) (result i32) + ;; CHECK: (func $block-passthrough-nop (type $1) (result i32) ;; CHECK-NEXT: (local $scratch i32) ;; CHECK-NEXT: (local $scratch_1 i32) ;; CHECK-NEXT: (local.set $scratch @@ -126,7 +232,7 @@ end ) - ;; CHECK: (func $block-passthrough-type (type $0) (result i32) + ;; CHECK: (func $block-passthrough-type (type $1) (result i32) ;; CHECK-NEXT: (local $scratch i32) ;; CHECK-NEXT: (local.set $scratch ;; CHECK-NEXT: (i32.const 0) @@ -141,7 +247,7 @@ end ) - ;; CHECK: (func $loop (type $0) (result i32) + ;; CHECK: (func $loop (type $1) (result i32) ;; CHECK-NEXT: (local $scratch i32) ;; CHECK-NEXT: (local.set $scratch ;; CHECK-NEXT: (i32.const 0) @@ -182,7 +288,7 @@ end ) - ;; CHECK: (func $loop-branch-cond (type $0) (result i32) + ;; CHECK: (func $loop-branch-cond (type $1) (result i32) ;; CHECK-NEXT: (local $scratch i32) ;; CHECK-NEXT: (local.set $scratch ;; CHECK-NEXT: (i32.const 0) @@ -278,40 +384,70 @@ end ) - ;; CHECK: (func $nested-loops-multivalue (type $1) (result i32 i64) - ;; CHECK-NEXT: (local $scratch (tuple i32 i64)) - ;; CHECK-NEXT: (local $scratch_1 (tuple i32 i64)) - ;; CHECK-NEXT: (block $label2 (type $1) (result i32 i64) + ;; CHECK: (func $nested-loops-multivalue (type $0) (result i32 i64) + ;; CHECK-NEXT: (local $scratch i32) + ;; CHECK-NEXT: (local $scratch_1 i64) + ;; CHECK-NEXT: (local $scratch_2 i32) + ;; CHECK-NEXT: (local $scratch_3 i64) + ;; CHECK-NEXT: (local $scratch_4 (tuple i32 i64)) + ;; CHECK-NEXT: (local $scratch_5 (tuple i32 i64)) + ;; CHECK-NEXT: (block $label2 (type $0) (result i32 i64) ;; CHECK-NEXT: (local.set $scratch - ;; CHECK-NEXT: (tuple.make 2 - ;; CHECK-NEXT: (i32.const 0) - ;; CHECK-NEXT: (i64.const 1) - ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.set $scratch_1 + ;; CHECK-NEXT: (i64.const 1) ;; CHECK-NEXT: ) ;; CHECK-NEXT: (loop $label10 ;; CHECK-NEXT: (block $label11 - ;; CHECK-NEXT: (local.set $scratch - ;; CHECK-NEXT: (block $label1 (type $1) (result i32 i64) - ;; CHECK-NEXT: (local.set $scratch_1 + ;; CHECK-NEXT: (local.set $scratch_5 + ;; CHECK-NEXT: (block $label1 (type $0) (result i32 i64) + ;; CHECK-NEXT: (local.set $scratch_2 ;; CHECK-NEXT: (local.get $scratch) ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.set $scratch_3 + ;; CHECK-NEXT: (local.get $scratch_1) + ;; CHECK-NEXT: ) ;; CHECK-NEXT: (loop $label0 ;; CHECK-NEXT: (block $label3 - ;; CHECK-NEXT: (local.set $scratch_1 - ;; CHECK-NEXT: (block $label (type $1) (result i32 i64) + ;; CHECK-NEXT: (local.set $scratch_4 + ;; CHECK-NEXT: (block $label (type $0) (result i32 i64) ;; CHECK-NEXT: (br_table $label $label1 $label2 - ;; CHECK-NEXT: (local.get $scratch_1) + ;; CHECK-NEXT: (tuple.make 2 + ;; CHECK-NEXT: (local.get $scratch_2) + ;; CHECK-NEXT: (local.get $scratch_3) + ;; CHECK-NEXT: ) ;; CHECK-NEXT: (i32.const 2) ;; CHECK-NEXT: ) ;; CHECK-NEXT: (br $label3) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.set $scratch_2 + ;; CHECK-NEXT: (tuple.extract 2 0 + ;; CHECK-NEXT: (local.get $scratch_4) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.set $scratch_3 + ;; CHECK-NEXT: (tuple.extract 2 1 + ;; CHECK-NEXT: (local.get $scratch_4) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) ;; CHECK-NEXT: (br $label0) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) ;; CHECK-NEXT: (br $label11) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.set $scratch + ;; CHECK-NEXT: (tuple.extract 2 0 + ;; CHECK-NEXT: (local.get $scratch_5) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.set $scratch_1 + ;; CHECK-NEXT: (tuple.extract 2 1 + ;; CHECK-NEXT: (local.get $scratch_5) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) ;; CHECK-NEXT: (br $label10) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) @@ -330,7 +466,7 @@ unreachable ) - ;; CHECK: (func $if (type $0) (result i32) + ;; CHECK: (func $if (type $1) (result i32) ;; CHECK-NEXT: (local $scratch i32) ;; CHECK-NEXT: (local.set $scratch ;; CHECK-NEXT: (i32.const 0) @@ -352,7 +488,7 @@ end ) - ;; CHECK: (func $if-new-val (type $0) (result i32) + ;; CHECK: (func $if-new-val (type $1) (result i32) ;; CHECK-NEXT: (local $scratch i32) ;; CHECK-NEXT: (local.set $scratch ;; CHECK-NEXT: (i32.const 0) @@ -379,21 +515,28 @@ end ) - ;; CHECK: (func $if-multivalue (type $1) (result i32 i64) - ;; CHECK-NEXT: (local $scratch (tuple i32 i64)) + ;; CHECK: (func $if-multivalue (type $0) (result i32 i64) + ;; CHECK-NEXT: (local $scratch i32) + ;; CHECK-NEXT: (local $scratch_1 i64) ;; CHECK-NEXT: (local.set $scratch - ;; CHECK-NEXT: (tuple.make 2 - ;; CHECK-NEXT: (i32.const 0) - ;; CHECK-NEXT: (i64.const 1) - ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.set $scratch_1 + ;; CHECK-NEXT: (i64.const 1) ;; CHECK-NEXT: ) - ;; CHECK-NEXT: (if (type $1) (result i32 i64) + ;; CHECK-NEXT: (if (type $0) (result i32 i64) ;; CHECK-NEXT: (i32.const 2) ;; CHECK-NEXT: (then - ;; CHECK-NEXT: (local.get $scratch) + ;; CHECK-NEXT: (tuple.make 2 + ;; CHECK-NEXT: (local.get $scratch) + ;; CHECK-NEXT: (local.get $scratch_1) + ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) ;; CHECK-NEXT: (else - ;; CHECK-NEXT: (local.get $scratch) + ;; CHECK-NEXT: (tuple.make 2 + ;; CHECK-NEXT: (local.get $scratch) + ;; CHECK-NEXT: (local.get $scratch_1) + ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) @@ -405,7 +548,7 @@ end ) - ;; CHECK: (func $if-else (type $0) (result i32) + ;; CHECK: (func $if-else (type $1) (result i32) ;; CHECK-NEXT: (local $scratch i32) ;; CHECK-NEXT: (local.set $scratch ;; CHECK-NEXT: (i32.const 0) @@ -458,16 +601,15 @@ ) ;; CHECK: (func $if-else-multivalue (type $5) (result f32) - ;; CHECK-NEXT: (local $scratch (tuple i32 i64)) - ;; CHECK-NEXT: (local $scratch_1 (tuple i32 i64)) + ;; CHECK-NEXT: (local $scratch i32) + ;; CHECK-NEXT: (local $scratch_1 i64) ;; CHECK-NEXT: (local $scratch_2 i32) - ;; CHECK-NEXT: (local $scratch_3 (tuple i32 i64)) - ;; CHECK-NEXT: (local $scratch_4 i32) + ;; CHECK-NEXT: (local $scratch_3 i32) ;; CHECK-NEXT: (local.set $scratch - ;; CHECK-NEXT: (tuple.make 2 - ;; CHECK-NEXT: (i32.const 0) - ;; CHECK-NEXT: (i64.const 1) - ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.set $scratch_1 + ;; CHECK-NEXT: (i64.const 1) ;; CHECK-NEXT: ) ;; CHECK-NEXT: (if (result f32) ;; CHECK-NEXT: (i32.const 2) @@ -475,16 +617,10 @@ ;; CHECK-NEXT: (drop ;; CHECK-NEXT: (block (result i32) ;; CHECK-NEXT: (local.set $scratch_2 - ;; CHECK-NEXT: (tuple.extract 2 0 - ;; CHECK-NEXT: (local.tee $scratch_1 - ;; CHECK-NEXT: (local.get $scratch) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.get $scratch) ;; CHECK-NEXT: ) ;; CHECK-NEXT: (drop - ;; CHECK-NEXT: (tuple.extract 2 1 - ;; CHECK-NEXT: (local.get $scratch_1) - ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.get $scratch_1) ;; CHECK-NEXT: ) ;; CHECK-NEXT: (local.get $scratch_2) ;; CHECK-NEXT: ) @@ -494,19 +630,13 @@ ;; CHECK-NEXT: (else ;; CHECK-NEXT: (drop ;; CHECK-NEXT: (block (result i32) - ;; CHECK-NEXT: (local.set $scratch_4 - ;; CHECK-NEXT: (tuple.extract 2 0 - ;; CHECK-NEXT: (local.tee $scratch_3 - ;; CHECK-NEXT: (local.get $scratch) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.set $scratch_3 + ;; CHECK-NEXT: (local.get $scratch) ;; CHECK-NEXT: ) ;; CHECK-NEXT: (drop - ;; CHECK-NEXT: (tuple.extract 2 1 - ;; CHECK-NEXT: (local.get $scratch_3) - ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.get $scratch_1) ;; CHECK-NEXT: ) - ;; CHECK-NEXT: (local.get $scratch_4) + ;; CHECK-NEXT: (local.get $scratch_3) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) ;; CHECK-NEXT: (f32.const 4) @@ -528,7 +658,7 @@ end ) - ;; CHECK: (func $try (type $0) (result i32) + ;; CHECK: (func $try (type $1) (result i32) ;; CHECK-NEXT: (local $scratch i32) ;; CHECK-NEXT: (local.set $scratch ;; CHECK-NEXT: (i32.const 0) @@ -545,7 +675,7 @@ end ) - ;; CHECK: (func $try-catch (type $0) (result i32) + ;; CHECK: (func $try-catch (type $1) (result i32) ;; CHECK-NEXT: (local $scratch i32) ;; CHECK-NEXT: (local.set $scratch ;; CHECK-NEXT: (i32.const 0) @@ -566,7 +696,7 @@ end ) - ;; CHECK: (func $try-catch-all (type $0) (result i32) + ;; CHECK: (func $try-catch-all (type $1) (result i32) ;; CHECK-NEXT: (local $scratch i32) ;; CHECK-NEXT: (local.set $scratch ;; CHECK-NEXT: (i32.const 0) @@ -588,7 +718,7 @@ end ) - ;; CHECK: (func $try-catch-delegate (type $0) (result i32) + ;; CHECK: (func $try-catch-delegate (type $1) (result i32) ;; CHECK-NEXT: (local $scratch i32) ;; CHECK-NEXT: (local.set $scratch ;; CHECK-NEXT: (i32.const 0) @@ -606,7 +736,7 @@ delegate 0 ) - ;; CHECK: (func $try-table (type $0) (result i32) + ;; CHECK: (func $try-table (type $1) (result i32) ;; CHECK-NEXT: (local $scratch i32) ;; CHECK-NEXT: (local.set $scratch ;; CHECK-NEXT: (i32.const 0) diff --git a/test/lit/control-flow-input.wast.wasm b/test/lit/control-flow-input.wast.wasm index 664d1be843a4e81a76bd4dc15f445160e49a08c4..9c8408ca0a76dce8aec39dc79dd8cb9f930d3d11 100644 GIT binary patch literal 933 zcmZWo+mf0<5bd4;R0dqv^*Tvas`5UM`!3{1M4Vl%AW#BH?ZdA71NoSILjEk>sL`xc zF@4VI(>)gkNU9wI06wZE9`HZ|EP*13!`qTx4gic;e`6B~_{s{t%1>HIdR$is;R?4S zg^fU1NmKsg#k9D8W5r zjD$n$;%)qALG4m^eYW^o(C6VN3;>%kq0wq!55$;{BoQ; z*;)J}HMpy+NWbHt4QAJ5_4Y@SHCBB7sF=DbGi$ba+0;_sWb>jt=A5rLxhCJ+B;6#d zbG_>yJ?ni@dgB{E%bk;4CRJ5$_C@o&F*HYaa&%wI^-F1cPF579uXr@8eUhgeHMk-s zE%J3R{-_w9DxBTOx&3MA9QxDTR<&IlHdI%~b0@Y>1AqHuPR9F?@a6P|9ild?>_cz@ z?L?#DnO&@7S?^yaJsM1!)ahm#{(~mTvf1!L*EX}yNp0t?yXNB*kDC52y8aRzaNDon mg&z2zAb=41a0dgphao(`2*xmhDP4s*I4;#}*UF(^$o>V?j>!`M delta 397 zcmZ9IK~jS-6o%o26d_P6N@4U2QIDYBBtTjiQ^G*d>8clSp~I#wyKvz#Jd5{mOj{}) zH!uJ9T!%fs>=-Rii MX+V(?OP`tl0%ZAh@&Et; From 7f8938a077d92ede3316b01db851e8b5923b07b0 Mon Sep 17 00:00:00 2001 From: Thomas Lively Date: Sat, 31 Oct 2026 22:49:31 -0700 Subject: [PATCH 2/6] [IRBuilder] Tunnel block inputs into nested scopes When entering a nested control flow scope whose input parameters are scratch local.gets from the enclosing scope's inputs, reuse the enclosing scope's scratch locals directly instead of allocating new scratch locals. Share the movement logic in pushScope with moving legacy EH pops into nested unnamed blocks. --- src/wasm/wasm-ir-builder.cpp | 99 +++--- test/lit/basic/exception-handling-legacy.wast | 49 +-- test/lit/basic/pop-fixup.wast | 125 +++++++- test/lit/control-flow-input.wast | 296 ++++++++++++++++-- test/lit/control-flow-input.wast.wasm | Bin 933 -> 1250 bytes test/lit/parse-bad-pop.wast | 139 ++++++++ 6 files changed, 581 insertions(+), 127 deletions(-) create mode 100644 test/lit/parse-bad-pop.wast diff --git a/src/wasm/wasm-ir-builder.cpp b/src/wasm/wasm-ir-builder.cpp index 3535561db2f..db7c1e6d888 100644 --- a/src/wasm/wasm-ir-builder.cpp +++ b/src/wasm/wasm-ir-builder.cpp @@ -901,24 +901,59 @@ Result<> IRBuilder::pushScope(ScopeCtx&& scope) { lastBinaryPos = *binaryPos; } bool hasInput = scope.inputType != Type::none; - if (hasInput && !scope.isDelimiter()) { - std::vector inputs; - CHECK_ERR(ChildPopper{*this}.popInputs(scope.inputType, inputs)); - scope.inputLocals.reserve(inputs.size()); + std::vector inputs; + if (!scope.isDelimiter()) { + bool isUnnamedBlock = scope.getBlock() && !scope.getBlock()->name; + if (hasInput) { + CHECK_ERR(ChildPopper{*this}.popInputs(scope.inputType, inputs)); + } else if (isUnnamedBlock && getScope().exprStack.size() == 1 && + getScope().exprStack[0]->is()) { + // Allow pops to move into unnamed blocks even without input types so we + // can round-trip WAT without the printer needing to synthesize block + // input types. (Note that the block may receive a name later if we + // encounter a break to it; in that case, the fixups that run later will + // have work to do.) + inputs.push_back(getScope().exprStack.back()); + getScope().exprStack.pop_back(); + } + const auto& parentLocals = getScope().inputLocals; + // The new scope may only receive a suffix of the outer scope's parameters, + // so match inputs starting from the remaining size of the outer stack + // rather than from the beginning. + size_t baseIndex = getScope().exprStack.size(); for (size_t i = 0; i < inputs.size(); ++i) { - auto scratch = addScratchLocal(scope.inputType[i]); - CHECK_ERR(scratch); - scope.inputLocals.push_back(*scratch); - pushSynthetic(builder.makeLocalSet(*scratch, inputs[i])); + // Move an enclosing scope's input expression (a scratch local.get or a + // catch pop) directly into this scope rather than allocating a new + // scratch local. + auto* expr = inputs[i]; + size_t parentIndex = baseIndex + i; + bool isInputGet = expr->is() && + expr->type == scope.inputType[i] && + parentIndex < parentLocals.size(); + bool isPop = isUnnamedBlock && inputs.size() == 1 && expr->is(); + if (isInputGet) { + assert(expr->cast()->index == parentLocals[parentIndex]); + scope.inputLocals.push_back(parentLocals[parentIndex]); + } else if (!isPop) { + auto scratch = addScratchLocal(scope.inputType[i]); + CHECK_ERR(scratch); + scope.inputLocals.push_back(*scratch); + pushSynthetic(builder.makeLocalSet(*scratch, expr)); + inputs[i] = builder.makeLocalGet(*scratch, scope.inputType[i]); + } + } + } else if (hasInput) { + // Delimiters (such as `else`) reuse the scratch locals that were already + // populated when the enclosing scope started, pushing fresh `local.get`s + // onto the new scope's stack. + for (size_t i = 0; i < scope.inputLocals.size(); ++i) { + inputs.push_back( + builder.makeLocalGet(scope.inputLocals[i], scope.inputType[i])); } } scopeStack.emplace_back(std::move(scope)); - if (hasInput) { - auto& newScope = getScope(); - for (size_t i = 0; i < newScope.inputLocals.size(); ++i) { - pushSynthetic( - builder.makeLocalGet(newScope.inputLocals[i], newScope.inputType[i])); - } + for (auto* input : inputs) { + pushSynthetic(input); } return Ok{}; } @@ -2005,38 +2040,22 @@ Result<> IRBuilder::makeUnreachable() { Result<> IRBuilder::makePop(Type type) { // We don't actually want to create a new Pop expression here because we // already create them automatically when starting a legacy catch block that - // needs one. Just verify that the Pop we are being asked to make is the same - // type as the Pop we have already made, and move it into the current scope if - // we have entered unnamed blocks since the catch began. (Note that the block - // may receive a name later, if we encounter a break to it, if we process - // that after the pop. In that case, the fixups that run later will have work - // to do.) - ScopeCtx* catchScope = nullptr; - for (int i = int(scopeStack.size()) - 1; i >= 0; --i) { - auto& scope = scopeStack[i]; - if (scope.getCatch()) { - catchScope = &scope; - break; - } - auto* block = scope.getBlock(); - if (!block || block->name || !scope.exprStack.empty()) { - break; + // needs one (and move them into nested unnamed blocks in pushScope). Just + // verify that the Pop we are being asked to make is the same type as the Pop + // we have already made. + auto& scope = getScope(); + if (scope.exprStack.size() != 1 || !scope.exprStack[0]->is()) { + if (auto* tryy = scope.getCatch(); + tryy && wasm.getTag(tryy->catchTags.back())->params() == Type::none) { + return Err{"cannot pop from empty tag"}; } - } - if (!catchScope || catchScope->exprStack.size() != 1 || - !catchScope->exprStack[0]->is()) { return Err{ "pop instructions may only appear at the beginning of catch blocks"}; } - auto* pop = catchScope->exprStack[0]; - auto expectedType = pop->type; - if (!Type::isSubType(expectedType, type)) { + auto expectedType = scope.exprStack[0]->type; + if (type != expectedType) { return Err{std::string("Expected pop of type ") + expectedType.toString()}; } - if (catchScope != &getScope()) { - catchScope->exprStack.pop_back(); - pushSynthetic(pop); - } return Ok{}; } diff --git a/test/lit/basic/exception-handling-legacy.wast b/test/lit/basic/exception-handling-legacy.wast index 11bdb4ca77c..a47932538cc 100644 --- a/test/lit/basic/exception-handling-legacy.wast +++ b/test/lit/basic/exception-handling-legacy.wast @@ -1181,39 +1181,6 @@ ) ) - ;; CHECK-TEXT: (func $pop-can-be-supertype (type $0) - ;; CHECK-TEXT-NEXT: (try - ;; CHECK-TEXT-NEXT: (do - ;; CHECK-TEXT-NEXT: ) - ;; CHECK-TEXT-NEXT: (catch $e-eqref - ;; CHECK-TEXT-NEXT: (drop - ;; CHECK-TEXT-NEXT: (pop eqref) - ;; CHECK-TEXT-NEXT: ) - ;; CHECK-TEXT-NEXT: ) - ;; CHECK-TEXT-NEXT: ) - ;; CHECK-TEXT-NEXT: ) - ;; CHECK-BIN: (func $pop-can-be-supertype (type $0) - ;; CHECK-BIN-NEXT: (try - ;; CHECK-BIN-NEXT: (do - ;; CHECK-BIN-NEXT: ) - ;; CHECK-BIN-NEXT: (catch $e-eqref - ;; CHECK-BIN-NEXT: (drop - ;; CHECK-BIN-NEXT: (pop eqref) - ;; CHECK-BIN-NEXT: ) - ;; CHECK-BIN-NEXT: ) - ;; CHECK-BIN-NEXT: ) - ;; CHECK-BIN-NEXT: ) - (func $pop-can-be-supertype - (try - (do) - (catch $e-eqref - (drop - (pop anyref) ;; pop can be supertype - ) - ) - ) - ) - ;; CHECK-TEXT: (func $catchless-try-with-inner-delegate (type $0) ;; CHECK-TEXT-NEXT: (try $label$0 ;; CHECK-TEXT-NEXT: (do @@ -1780,18 +1747,6 @@ ;; CHECK-BIN-NODEBUG-NEXT: ) ;; CHECK-BIN-NODEBUG: (func $22 (type $0) -;; CHECK-BIN-NODEBUG-NEXT: (try -;; CHECK-BIN-NODEBUG-NEXT: (do -;; CHECK-BIN-NODEBUG-NEXT: ) -;; CHECK-BIN-NODEBUG-NEXT: (catch $tag$3 -;; CHECK-BIN-NODEBUG-NEXT: (drop -;; CHECK-BIN-NODEBUG-NEXT: (pop eqref) -;; CHECK-BIN-NODEBUG-NEXT: ) -;; CHECK-BIN-NODEBUG-NEXT: ) -;; CHECK-BIN-NODEBUG-NEXT: ) -;; CHECK-BIN-NODEBUG-NEXT: ) - -;; CHECK-BIN-NODEBUG: (func $23 (type $0) ;; CHECK-BIN-NODEBUG-NEXT: (try $label ;; CHECK-BIN-NODEBUG-NEXT: (do ;; CHECK-BIN-NODEBUG-NEXT: (try @@ -1808,7 +1763,7 @@ ;; CHECK-BIN-NODEBUG-NEXT: (unreachable) ;; CHECK-BIN-NODEBUG-NEXT: ) -;; CHECK-BIN-NODEBUG: (func $24 (type $0) +;; CHECK-BIN-NODEBUG: (func $23 (type $0) ;; CHECK-BIN-NODEBUG-NEXT: (block ;; CHECK-BIN-NODEBUG-NEXT: (block ;; CHECK-BIN-NODEBUG-NEXT: ) @@ -1821,7 +1776,7 @@ ;; CHECK-BIN-NODEBUG-NEXT: (nop) ;; CHECK-BIN-NODEBUG-NEXT: ) -;; CHECK-BIN-NODEBUG: (func $25 (type $0) +;; CHECK-BIN-NODEBUG: (func $24 (type $0) ;; CHECK-BIN-NODEBUG-NEXT: (if ;; CHECK-BIN-NODEBUG-NEXT: (try (result i32) ;; CHECK-BIN-NODEBUG-NEXT: (do diff --git a/test/lit/basic/pop-fixup.wast b/test/lit/basic/pop-fixup.wast index 5ba40f46924..2be69ae28d3 100644 --- a/test/lit/basic/pop-fixup.wast +++ b/test/lit/basic/pop-fixup.wast @@ -184,23 +184,17 @@ ;; CHECK: (func $stacky-block-param (type $0) ;; CHECK-NEXT: (local $scratch i32) - ;; CHECK-NEXT: (local $scratch_1 i32) ;; CHECK-NEXT: (try ;; CHECK-NEXT: (do ;; CHECK-NEXT: ) ;; CHECK-NEXT: (catch $e - ;; CHECK-NEXT: (local.set $scratch - ;; CHECK-NEXT: (pop i32) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: (block - ;; CHECK-NEXT: (drop - ;; CHECK-NEXT: (block (result i32) - ;; CHECK-NEXT: (local.set $scratch_1 - ;; CHECK-NEXT: (local.get $scratch) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: (nop) - ;; CHECK-NEXT: (local.get $scratch_1) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (block (result i32) + ;; CHECK-NEXT: (local.set $scratch + ;; CHECK-NEXT: (pop i32) ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (nop) + ;; CHECK-NEXT: (local.get $scratch) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) @@ -209,8 +203,8 @@ (func $stacky-block-param try catch $e - ;; Add a block with an input param, forcing us to use a local to capture the - ;; pop. + ;; An unnamed block with an input param moves the pop directly into the + ;; block rather than using an extra scratch local to capture it. block (param i32) nop drop @@ -290,4 +284,107 @@ end end ) + + ;; CHECK: (func $stacky-block-param-tuple (type $0) + ;; CHECK-NEXT: (local $scratch (tuple i32 i32)) + ;; CHECK-NEXT: (local $scratch_1 i32) + ;; CHECK-NEXT: (local $scratch_2 i32) + ;; CHECK-NEXT: (local $scratch_3 i32) + ;; CHECK-NEXT: (local $scratch_4 i32) + ;; CHECK-NEXT: (try + ;; CHECK-NEXT: (do + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (catch $e2 + ;; CHECK-NEXT: (local.set $scratch_1 + ;; CHECK-NEXT: (tuple.extract 2 0 + ;; CHECK-NEXT: (local.tee $scratch + ;; CHECK-NEXT: (pop (tuple i32 i32)) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.set $scratch_2 + ;; CHECK-NEXT: (tuple.extract 2 1 + ;; CHECK-NEXT: (local.get $scratch) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (block + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (block (result i32) + ;; CHECK-NEXT: (local.set $scratch_4 + ;; CHECK-NEXT: (local.get $scratch_1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (block (result i32) + ;; CHECK-NEXT: (local.set $scratch_3 + ;; CHECK-NEXT: (local.get $scratch_2) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (nop) + ;; CHECK-NEXT: (local.get $scratch_3) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.get $scratch_4) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $stacky-block-param-tuple + try + catch $e2 + ;; An unnamed block with multivalue input params extracts the tuple elements + ;; into scratch locals before the block rather than moving the pop into the + ;; block. + block (param i32 i32) + nop + drop + drop + end + end + ) + + ;; CHECK: (func $stacky-unnamed-block-tuple (type $0) + ;; CHECK-NEXT: (local $scratch (tuple i32 i32)) + ;; CHECK-NEXT: (local $scratch_1 i32) + ;; CHECK-NEXT: (try + ;; CHECK-NEXT: (do + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (catch $e2 + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (block (result i32) + ;; CHECK-NEXT: (local.set $scratch_1 + ;; CHECK-NEXT: (block (result i32) + ;; CHECK-NEXT: (local.set $scratch + ;; CHECK-NEXT: (pop (tuple i32 i32)) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (nop) + ;; CHECK-NEXT: (tuple.extract 2 0 + ;; CHECK-NEXT: (local.get $scratch) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (tuple.extract 2 1 + ;; CHECK-NEXT: (local.get $scratch) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.get $scratch_1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $stacky-unnamed-block-tuple + try + catch $e2 + ;; An unnamed block without explicit input params moves a multivalue pop + ;; directly into the block. + block + pop (tuple i32 i32) + nop + drop + drop + end + end + ) ) diff --git a/test/lit/control-flow-input.wast b/test/lit/control-flow-input.wast index babf5293d47..6992e7021eb 100644 --- a/test/lit/control-flow-input.wast +++ b/test/lit/control-flow-input.wast @@ -9,8 +9,6 @@ ;; RUN: wasm-opt -all %s.wasm -S -o - | filecheck %s (module - (type $id (func (param i32) (result i32))) - ;; CHECK: (type $0 (func (result i32 i64))) ;; CHECK: (type $1 (func (result i32))) @@ -19,9 +17,16 @@ ;; CHECK: (type $3 (func (param i32))) - ;; CHECK: (type $4 (func (result i64))) + ;; CHECK: (type $4 (func (param i64) (result i32 i64))) + + ;; CHECK: (type $5 (func (result funcref))) + + ;; CHECK: (type $id (func (param i32) (result i32))) + (type $id (func (param i32) (result i32))) + + ;; CHECK: (type $7 (func (result i64))) - ;; CHECK: (type $5 (func (result f32))) + ;; CHECK: (type $8 (func (result f32))) ;; CHECK: (tag $e (type $3) (param i32)) (tag $e (param i32)) @@ -247,6 +252,140 @@ end ) + ;; CHECK: (func $block-tunnel (type $1) (result i32) + ;; CHECK-NEXT: (local $scratch i32) + ;; CHECK-NEXT: (local.set $scratch + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (block (result i32) + ;; CHECK-NEXT: (block (result i32) + ;; CHECK-NEXT: (local.get $scratch) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $block-tunnel (result i32) + ;; A parameter tunneled through nested blocks should reuse a single scratch + ;; local. + i32.const 0 + block (param i32) (result i32) + block (param i32) (result i32) + end + end + ) + + ;; CHECK: (func $block-tunnel-multivalue (type $0) (result i32 i64) + ;; CHECK-NEXT: (local $scratch i32) + ;; CHECK-NEXT: (local $scratch_1 i64) + ;; CHECK-NEXT: (local.set $scratch + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.set $scratch_1 + ;; CHECK-NEXT: (i64.const 1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (block (type $0) (result i32 i64) + ;; CHECK-NEXT: (block (type $0) (result i32 i64) + ;; CHECK-NEXT: (tuple.make 2 + ;; CHECK-NEXT: (local.get $scratch) + ;; CHECK-NEXT: (local.get $scratch_1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $block-tunnel-multivalue (result i32 i64) + ;; Multiple parameters tunneled through nested blocks should reuse the same + ;; scratch locals. + i32.const 0 + i64.const 1 + block (param i32 i64) (result i32 i64) + block (param i32 i64) (result i32 i64) + end + end + ) + + ;; CHECK: (func $block-tunnel-suffix (type $0) (result i32 i64) + ;; CHECK-NEXT: (local $scratch i32) + ;; CHECK-NEXT: (local $scratch_1 i64) + ;; CHECK-NEXT: (local.set $scratch + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.set $scratch_1 + ;; CHECK-NEXT: (i64.const 1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (block (type $0) (result i32 i64) + ;; CHECK-NEXT: (tuple.make 2 + ;; CHECK-NEXT: (local.get $scratch) + ;; CHECK-NEXT: (block (result i64) + ;; CHECK-NEXT: (local.get $scratch_1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $block-tunnel-suffix (result i32 i64) + ;; An inner block that only consumes a suffix of the enclosing block's + ;; parameters should still reuse the corresponding scratch local. + i32.const 0 + i64.const 1 + block (param i32 i64) (result i32 i64) + block (param i64) (result i64) + end + end + ) + + ;; CHECK: (func $block-tunnel-partial (type $4) (param $x i64) (result i32 i64) + ;; CHECK-NEXT: (local $scratch i32) + ;; CHECK-NEXT: (local $scratch_2 i64) + ;; CHECK-NEXT: (local.set $scratch + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (block (type $0) (result i32 i64) + ;; CHECK-NEXT: (local.set $scratch_2 + ;; CHECK-NEXT: (local.get $x) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (block (type $0) (result i32 i64) + ;; CHECK-NEXT: (tuple.make 2 + ;; CHECK-NEXT: (local.get $scratch) + ;; CHECK-NEXT: (local.get $scratch_2) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $block-tunnel-partial (param $x i64) (result i32 i64) + ;; A parameter tunneled alongside a newly pushed parameter should still + ;; reuse the enclosing block's scratch local. + i32.const 0 + block (param i32) (result i32 i64) + local.get $x + block (param i32 i64) (result i32 i64) + end + end + ) + + ;; CHECK: (func $block-tunnel-subtype (type $5) (result funcref) + ;; CHECK-NEXT: (local $scratch (ref null $id)) + ;; CHECK-NEXT: (local $scratch_1 funcref) + ;; CHECK-NEXT: (local.set $scratch + ;; CHECK-NEXT: (ref.null nofunc) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (block (result funcref) + ;; CHECK-NEXT: (local.set $scratch_1 + ;; CHECK-NEXT: (local.get $scratch) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (block (result funcref) + ;; CHECK-NEXT: (local.get $scratch_1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $block-tunnel-subtype (result funcref) + ;; A parameter whose type is widened in a nested block cannot reuse the + ;; narrower scratch local because keeping the narrower type on the stack + ;; would allow us to parse invalid WAT that requires the narrower type. + ref.null $id + block (param (ref null $id)) (result funcref) + block (param funcref) (result funcref) + end + end + ) + ;; CHECK: (func $loop (type $1) (result i32) ;; CHECK-NEXT: (local $scratch i32) ;; CHECK-NEXT: (local.set $scratch @@ -348,7 +487,7 @@ end ) - ;; CHECK: (func $loop-branch-cond-new-val (type $4) (result i64) + ;; CHECK: (func $loop-branch-cond-new-val (type $7) (result i64) ;; CHECK-NEXT: (local $scratch i32) ;; CHECK-NEXT: (local.set $scratch ;; CHECK-NEXT: (i32.const 0) @@ -387,10 +526,8 @@ ;; CHECK: (func $nested-loops-multivalue (type $0) (result i32 i64) ;; CHECK-NEXT: (local $scratch i32) ;; CHECK-NEXT: (local $scratch_1 i64) - ;; CHECK-NEXT: (local $scratch_2 i32) - ;; CHECK-NEXT: (local $scratch_3 i64) - ;; CHECK-NEXT: (local $scratch_4 (tuple i32 i64)) - ;; CHECK-NEXT: (local $scratch_5 (tuple i32 i64)) + ;; CHECK-NEXT: (local $scratch_2 (tuple i32 i64)) + ;; CHECK-NEXT: (local $scratch_3 (tuple i32 i64)) ;; CHECK-NEXT: (block $label2 (type $0) (result i32 i64) ;; CHECK-NEXT: (local.set $scratch ;; CHECK-NEXT: (i32.const 0) @@ -400,36 +537,30 @@ ;; CHECK-NEXT: ) ;; CHECK-NEXT: (loop $label10 ;; CHECK-NEXT: (block $label11 - ;; CHECK-NEXT: (local.set $scratch_5 + ;; CHECK-NEXT: (local.set $scratch_3 ;; CHECK-NEXT: (block $label1 (type $0) (result i32 i64) - ;; CHECK-NEXT: (local.set $scratch_2 - ;; CHECK-NEXT: (local.get $scratch) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: (local.set $scratch_3 - ;; CHECK-NEXT: (local.get $scratch_1) - ;; CHECK-NEXT: ) ;; CHECK-NEXT: (loop $label0 ;; CHECK-NEXT: (block $label3 - ;; CHECK-NEXT: (local.set $scratch_4 + ;; CHECK-NEXT: (local.set $scratch_2 ;; CHECK-NEXT: (block $label (type $0) (result i32 i64) ;; CHECK-NEXT: (br_table $label $label1 $label2 ;; CHECK-NEXT: (tuple.make 2 - ;; CHECK-NEXT: (local.get $scratch_2) - ;; CHECK-NEXT: (local.get $scratch_3) + ;; CHECK-NEXT: (local.get $scratch) + ;; CHECK-NEXT: (local.get $scratch_1) ;; CHECK-NEXT: ) ;; CHECK-NEXT: (i32.const 2) ;; CHECK-NEXT: ) ;; CHECK-NEXT: (br $label3) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) - ;; CHECK-NEXT: (local.set $scratch_2 + ;; CHECK-NEXT: (local.set $scratch ;; CHECK-NEXT: (tuple.extract 2 0 - ;; CHECK-NEXT: (local.get $scratch_4) + ;; CHECK-NEXT: (local.get $scratch_2) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) - ;; CHECK-NEXT: (local.set $scratch_3 + ;; CHECK-NEXT: (local.set $scratch_1 ;; CHECK-NEXT: (tuple.extract 2 1 - ;; CHECK-NEXT: (local.get $scratch_4) + ;; CHECK-NEXT: (local.get $scratch_2) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) ;; CHECK-NEXT: (br $label0) @@ -440,12 +571,12 @@ ;; CHECK-NEXT: ) ;; CHECK-NEXT: (local.set $scratch ;; CHECK-NEXT: (tuple.extract 2 0 - ;; CHECK-NEXT: (local.get $scratch_5) + ;; CHECK-NEXT: (local.get $scratch_3) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) ;; CHECK-NEXT: (local.set $scratch_1 ;; CHECK-NEXT: (tuple.extract 2 1 - ;; CHECK-NEXT: (local.get $scratch_5) + ;; CHECK-NEXT: (local.get $scratch_3) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) ;; CHECK-NEXT: (br $label10) @@ -466,6 +597,41 @@ unreachable ) + ;; CHECK: (func $block-loop-tunnel (type $1) (result i32) + ;; CHECK-NEXT: (local $scratch i32) + ;; CHECK-NEXT: (local.set $scratch + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (block (result i32) + ;; CHECK-NEXT: (loop $label0 (result i32) + ;; CHECK-NEXT: (block $label1 (result i32) + ;; CHECK-NEXT: (local.set $scratch + ;; CHECK-NEXT: (block $label (result i32) + ;; CHECK-NEXT: (br $label1 + ;; CHECK-NEXT: (br_if $label + ;; CHECK-NEXT: (local.get $scratch) + ;; CHECK-NEXT: (i32.const 1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (br $label0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $block-loop-tunnel (result i32) + ;; A parameter tunneled from a block into a loop should reuse the same + ;; scratch local. + i32.const 0 + block (param i32) (result i32) + loop (param i32) (result i32) + i32.const 1 + br_if 0 + end + end + ) + ;; CHECK: (func $if (type $1) (result i32) ;; CHECK-NEXT: (local $scratch i32) ;; CHECK-NEXT: (local.set $scratch @@ -600,7 +766,7 @@ end ) - ;; CHECK: (func $if-else-multivalue (type $5) (result f32) + ;; CHECK: (func $if-else-multivalue (type $8) (result f32) ;; CHECK-NEXT: (local $scratch i32) ;; CHECK-NEXT: (local $scratch_1 i64) ;; CHECK-NEXT: (local $scratch_2 i32) @@ -658,6 +824,84 @@ end ) + ;; CHECK: (func $if-tunnel (type $1) (result i32) + ;; CHECK-NEXT: (local $scratch i32) + ;; CHECK-NEXT: (local.set $scratch + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (if (result i32) + ;; CHECK-NEXT: (i32.const 1) + ;; CHECK-NEXT: (then + ;; CHECK-NEXT: (if (result i32) + ;; CHECK-NEXT: (i32.const 2) + ;; CHECK-NEXT: (then + ;; CHECK-NEXT: (local.get $scratch) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (else + ;; CHECK-NEXT: (local.get $scratch) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (else + ;; CHECK-NEXT: (local.get $scratch) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $if-tunnel (result i32) + ;; A parameter tunneled through nested ifs and blocks should reuse the same + ;; scratch local in both arms. + i32.const 0 + i32.const 1 + if (param i32) (result i32) + i32.const 2 + if (param i32) (result i32) + end + else + block (param i32) (result i32) + end + end + ) + + ;; CHECK: (func $if-tunnel-nop (type $1) (result i32) + ;; CHECK-NEXT: (local $scratch i32) + ;; CHECK-NEXT: (local $scratch_1 i32) + ;; CHECK-NEXT: (local $scratch_2 i32) + ;; CHECK-NEXT: (local.set $scratch + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (block (result i32) + ;; CHECK-NEXT: (local.set $scratch_2 + ;; CHECK-NEXT: (block (result i32) + ;; CHECK-NEXT: (local.set $scratch_1 + ;; CHECK-NEXT: (local.get $scratch) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (nop) + ;; CHECK-NEXT: (local.get $scratch_1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (if (result i32) + ;; CHECK-NEXT: (i32.const 1) + ;; CHECK-NEXT: (then + ;; CHECK-NEXT: (local.get $scratch_2) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (else + ;; CHECK-NEXT: (local.get $scratch_2) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $if-tunnel-nop (result i32) + ;; A none-typed instruction before an if wraps the outer input in a block + ;; when popping the if input, requiring a new scratch local for the if. + i32.const 0 + block (param i32) (result i32) + nop + i32.const 1 + if (param i32) (result i32) + end + end + ) + ;; CHECK: (func $try (type $1) (result i32) ;; CHECK-NEXT: (local $scratch i32) ;; CHECK-NEXT: (local.set $scratch diff --git a/test/lit/control-flow-input.wast.wasm b/test/lit/control-flow-input.wast.wasm index 9c8408ca0a76dce8aec39dc79dd8cb9f930d3d11..1a67e6e0062472bfcdd9977bdd6bd711a564ef17 100644 GIT binary patch literal 1250 zcmZWpOLE#k5PdWH02+h=gRx~hw#iy|u*fDC$Q}WMp&|1}a zIJ_YP@xm42#riyy7X2JYN&?oqeNSJ`a7rHgTe4WcgP$f{E8xS@mgiu^Zt)O}+$IR^$MOUbF%uFuzWN8;Q zLcAgu7ZGiZ*XQCK2Hb#x_~Y1V{HVo0dkANetft>Z+Z^FznH0+(aZ+k|^^pmSGD~#0 zOtZ4Ef}?1<%Bq+>Jc^a+TF28xJlWMdUX?RjuQIoPl+Sc+OJ{MO7mM|(d{~6kqkdLp z@6*L|rW?(QFUmC4i8t8y_Qq1D2Z6kt&zDcl-T|4#>tY!v{vn%}6YkTplhrCaQEbD> zI!>pHc6-kWr>k_<>3?KG9;thNM5p>^Nc%n=q&hG3EaZfI@4mfGbv091&DNL8`I)zz zhuhkLPx$@8o!7|KNv3#>I4TiG_{TCdLF~Ny0lA8xxkhMJ`$H2pk#^; zkT#_vgI^P?aJaLtgE*BhXGt@i9m04chEq~LxTA}&iZqIWfBb7(p{qV<1WlXia##kGD ht#V!Sii?5lIvCm9|j03?W4w!qK{sO)GY4ZR8 diff --git a/test/lit/parse-bad-pop.wast b/test/lit/parse-bad-pop.wast new file mode 100644 index 00000000000..4f0a201fe6e --- /dev/null +++ b/test/lit/parse-bad-pop.wast @@ -0,0 +1,139 @@ +;; Test that misplaced pop instructions result in the expected parse errors + +;; RUN: foreach %s %t not wasm-opt -all 2>&1 | filecheck %s +;; RUN: foreach %s %t not wasm-shell 2>&1 | filecheck %s + +;; CHECK: 3:5: error: pop instructions may only appear at the beginning of catch blocks +(module + (func $empty-stack + (pop i32) + ) +) + +;; CHECK: 4:5: error: pop instructions may only appear at the beginning of catch blocks +(module + (func $non-pop-on-stack + (i32.const 0) + (pop i32) + ) +) + +;; CHECK: 5:9: error: pop instructions may only appear at the beginning of catch blocks +(module + (func $in-try-body + (try + (do + (pop i32) + ) + (catch_all) + ) + ) +) + +;; CHECK: 6:9: error: pop instructions may only appear at the beginning of catch blocks +(module + (func $in-catch-all + (try + (do) + (catch_all + (pop i32) + ) + ) + ) +) + +;; CHECK: 7:9: error: cannot pop from empty tag +(module + (tag $e) + (func $empty-tag-catch + (try + (do) + (catch $e + (pop i32) + ) + ) + ) +) + +;; CHECK: 8:9: error: pop instructions may only appear at the beginning of catch blocks +(module + (tag $e (param i32)) + (func $after-inst-in-catch + (try + (do) + (catch $e + (nop) + (pop i32) + ) + ) + ) +) + +;; CHECK: 8:11: error: pop instructions may only appear at the beginning of catch blocks +(module + (tag $e (param i32)) + (func $in-named-block + (try + (do) + (catch $e + (block $b + (pop i32) + ) + ) + ) + ) +) + +;; CHECK: 8:11: error: pop instructions may only appear at the beginning of catch blocks +(module + (tag $e (param i32)) + (func $in-loop + (try + (do) + (catch $e + (loop + (pop i32) + ) + ) + ) + ) +) + +;; CHECK: 7:9: error: Expected pop of type i32 +(module + (tag $e (param i32)) + (func $wrong-type + (try + (do) + (catch $e + (pop i64) + ) + ) + ) +) + +;; CHECK: 7:9: error: Expected pop of type anyref +(module + (tag $e (param anyref)) + (func $subtype + (try + (do) + (catch $e + (pop eqref) + ) + ) + ) +) + +;; CHECK: 7:9: error: Expected pop of type eqref +(module + (tag $e (param eqref)) + (func $supertype + (try + (do) + (catch $e + (pop anyref) + ) + ) + ) +) \ No newline at end of file From 26fcd22e29723211caf9af7ede1a1f687a0b6b4a Mon Sep 17 00:00:00 2001 From: Thomas Lively Date: Mon, 2 Nov 2026 15:31:45 -0800 Subject: [PATCH 3/6] simplify popInputs, add assertion --- src/support/result.h | 4 ++++ src/wasm/wasm-ir-builder.cpp | 24 +++++++++++++----------- 2 files changed, 17 insertions(+), 11 deletions(-) diff --git a/src/support/result.h b/src/support/result.h index f34f6300759..615e88ea3df 100644 --- a/src/support/result.h +++ b/src/support/result.h @@ -17,6 +17,7 @@ #ifndef wasm_support_result_h #define wasm_support_result_h +#include #include #include @@ -39,6 +40,9 @@ struct Err { return (typename decltype(_val)::ErrorType)(*err); \ } +// Assert that the result is not an error. +#define ASSERT_OK(val) assert(!(val).getErr()); + // Represent a result of type T or an error message. template struct [[nodiscard]] Result { using ErrorType = E; diff --git a/src/wasm/wasm-ir-builder.cpp b/src/wasm/wasm-ir-builder.cpp index 3535561db2f..2ca99b9fca9 100644 --- a/src/wasm/wasm-ir-builder.cpp +++ b/src/wasm/wasm-ir-builder.cpp @@ -779,14 +779,15 @@ struct IRBuilder::ChildPopper return popConstrainedChildren(children); } - Result<> popInputs(Type inputType, std::vector& inputs) { - inputs.resize(inputType.size()); + Result> popInputs(Type inputType) { + std::vector inputs(inputType.size()); std::vector children; children.reserve(inputType.size()); for (size_t i = 0; i < inputType.size(); ++i) { children.push_back({&inputs[i], {inputType[i]}}); } - return popConstrainedChildren(children); + CHECK_ERR(popConstrainedChildren(children)); + return inputs; } }; @@ -902,14 +903,14 @@ Result<> IRBuilder::pushScope(ScopeCtx&& scope) { } bool hasInput = scope.inputType != Type::none; if (hasInput && !scope.isDelimiter()) { - std::vector inputs; - CHECK_ERR(ChildPopper{*this}.popInputs(scope.inputType, inputs)); - scope.inputLocals.reserve(inputs.size()); - for (size_t i = 0; i < inputs.size(); ++i) { + auto inputs = ChildPopper{*this}.popInputs(scope.inputType); + CHECK_ERR(inputs); + scope.inputLocals.reserve(inputs->size()); + for (size_t i = 0; i < inputs->size(); ++i) { auto scratch = addScratchLocal(scope.inputType[i]); CHECK_ERR(scratch); scope.inputLocals.push_back(*scratch); - pushSynthetic(builder.makeLocalSet(*scratch, inputs[i])); + pushSynthetic(builder.makeLocalSet(*scratch, (*inputs)[i])); } } scopeStack.emplace_back(std::move(scope)); @@ -1463,12 +1464,13 @@ void IRBuilder::fixLoopWithInput(Loop* loop, if (inputType.isSingle()) { list.push_back(builder.makeLocalSet(scratch[0], inner)); } else { - auto tupleScratch = *addScratchLocal(inputType); - list.push_back(builder.makeLocalSet(tupleScratch, inner)); + auto tupleScratch = addScratchLocal(inputType); + ASSERT_OK(tupleScratch); + list.push_back(builder.makeLocalSet(*tupleScratch, inner)); for (Index i = 0; i < scratch.size(); ++i) { list.push_back(builder.makeLocalSet( scratch[i], - builder.makeTupleExtract(builder.makeLocalGet(tupleScratch, inputType), + builder.makeTupleExtract(builder.makeLocalGet(*tupleScratch, inputType), i))); } } From d79a69e77a9968a7bfb09233257038176663bb76 Mon Sep 17 00:00:00 2001 From: Thomas Lively Date: Mon, 2 Nov 2026 16:33:14 -0800 Subject: [PATCH 4/6] Fix ASSERT_OK redefinition and NDEBUG evaluation --- src/passes/Outlining.cpp | 6 ------ src/support/result.h | 8 ++++++-- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/src/passes/Outlining.cpp b/src/passes/Outlining.cpp index 840a84bad37..6a48a42b0d9 100644 --- a/src/passes/Outlining.cpp +++ b/src/passes/Outlining.cpp @@ -34,12 +34,6 @@ #define ODBG(statement) #endif -// Check a Result or MaybeResult for error and call Fatal() if the error exists. -#define ASSERT_OK(val) \ - if (auto _val = (val); auto err = _val.getErr()) { \ - Fatal() << err->msg; \ - } - namespace wasm { // This custom hasher conforms to std::hash. Its purpose is to provide diff --git a/src/support/result.h b/src/support/result.h index 615e88ea3df..490facfd28c 100644 --- a/src/support/result.h +++ b/src/support/result.h @@ -40,8 +40,12 @@ struct Err { return (typename decltype(_val)::ErrorType)(*err); \ } -// Assert that the result is not an error. -#define ASSERT_OK(val) assert(!(val).getErr()); +// Assert that a Result or MaybeResult is not an error. +#define ASSERT_OK(val) \ + do { \ + [[maybe_unused]] auto _val = (val); \ + assert(!_val.getErr()); \ + } while (false) // Represent a result of type T or an error message. template struct [[nodiscard]] Result { From 2bbd16a298939d5144514fab1ec808ddb8530fa5 Mon Sep 17 00:00:00 2001 From: Thomas Lively Date: Wed, 4 Nov 2026 17:48:30 -0800 Subject: [PATCH 5/6] fix bugs found by bots --- src/wasm/wasm-ir-builder.cpp | 16 +++++--- test/lit/basic/pop-fixup.wast | 32 +++++++++++++++ test/lit/control-flow-input.wast | 57 ++++++++++++++++++++++---- test/lit/control-flow-input.wast.wasm | Bin 1250 -> 1305 bytes 4 files changed, 92 insertions(+), 13 deletions(-) diff --git a/src/wasm/wasm-ir-builder.cpp b/src/wasm/wasm-ir-builder.cpp index 9799b8861f6..4a515d9a5a6 100644 --- a/src/wasm/wasm-ir-builder.cpp +++ b/src/wasm/wasm-ir-builder.cpp @@ -930,14 +930,20 @@ Result<> IRBuilder::pushScope(ScopeCtx&& scope) { // scratch local. auto* expr = inputs[i]; size_t parentIndex = baseIndex + i; - bool isInputGet = expr->is() && - expr->type == scope.inputType[i] && - parentIndex < parentLocals.size(); bool isPop = isUnnamedBlock && inputs.size() == 1 && expr->is(); + if (isPop) { + // If this unnamed block later receives a name from a branch targeting + // it, the nested pop will need to be fixed up. + scopeStack[0].noteSyntheticBlock(); + continue; + } + bool isInputGet = + parentIndex < parentLocals.size() && expr->is() && + expr->cast()->index == parentLocals[parentIndex] && + expr->type == scope.inputType[i]; if (isInputGet) { - assert(expr->cast()->index == parentLocals[parentIndex]); scope.inputLocals.push_back(parentLocals[parentIndex]); - } else if (!isPop) { + } else { auto scratch = addScratchLocal(scope.inputType[i]); CHECK_ERR(scratch); scope.inputLocals.push_back(*scratch); diff --git a/test/lit/basic/pop-fixup.wast b/test/lit/basic/pop-fixup.wast index 2be69ae28d3..569264dca9e 100644 --- a/test/lit/basic/pop-fixup.wast +++ b/test/lit/basic/pop-fixup.wast @@ -285,6 +285,38 @@ end ) + ;; CHECK: (func $named-block (type $0) + ;; CHECK-NEXT: (local $0 i32) + ;; CHECK-NEXT: (try + ;; CHECK-NEXT: (do + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (catch $e + ;; CHECK-NEXT: (local.set $0 + ;; CHECK-NEXT: (pop i32) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (block $block + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (local.get $0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (br $block) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $named-block + try + catch $e + ;; As in $stacky-named-block, but without the nop so no synthetic block is + ;; created for stacky code. The block still receives a name from the br, so + ;; the pop still needs to be fixed up. + block + pop i32 + drop + br 0 + end + end + ) + ;; CHECK: (func $stacky-block-param-tuple (type $0) ;; CHECK-NEXT: (local $scratch (tuple i32 i32)) ;; CHECK-NEXT: (local $scratch_1 i32) diff --git a/test/lit/control-flow-input.wast b/test/lit/control-flow-input.wast index 6992e7021eb..d069203522e 100644 --- a/test/lit/control-flow-input.wast +++ b/test/lit/control-flow-input.wast @@ -15,20 +15,20 @@ ;; CHECK: (type $2 (func)) - ;; CHECK: (type $3 (func (param i32))) + ;; CHECK: (type $id (func (param i32) (result i32))) + (type $id (func (param i32) (result i32))) - ;; CHECK: (type $4 (func (param i64) (result i32 i64))) + ;; CHECK: (type $4 (func (param i32))) - ;; CHECK: (type $5 (func (result funcref))) + ;; CHECK: (type $5 (func (param i64) (result i32 i64))) - ;; CHECK: (type $id (func (param i32) (result i32))) - (type $id (func (param i32) (result i32))) + ;; CHECK: (type $6 (func (result funcref))) ;; CHECK: (type $7 (func (result i64))) ;; CHECK: (type $8 (func (result f32))) - ;; CHECK: (tag $e (type $3) (param i32)) + ;; CHECK: (tag $e (type $4) (param i32)) (tag $e (param i32)) ;; CHECK: (func $block (type $1) (result i32) @@ -331,7 +331,7 @@ end ) - ;; CHECK: (func $block-tunnel-partial (type $4) (param $x i64) (result i32 i64) + ;; CHECK: (func $block-tunnel-partial (type $5) (param $x i64) (result i32 i64) ;; CHECK-NEXT: (local $scratch i32) ;; CHECK-NEXT: (local $scratch_2 i64) ;; CHECK-NEXT: (local.set $scratch @@ -360,7 +360,48 @@ end ) - ;; CHECK: (func $block-tunnel-subtype (type $5) (result funcref) + ;; CHECK: (func $block-tunnel-other-local (type $id) (param $x i32) (result i32) + ;; CHECK-NEXT: (local $scratch i32) + ;; CHECK-NEXT: (local $scratch_2 i32) + ;; CHECK-NEXT: (local $scratch_3 i32) + ;; CHECK-NEXT: (local.set $scratch + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.set $scratch_2 + ;; CHECK-NEXT: (i32.const 1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (block (result i32) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (i32.add + ;; CHECK-NEXT: (local.get $scratch) + ;; CHECK-NEXT: (local.get $scratch_2) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.set $scratch_3 + ;; CHECK-NEXT: (local.get $x) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (block (result i32) + ;; CHECK-NEXT: (local.get $scratch_3) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $block-tunnel-other-local (param $x i32) (result i32) + ;; A local.get of a different local at a stack position corresponding to an + ;; enclosing block's input should not be mistaken for that input's scratch + ;; local. + i32.const 0 + i32.const 1 + block (param i32 i32) (result i32) + i32.add + drop + local.get $x + block (param i32) (result i32) + ;; $x should not be treated like a scratch input local here. + end + end + ) + + ;; CHECK: (func $block-tunnel-subtype (type $6) (result funcref) ;; CHECK-NEXT: (local $scratch (ref null $id)) ;; CHECK-NEXT: (local $scratch_1 funcref) ;; CHECK-NEXT: (local.set $scratch diff --git a/test/lit/control-flow-input.wast.wasm b/test/lit/control-flow-input.wast.wasm index 1a67e6e0062472bfcdd9977bdd6bd711a564ef17..759926bcb7cf30db1d4897caa7d6a7704df222e8 100644 GIT binary patch delta 492 zcmZXRJx;?g7=~>p5hU(U8d_*eDW&PaVwOrs3=BO0I}($VxU}FVQR)DlasohACJuoZ z*xA__H~}Z%2slkq`S}L>eII{6w)~+#7#Ps$1OfoynKj3K4J8Ro=EwFy&qj(2O+!eE zb%2qEw33oJq?vNdqNqRsjVHyP-`+P&FaQIHjEiOmAb>G8z#BjXR7`uY56kH(3}GdG zg${n#RG&^5{8a7dL)3yv-Qj_INkr`XjFUi&SVSbDyj1;lGL0QE3t3s^K@gfaTZt1< z-yK!VABMPr?^(6Kgm_U9Zq)x_e~n#}E6S_si&9T&m3-tPEx*ww8V|RO@sRj=V!bH! zB<5l)t76tzla}!*YZjJbF=^ZScB|BuE?M4mCu_2Nu^)tOMMSe*T^vO-;!@#`Iz From 6517b0757af0399c0de9906cc522daa9262e302c Mon Sep 17 00:00:00 2001 From: Thomas Lively Date: Wed, 4 Nov 2026 17:50:13 -0800 Subject: [PATCH 6/6] whitespace --- test/lit/parse-bad-pop.wast | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/lit/parse-bad-pop.wast b/test/lit/parse-bad-pop.wast index 4f0a201fe6e..13a22349320 100644 --- a/test/lit/parse-bad-pop.wast +++ b/test/lit/parse-bad-pop.wast @@ -136,4 +136,4 @@ ) ) ) -) \ No newline at end of file +)