summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorThomas Lively <tlively@google.com>2023-01-05 09:48:51 -0600
committerGitHub <noreply@github.com>2023-01-05 07:48:51 -0800
commit5bf548b32e30fbae16dde5df703541ca7ac72f15 (patch)
tree5b56b8402d1a6fe37103bc6c12afb417f885721c
parentf92350d2949934c0e0ce4a27ec8b799ac2a85e45 (diff)
downloadbinaryen-5bf548b32e30fbae16dde5df703541ca7ac72f15.tar.gz
binaryen-5bf548b32e30fbae16dde5df703541ca7ac72f15.tar.bz2
binaryen-5bf548b32e30fbae16dde5df703541ca7ac72f15.zip
[Parser] Parse blocks (#5393)
Parse both the folded and unfolded forms of blocks and structure the code to make supporting additional block instructions like if-else and try-catch relatively simple. Parsing block types is extra fun because they may implicitly define new signature heap types via a typeuse, but only if their types are not given by a single result type. To figuring out whether a new type may be introduced in all the relevant parsing stages, always track at least the arity of parsed results. The parser parses block labels, but more work will be required to support branch instructions that use them.
-rwxr-xr-xscripts/gen-s-parser.py2
-rw-r--r--src/gen-s-parser.inc2
-rw-r--r--src/wasm-builder.h8
-rw-r--r--src/wasm/wat-parser.cpp248
-rw-r--r--test/lit/wat-kitchen-sink.wast110
5 files changed, 349 insertions, 21 deletions
diff --git a/scripts/gen-s-parser.py b/scripts/gen-s-parser.py
index 0874d3218..689c30ae1 100755
--- a/scripts/gen-s-parser.py
+++ b/scripts/gen-s-parser.py
@@ -767,7 +767,7 @@ def instruction_parser(new_parser=False):
printer.print_line("parse_error:")
with printer.indent():
if new_parser:
- printer.print_line("return ctx.in.err(\"unrecognized instruction\");")
+ printer.print_line("return ctx.in.err(pos, \"unrecognized instruction\");")
else:
printer.print_line("throw ParseException(std::string(op), s.line, s.col);")
diff --git a/src/gen-s-parser.inc b/src/gen-s-parser.inc
index 148632ff5..83a7a3121 100644
--- a/src/gen-s-parser.inc
+++ b/src/gen-s-parser.inc
@@ -9544,7 +9544,7 @@ switch (buf[0]) {
default: goto parse_error;
}
parse_error:
- return ctx.in.err("unrecognized instruction");
+ return ctx.in.err(pos, "unrecognized instruction");
#endif // NEW_INSTRUCTION_PARSER
// clang-format on
diff --git a/src/wasm-builder.h b/src/wasm-builder.h
index 58a3a3188..a83a0c0c2 100644
--- a/src/wasm-builder.h
+++ b/src/wasm-builder.h
@@ -195,6 +195,14 @@ public:
ret->finalize(type);
return ret;
}
+ Block*
+ makeBlock(Name name, const std::vector<Expression*>& items, Type type) {
+ auto* ret = wasm.allocator.alloc<Block>();
+ ret->name = name;
+ ret->list.set(items);
+ ret->finalize(type);
+ return ret;
+ }
Block* makeBlock(const ExpressionList& items) {
auto* ret = wasm.allocator.alloc<Block>();
ret->list.set(items);
diff --git a/src/wasm/wat-parser.cpp b/src/wasm/wat-parser.cpp
index b1be9b75e..409d51052 100644
--- a/src/wasm/wat-parser.cpp
+++ b/src/wasm/wat-parser.cpp
@@ -471,7 +471,8 @@ struct NullTypeParserCtx {
using HeapTypeT = Ok;
using TypeT = Ok;
using ParamsT = Ok;
- using ResultsT = Ok;
+ using ResultsT = size_t;
+ using BlockTypeT = Ok;
using SignatureT = Ok;
using StorageT = Ok;
using FieldT = Ok;
@@ -504,8 +505,12 @@ struct NullTypeParserCtx {
ParamsT makeParams() { return Ok{}; }
void appendParam(ParamsT&, Name, TypeT) {}
- ResultsT makeResults() { return Ok{}; }
- void appendResult(ResultsT&, TypeT) {}
+ // We have to count results because whether or not a block introduces a
+ // typeuse that may implicitly define a type depends on how many results it
+ // has.
+ size_t makeResults() { return 0; }
+ void appendResult(size_t& results, TypeT) { ++results; }
+ size_t getResultsSize(size_t results) { return results; }
SignatureT makeFuncType(ParamsT*, ResultsT*) { return Ok{}; }
@@ -534,6 +539,10 @@ struct NullTypeParserCtx {
void appendDataString(DataStringT&, std::string_view) {}
MemTypeT makeMemType(Type, LimitsT, bool) { return Ok{}; }
+
+ BlockTypeT getBlockTypeFromResult(size_t results) { return Ok{}; }
+
+ Result<> getBlockTypeFromTypeUse(Index, TypeUseT) { return Ok{}; }
};
template<typename Ctx> struct TypeParserCtx {
@@ -542,6 +551,7 @@ template<typename Ctx> struct TypeParserCtx {
using TypeT = Type;
using ParamsT = std::vector<NameType>;
using ResultsT = std::vector<Type>;
+ using BlockTypeT = HeapType;
using SignatureT = Signature;
using StorageT = Field;
using FieldT = Field;
@@ -587,6 +597,7 @@ template<typename Ctx> struct TypeParserCtx {
ResultsT makeResults() { return {}; }
void appendResult(ResultsT& results, TypeT type) { results.push_back(type); }
+ size_t getResultsSize(const ResultsT& results) { return results.size(); }
SignatureT makeFuncType(ParamsT* params, ResultsT* results) {
std::vector<Type> empty;
@@ -644,6 +655,11 @@ template<typename Ctx> struct TypeParserCtx {
LimitsT getLimitsFromData(DataStringT) { return Ok{}; }
MemTypeT makeMemType(Type, LimitsT, bool) { return Ok{}; }
+
+ HeapType getBlockTypeFromResult(const std::vector<Type> results) {
+ assert(results.size() == 1);
+ return HeapType(Signature(Type::none, results[0]));
+ }
};
struct NullInstrParserCtx {
@@ -683,6 +699,10 @@ struct NullInstrParserCtx {
MemargT getMemarg(uint64_t, uint32_t) { return Ok{}; }
+ template<typename BlockTypeT>
+ InstrT makeBlock(Index, std::optional<Name>, BlockTypeT, InstrsT) {
+ return Ok{};
+ }
InstrT makeUnreachable(Index) { return Ok{}; }
InstrT makeNop(Index) { return Ok{}; }
InstrT makeBinary(Index, BinaryOp) { return Ok{}; }
@@ -792,7 +812,7 @@ struct ParseDeclsCtx : NullTypeParserCtx, NullInstrParserCtx {
ParseInput in;
// At this stage we only look at types to find implicit type definitions,
- // which are inserted directly in to the context. We cannot materialize or
+ // which are inserted directly into the context. We cannot materialize or
// validate any types because we don't know what types exist yet.
//
// Declared module elements are inserted into the module, but their bodies are
@@ -1190,6 +1210,16 @@ struct ParseModuleTypesCtx : TypeParserCtx<ParseModuleTypesCtx>,
return TypeUse{it->second, ids};
}
+ Result<HeapType> getBlockTypeFromTypeUse(Index pos, TypeUse use) {
+ assert(use.type.isSignature());
+ if (use.type.getSignature().params != Type::none) {
+ return in.err(pos, "block parameters not yet supported");
+ }
+ // TODO: Once we support block parameters, return an error here if any of
+ // them are named.
+ return use.type;
+ }
+
GlobalTypeT makeGlobalType(Mutability mutability, TypeT type) {
return {mutability, type};
}
@@ -1273,17 +1303,21 @@ struct ParseDefsCtx : TypeParserCtx<ParseDefsCtx> {
// local.get, etc.
Function* func = nullptr;
- // The stack of parsed expressions, used as the children of newly parsed
- // expressions.
- std::vector<Expression*> exprStack;
+ // The context for a single block scope, including the instructions parsed
+ // inside that scope so far and the ultimate result type we expect this block
+ // to have.
+ struct BlockCtx {
+ std::vector<Expression*> exprStack;
+ Type type;
+ };
+
+ // The stack of block contexts currently being parsed.
+ std::vector<BlockCtx> scopeStack;
// Whether we have seen an unreachable instruction and are in
// stack-polymorphic unreachable mode.
bool unreachable = false;
- // The expected result type of the instruction sequence we are parsing.
- Type type;
-
ParseDefsCtx(std::string_view in,
Module& wasm,
const std::vector<HeapType>& types,
@@ -1292,7 +1326,23 @@ struct ParseDefsCtx : TypeParserCtx<ParseDefsCtx> {
: TypeParserCtx(typeIndices), in(in), wasm(wasm), builder(wasm),
types(types), implicitTypes(implicitTypes) {}
+ std::vector<Expression*>& getExprStack() {
+ if (scopeStack.empty()) {
+ // We are not in a function, so push a dummy scope.
+ scopeStack.push_back({{}, Type::none});
+ }
+ return scopeStack.back().exprStack;
+ }
+
+ void pushScope(Type type) { scopeStack.push_back({{}, type}); }
+
+ Type getResultType() {
+ assert(!scopeStack.empty());
+ return scopeStack.back().type;
+ }
+
Result<> push(Index pos, Expression* expr) {
+ auto& exprStack = getExprStack();
if (expr->type == Type::unreachable) {
// We want to avoid popping back past this most recent unreachable
// instruction. Drop all prior instructions so they won't be consumed by
@@ -1310,7 +1360,7 @@ struct ParseDefsCtx : TypeParserCtx<ParseDefsCtx> {
for (Index i = 0; i < expr->type.size(); ++i) {
CHECK_ERR(push(pos,
builder.makeTupleExtract(
- builder.makeLocalGet(*scratchIdx, expr->type[i]), i)));
+ builder.makeLocalGet(*scratchIdx, expr->type), i)));
}
} else {
exprStack.push_back(expr);
@@ -1319,6 +1369,8 @@ struct ParseDefsCtx : TypeParserCtx<ParseDefsCtx> {
}
Result<Expression*> pop(Index pos) {
+ auto& exprStack = getExprStack();
+
// Find the suffix of expressions that do not produce values.
auto firstNone = exprStack.size();
for (; firstNone > 0; --firstNone) {
@@ -1363,11 +1415,25 @@ struct ParseDefsCtx : TypeParserCtx<ParseDefsCtx> {
return builder.makeBlock(exprs, expr->type);
}
+ HeapType getBlockTypeFromResult(const std::vector<Type> results) {
+ assert(results.size() == 1);
+ pushScope(results[0]);
+ return HeapType(Signature(Type::none, results[0]));
+ }
+
+ Result<HeapType> getBlockTypeFromTypeUse(Index pos, HeapType type) {
+ pushScope(type.getSignature().results);
+ return type;
+ }
+
Ok makeInstrs() { return Ok{}; }
void appendInstr(Ok&, InstrT instr) {}
Result<InstrsT> finishInstrs(Ok&) {
+ auto& exprStack = getExprStack();
+ auto type = getResultType();
+
// We have finished parsing a sequence of instructions. Fix up the parsed
// instructions and reset the context for the next sequence.
if (type.isTuple()) {
@@ -1396,11 +1462,16 @@ struct ParseDefsCtx : TypeParserCtx<ParseDefsCtx> {
exprStack.push_back(*expr);
}
unreachable = false;
- return std::move(exprStack);
+ auto ret = std::move(exprStack);
+ scopeStack.pop_back();
+ return ret;
}
Expression* instrToExpr(Ok&) {
+ auto& exprStack = getExprStack();
+ assert(scopeStack.size() == 1);
assert(exprStack.size() == 1);
+
auto e = exprStack.back();
exprStack.clear();
unreachable = false;
@@ -1433,7 +1504,7 @@ struct ParseDefsCtx : TypeParserCtx<ParseDefsCtx> {
Result<Index> getLocalFromIdx(uint32_t idx) {
if (!func) {
- return in.err("cannot access locals outside of a funcion");
+ return in.err("cannot access locals outside of a function");
}
if (idx >= func->getNumLocals()) {
return in.err("local index out of bounds");
@@ -1627,6 +1698,20 @@ struct ParseDefsCtx : TypeParserCtx<ParseDefsCtx> {
return wasm.memories[0]->name;
}
+ Result<> makeBlock(Index pos,
+ std::optional<Name> label,
+ HeapType type,
+ const std::vector<Expression*>& instrs) {
+ // TODO: validate labels?
+ // TODO: Move error on input types to here?
+ auto results = type.getSignature().results;
+ if (label) {
+ return push(pos, builder.makeBlock(*label, instrs, results));
+ } else {
+ return push(pos, builder.makeBlock(instrs, results));
+ }
+ }
+
Result<> makeUnreachable(Index pos) {
return push(pos, builder.makeUnreachable());
}
@@ -2153,10 +2238,17 @@ template<typename Ctx> Result<typename Ctx::MemTypeT> memtype(Ctx&);
template<typename Ctx> Result<typename Ctx::GlobalTypeT> globaltype(Ctx&);
// Instructions
+template<typename Ctx> MaybeResult<typename Ctx::InstrT> foldedBlockinstr(Ctx&);
+template<typename Ctx>
+MaybeResult<typename Ctx::InstrT> unfoldedBlockinstr(Ctx&);
+template<typename Ctx> MaybeResult<typename Ctx::InstrT> blockinstr(Ctx&);
+template<typename Ctx> MaybeResult<typename Ctx::InstrT> plaininstr(Ctx&);
template<typename Ctx> MaybeResult<typename Ctx::InstrT> instr(Ctx&);
template<typename Ctx> Result<typename Ctx::InstrsT> instrs(Ctx&);
template<typename Ctx> Result<typename Ctx::ExprT> expr(Ctx&);
template<typename Ctx> Result<typename Ctx::MemargT> memarg(Ctx&, uint32_t);
+template<typename Ctx> Result<typename Ctx::BlockTypeT> blocktype(Ctx&);
+template<typename Ctx> MaybeResult<typename Ctx::InstrT> block(Ctx&, bool);
template<typename Ctx>
Result<typename Ctx::InstrT> makeUnreachable(Ctx&, Index);
template<typename Ctx> Result<typename Ctx::InstrT> makeNop(Ctx&, Index);
@@ -2665,7 +2757,37 @@ template<typename Ctx> Result<typename Ctx::GlobalTypeT> globaltype(Ctx& ctx) {
// Instructions
// ============
-template<typename Ctx> MaybeResult<typename Ctx::InstrT> instr(Ctx& ctx) {
+// blockinstr ::= block | loop | if-else | try-catch
+template<typename Ctx>
+MaybeResult<typename Ctx::InstrT> foldedBlockinstr(Ctx& ctx) {
+ if (auto i = block(ctx, true)) {
+ return i;
+ }
+ // TODO: Other block instructions
+ return {};
+}
+
+template<typename Ctx>
+MaybeResult<typename Ctx::InstrT> unfoldedBlockinstr(Ctx& ctx) {
+ if (auto i = block(ctx, false)) {
+ return i;
+ }
+ // TODO: Other block instructions
+ return {};
+}
+
+template<typename Ctx> MaybeResult<typename Ctx::InstrT> blockinstr(Ctx& ctx) {
+ if (auto i = foldedBlockinstr(ctx)) {
+ return i;
+ }
+ if (auto i = unfoldedBlockinstr(ctx)) {
+ return i;
+ }
+ return {};
+}
+
+// plaininstr ::= ... all plain instructions ...
+template<typename Ctx> MaybeResult<typename Ctx::InstrT> plaininstr(Ctx& ctx) {
auto pos = ctx.in.getPos();
auto keyword = ctx.in.takeKeyword();
if (!keyword) {
@@ -2677,10 +2799,36 @@ template<typename Ctx> MaybeResult<typename Ctx::InstrT> instr(Ctx& ctx) {
#include <gen-s-parser.inc>
}
+// instr ::= plaininstr | blockinstr
+template<typename Ctx> MaybeResult<typename Ctx::InstrT> instr(Ctx& ctx) {
+ // Check for valid strings that are not instructions.
+ if (auto tok = ctx.in.peek()) {
+ if (auto keyword = tok->getKeyword()) {
+ if (keyword == "end"sv) {
+ return {};
+ }
+ }
+ }
+ if (auto i = blockinstr(ctx)) {
+ return i;
+ }
+ if (auto i = plaininstr(ctx)) {
+ return i;
+ }
+ // TODO: Handle folded plain instructions as well.
+ return {};
+}
+
template<typename Ctx> Result<typename Ctx::InstrsT> instrs(Ctx& ctx) {
auto insts = ctx.makeInstrs();
while (true) {
+ if (auto blockinst = foldedBlockinstr(ctx)) {
+ CHECK_ERR(blockinst);
+ ctx.appendInstr(insts, *blockinst);
+ continue;
+ }
+ // Parse an arbitrary number of folded instructions.
if (ctx.in.takeLParen()) {
// A stack of (start, end) position pairs defining the positions of
// instructions that need to be parsed after their folded children.
@@ -2704,7 +2852,10 @@ template<typename Ctx> Result<typename Ctx::InstrsT> instrs(Ctx& ctx) {
// We have either the start of a new folded child or the end of the last
// one.
- if (ctx.in.takeLParen()) {
+ if (auto blockinst = foldedBlockinstr(ctx)) {
+ CHECK_ERR(blockinst);
+ ctx.appendInstr(insts, *blockinst);
+ } else if (ctx.in.takeLParen()) {
foldedInstrs.push_back({ctx.in.getPos(), {}});
} else if (ctx.in.takeRParen()) {
auto [start, end] = foldedInstrs.back();
@@ -2712,7 +2863,7 @@ template<typename Ctx> Result<typename Ctx::InstrsT> instrs(Ctx& ctx) {
foldedInstrs.pop_back();
WithPosition with(ctx, start);
- if (auto inst = instr(ctx)) {
+ if (auto inst = plaininstr(ctx)) {
CHECK_ERR(inst);
ctx.appendInstr(insts, *inst);
} else {
@@ -2763,6 +2914,69 @@ Result<typename Ctx::MemargT> memarg(Ctx& ctx, uint32_t n) {
return ctx.getMemarg(offset, align);
}
+// blocktype ::= (t:result)? => t? | x,I:typeuse => x if I = {}
+template<typename Ctx> Result<typename Ctx::BlockTypeT> blocktype(Ctx& ctx) {
+ auto pos = ctx.in.getPos();
+
+ if (auto res = results(ctx)) {
+ CHECK_ERR(res);
+ if (ctx.getResultsSize(*res) == 1) {
+ return ctx.getBlockTypeFromResult(*res);
+ }
+ }
+
+ // We either had no results or multiple results. Reset and parse again as a
+ // type use.
+ ctx.in.lexer.setIndex(pos);
+ auto use = typeuse(ctx);
+ CHECK_ERR(use);
+
+ auto type = ctx.getBlockTypeFromTypeUse(pos, *use);
+ CHECK_ERR(type);
+ return *type;
+}
+
+// block ::= 'block' label blocktype instr* 'end' id? if id = {} or id = label
+// | '(' 'block' label blocktype instr* ')'
+template<typename Ctx>
+MaybeResult<typename Ctx::InstrT> block(Ctx& ctx, bool folded) {
+ auto pos = ctx.in.getPos();
+
+ if (folded) {
+ if (!ctx.in.takeSExprStart("block"sv)) {
+ return {};
+ }
+ } else {
+ if (!ctx.in.takeKeyword("block"sv)) {
+ return {};
+ }
+ }
+
+ auto label = ctx.in.takeID();
+
+ auto type = blocktype(ctx);
+ CHECK_ERR(type);
+
+ auto insts = instrs(ctx);
+ CHECK_ERR(insts);
+
+ if (folded) {
+ if (!ctx.in.takeRParen()) {
+ return ctx.in.err("expected ')' at end of block");
+ }
+ } else {
+ if (!ctx.in.takeKeyword("end"sv)) {
+ return ctx.in.err("expected 'end' at end of block");
+ }
+ auto id = ctx.in.takeID();
+ if (id && id != label) {
+ return ctx.in.err("end label does not match block label");
+ }
+ }
+
+ return ctx.makeBlock(pos, label, *type, std::move(*insts));
+}
+
template<typename Ctx>
Result<typename Ctx::InstrT> makeUnreachable(Ctx& ctx, Index pos) {
return ctx.makeUnreachable(pos);
@@ -4023,7 +4237,7 @@ Result<> parseModule(Module& wasm, std::string_view input) {
for (Index i = 0; i < decls.funcDefs.size(); ++i) {
ctx.index = i;
ctx.func = wasm.functions[i].get();
- ctx.type = ctx.func->getResults();
+ ctx.pushScope(ctx.func->getResults());
WithPosition with(ctx, decls.funcDefs[i].pos);
auto parsed = func(ctx);
CHECK_ERR(parsed);
diff --git a/test/lit/wat-kitchen-sink.wast b/test/lit/wat-kitchen-sink.wast
index f2110dc42..889b82fc4 100644
--- a/test/lit/wat-kitchen-sink.wast
+++ b/test/lit/wat-kitchen-sink.wast
@@ -9,12 +9,12 @@
;; CHECK: (type $pair (struct (field (mut i32)) (field (mut i64))))
- ;; CHECK: (type $none_=>_i32 (func (result i32)))
-
;; CHECK: (type $ret2 (func (result i32 i32)))
(type $ret2 (func (result i32 i32)))
(rec
+ ;; CHECK: (type $none_=>_i32 (func (result i32)))
+
;; CHECK: (type $i32_i64_=>_none (func (param i32 i64)))
;; CHECK: (type $a1 (array i64))
@@ -701,6 +701,112 @@
drop
)
+ ;; CHECK: (func $block (type $void)
+ ;; CHECK-NEXT: (nop)
+ ;; CHECK-NEXT: (block $l
+ ;; CHECK-NEXT: (nop)
+ ;; CHECK-NEXT: (nop)
+ ;; CHECK-NEXT: (nop)
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: )
+ (func $block
+ nop
+ block $l (type $void)
+ nop
+ nop
+ nop
+ end $l
+ )
+
+ ;; CHECK: (func $block-folded (type $void)
+ ;; CHECK-NEXT: (local $scratch (i32 i32))
+ ;; CHECK-NEXT: (nop)
+ ;; CHECK-NEXT: (local.set $scratch
+ ;; CHECK-NEXT: (block $l (result i32 i32)
+ ;; CHECK-NEXT: (nop)
+ ;; CHECK-NEXT: (nop)
+ ;; CHECK-NEXT: (unreachable)
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: (drop
+ ;; CHECK-NEXT: (tuple.extract 0
+ ;; CHECK-NEXT: (local.get $scratch)
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: (drop
+ ;; CHECK-NEXT: (tuple.extract 1
+ ;; CHECK-NEXT: (local.get $scratch)
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: (unreachable)
+ ;; CHECK-NEXT: )
+ (func $block-folded
+ nop
+ (block $l (result i32) (result) (result i32)
+ nop
+ nop
+ unreachable
+ )
+ unreachable
+ )
+
+ ;; CHECK: (func $block-mix (type $void)
+ ;; CHECK-NEXT: (local $scratch i32)
+ ;; CHECK-NEXT: (local $scratch_0 (i32 i32))
+ ;; CHECK-NEXT: (local $scratch_1 i32)
+ ;; CHECK-NEXT: (block $0
+ ;; CHECK-NEXT: (local.set $scratch_0
+ ;; CHECK-NEXT: (block $1 (result i32 i32)
+ ;; CHECK-NEXT: (tuple.make
+ ;; CHECK-NEXT: (block $2 (result i32)
+ ;; CHECK-NEXT: (block (result i32)
+ ;; CHECK-NEXT: (local.set $scratch
+ ;; CHECK-NEXT: (block $3 (result i32)
+ ;; CHECK-NEXT: (i32.const 0)
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: (nop)
+ ;; CHECK-NEXT: (local.get $scratch)
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: (i32.const 1)
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: (drop
+ ;; CHECK-NEXT: (block (result i32)
+ ;; CHECK-NEXT: (local.set $scratch_1
+ ;; CHECK-NEXT: (tuple.extract 0
+ ;; CHECK-NEXT: (local.get $scratch_0)
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: (drop
+ ;; CHECK-NEXT: (tuple.extract 1
+ ;; CHECK-NEXT: (local.get $scratch_0)
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: (local.get $scratch_1)
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: (nop)
+ ;; CHECK-NEXT: )
+ (func $block-mix
+ (block $0
+ block $1 (type $ret2)
+ (block $2 (result i32)
+ block $3 (result i32)
+ i32.const 0
+ end
+ nop
+ )
+ i32.const 1
+ end $1
+ drop
+ drop
+ )
+ nop
+ )
;; CHECK: (func $binary (type $i32_i32_f64_f64_=>_none) (param $0 i32) (param $1 i32) (param $2 f64) (param $3 f64)
;; CHECK-NEXT: (drop