Skip to content

Commit 5e29a61

Browse files
authored
Fix emitting of unreachable block/if/loop (#911)
* an unreachable block is one with an unreachable child, plus no breaks * document new difference between binaryen IR and wasm * fix relooper missing finalize * add a bunch of tests * don't assume that test/*.wast files print to themselves exactly; print to from.wast. this allows wast tests with comments in them * emit unreachable blocks as (block .. unreachable) unreachable * if without else and unreachable ifTrue is still not unreachable, it should be none * update wasm.js * cleanups * empty blocks have none type
1 parent 9707769 commit 5e29a61

33 files changed

+2147
-25
lines changed

README.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,9 @@ Binaryen's internal IR is an AST, designed to be
3434

3535
The differences between Binaryen IR and WebAssembly are:
3636

37-
* Binaryen IR [is an AST](https://github.com/WebAssembly/binaryen/issues/663). This differs from the WebAssembly binary format which is a stack machine.
38-
* Binaryen IR requires the names of blocks and loops to be unique. This differs from the WebAssembly s-expression format which allows duplicate names (and depends on scoping to disambiguate).
37+
* Binaryen IR [is an AST](https://github.com/WebAssembly/binaryen/issues/663), for convenience of optimization. This differs from the WebAssembly binary format which is a stack machine.
38+
* WebAssembly limits block/if/loop types to none and the concrete value types (i32, i64, f32, f64). Binaryen IR has an unreachable type, and it allows block/if/loop to take it, allowing [local transforms that don't need to know the global context](https://github.com/WebAssembly/binaryen/issues/903).
39+
* Binaryen IR's text format requires the names of blocks and loops to be unique. This differs from the WebAssembly s-expression format which allows duplicate names (and depends on scoping to disambiguate).
3940

4041
As a result, you might notice that round-trip conversions (wasm => Binaryen IR => wasm) change code a little in some corner cases.
4142

auto_update_tests.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -174,10 +174,11 @@
174174
if t.endswith('.wast') and not t.startswith('spec'):
175175
print '..', t
176176
t = os.path.join('test', t)
177+
f = t + '.from-wast'
177178
cmd = [os.path.join('bin', 'wasm-opt'), t, '--print']
178179
actual = run_command(cmd)
179180
actual = actual.replace('printing before:\n', '')
180-
open(t, 'w').write(actual)
181+
open(f, 'w').write(actual)
181182

182183
print '\n[ checking wasm-dis on provided binaries... ]\n'
183184

bin/wasm.js

Lines changed: 9 additions & 9 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

check.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -184,11 +184,12 @@
184184
if t.endswith('.wast') and not t.startswith('spec'):
185185
print '..', t
186186
t = os.path.join(options.binaryen_test, t)
187+
f = t + '.from-wast'
187188
cmd = WASM_OPT + [t, '--print']
188189
actual = run_command(cmd)
189190
actual = actual.replace('printing before:\n', '')
190191

191-
expected = open(t, 'rb').read()
192+
expected = open(f, 'rb').read()
192193
if actual != expected:
193194
fail(actual, expected)
194195

src/cfg/Relooper.cpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@ static wasm::Expression* HandleFollowupMultiples(wasm::Expression* Ret, Shape* P
9090
}
9191
}
9292
}
93+
Curr->finalize();
9394
return Curr;
9495
}
9596

src/wasm-validator.h

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,9 @@ struct WasmValidator : public PostWalker<WasmValidator, Visitor<WasmValidator>>
128128
}
129129
}
130130
if (!isConcreteWasmType(curr->type) && curr->list.size() > 0) {
131-
shouldBeFalse(isConcreteWasmType(curr->list.back()->type), curr, "block with no value cannot have a last element with a value");
131+
if (isConcreteWasmType(curr->list.back()->type)) {
132+
shouldBeTrue(curr->type == unreachable, curr, "block with no value and a last element with a value must be unreachable");
133+
}
132134
}
133135
}
134136

@@ -155,6 +157,9 @@ struct WasmValidator : public PostWalker<WasmValidator, Visitor<WasmValidator>>
155157
shouldBeTrue(curr->condition->type == unreachable || curr->condition->type == i32 || curr->condition->type == i64, curr, "if condition must be valid");
156158
if (!curr->ifFalse) {
157159
shouldBeFalse(isConcreteWasmType(curr->ifTrue->type), curr, "if without else must not return a value in body");
160+
if (curr->condition->type != unreachable) {
161+
shouldBeEqual(curr->type, none, curr, "if without else and reachable condition must be none");
162+
}
158163
}
159164
}
160165

src/wasm/wasm-binary.cpp

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -461,24 +461,39 @@ void WasmBinaryWriter::recurse(Expression*& curr) {
461461
if (debug) std::cerr << "zz recurse from " << depth-- << " at " << o.size() << std::endl;
462462
}
463463

464+
static bool brokenTo(Block* block) {
465+
return block->name.is() && BreakSeeker::has(block, block->name);
466+
}
467+
464468
void WasmBinaryWriter::visitBlock(Block *curr) {
465469
if (debug) std::cerr << "zz node: Block" << std::endl;
466470
o << int8_t(BinaryConsts::Block);
467471
o << binaryWasmType(curr->type != unreachable ? curr->type : none);
468472
breakStack.push_back(curr->name);
469-
size_t i = 0;
473+
Index i = 0;
470474
for (auto* child : curr->list) {
471475
if (debug) std::cerr << " " << size_t(curr) << "\n zz Block element " << i++ << std::endl;
472476
recurse(child);
473477
}
474478
breakStack.pop_back();
479+
if (curr->type == unreachable) {
480+
// an unreachable block is one that cannot be exited. We cannot encode this directly
481+
// in wasm, where blocks must be none,i32,i64,f32,f64. Since the block cannot be
482+
// exited, we can emit an unreachable at the end, and that will always be valid,
483+
// and then the block is ok as a none
484+
o << int8_t(BinaryConsts::Unreachable);
485+
}
475486
o << int8_t(BinaryConsts::End);
487+
if (curr->type == unreachable) {
488+
// and emit an unreachable *outside* the block too, so later things can pop anything
489+
o << int8_t(BinaryConsts::Unreachable);
490+
}
476491
}
477492

478493
// emits a node, but if it is a block with no name, emit a list of its contents
479494
void WasmBinaryWriter::recursePossibleBlockContents(Expression* curr) {
480495
auto* block = curr->dynCast<Block>();
481-
if (!block || (block->name.is() && BreakSeeker::has(curr, block->name))) {
496+
if (!block || brokenTo(block)) {
482497
recurse(curr);
483498
return;
484499
}
@@ -489,6 +504,18 @@ void WasmBinaryWriter::recursePossibleBlockContents(Expression* curr) {
489504

490505
void WasmBinaryWriter::visitIf(If *curr) {
491506
if (debug) std::cerr << "zz node: If" << std::endl;
507+
if (curr->type == unreachable && curr->ifFalse) {
508+
if (curr->condition->type == unreachable) {
509+
// this if-else is unreachable because of the condition, i.e., the condition
510+
// does not exit. So don't emit the if, but do consume the condition
511+
recurse(curr->condition);
512+
o << int8_t(BinaryConsts::Unreachable);
513+
return;
514+
}
515+
// an unreachable if-else (with reachable condition) is one where both sides do not fall through.
516+
// wasm does not allow this to be emitted directly, so we must do something more. we could do
517+
// better, but for now we emit an extra unreachable instruction after the if, so it is not consumed itself
518+
}
492519
recurse(curr->condition);
493520
o << int8_t(BinaryConsts::If);
494521
o << binaryWasmType(curr->type != unreachable ? curr->type : none);
@@ -502,6 +529,11 @@ void WasmBinaryWriter::visitIf(If *curr) {
502529
breakStack.pop_back();
503530
}
504531
o << int8_t(BinaryConsts::End);
532+
if (curr->type == unreachable) {
533+
// see explanation above - we emitted an if without a return type, so it must not be consumed
534+
assert(curr->ifFalse); // otherwise, if without else, that is unreachable, must have an unreachable condition, which was handled earlier
535+
o << int8_t(BinaryConsts::Unreachable);
536+
}
505537
}
506538
void WasmBinaryWriter::visitLoop(Loop *curr) {
507539
if (debug) std::cerr << "zz node: Loop" << std::endl;
@@ -511,6 +543,10 @@ void WasmBinaryWriter::visitLoop(Loop *curr) {
511543
recursePossibleBlockContents(curr->body);
512544
breakStack.pop_back();
513545
o << int8_t(BinaryConsts::End);
546+
if (curr->type == unreachable) {
547+
// we emitted a loop without a return type, so it must not be consumed
548+
o << int8_t(BinaryConsts::Unreachable);
549+
}
514550
}
515551

516552
int32_t WasmBinaryWriter::getBreakIndex(Name name) { // -1 if not found

src/wasm/wasm.cpp

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -133,14 +133,30 @@ static WasmType mergeTypes(std::vector<WasmType>& types) {
133133
return type;
134134
}
135135

136+
// a block is unreachable if one of its elements is unreachable,
137+
// and there are no branches to it
138+
static void handleUnreachable(Block* block) {
139+
if (block->type == unreachable) return; // nothing to do
140+
for (auto* child : block->list) {
141+
if (child->type == unreachable) {
142+
// there is an unreachable child, so we are unreachable, unless we have a break
143+
BreakSeeker seeker(block->name);
144+
Expression* expr = block;
145+
seeker.walk(expr);
146+
if (!seeker.found) {
147+
block->type = unreachable;
148+
} else {
149+
block->type = seeker.valueType;
150+
}
151+
return;
152+
}
153+
}
154+
}
155+
136156
void Block::finalize(WasmType type_) {
137157
type = type_;
138158
if (type == none && list.size() > 0) {
139-
if (list.back()->type == unreachable) {
140-
if (!BreakSeeker::has(this, name)) {
141-
type = unreachable; // the last element is unreachable, and this block truly cannot be exited, so it is unreachable itself
142-
}
143-
}
159+
handleUnreachable(this);
144160
}
145161
}
146162

@@ -150,18 +166,19 @@ void Block::finalize() {
150166
if (list.size() > 0) {
151167
type = list.back()->type;
152168
} else {
153-
type = unreachable;
169+
type = none;
154170
}
155171
return;
156172
}
157173

158174
TypeSeeker seeker(this, this->name);
159175
type = mergeTypes(seeker.types);
176+
handleUnreachable(this);
160177
}
161178

162179
void If::finalize(WasmType type_) {
163180
type = type_;
164-
if (type == none && (condition->type == unreachable || (ifTrue->type == unreachable && (!ifFalse || ifFalse->type == unreachable)))) {
181+
if (type == none && (condition->type == unreachable || (ifFalse && ifTrue->type && ifFalse->type == unreachable))) {
165182
type = unreachable;
166183
}
167184
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
(module
2+
(import "env" "table" (table 0 0 anyfunc))
3+
(memory $0 0)
4+
)

test/empty_table.wast.from-wast

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
(module
2+
(table 0 0 anyfunc)
3+
(memory $0 0)
4+
)

0 commit comments

Comments
 (0)