Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix emitting of unreachable block/if/loop #911

Merged
merged 23 commits into from
Feb 16, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,9 @@ Binaryen's internal IR is an AST, designed to be

The differences between Binaryen IR and WebAssembly are:

* Binaryen IR [is an AST](https://github.com/WebAssembly/binaryen/issues/663). This differs from the WebAssembly binary format which is a stack machine.
* 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).
* 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.
* 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).
* 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).

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

Expand Down
3 changes: 2 additions & 1 deletion auto_update_tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -174,10 +174,11 @@
if t.endswith('.wast') and not t.startswith('spec'):
print '..', t
t = os.path.join('test', t)
f = t + '.from-wast'
cmd = [os.path.join('bin', 'wasm-opt'), t, '--print']
actual = run_command(cmd)
actual = actual.replace('printing before:\n', '')
open(t, 'w').write(actual)
open(f, 'w').write(actual)

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

Expand Down
18 changes: 9 additions & 9 deletions bin/wasm.js

Large diffs are not rendered by default.

3 changes: 2 additions & 1 deletion check.py
Original file line number Diff line number Diff line change
Expand Up @@ -184,11 +184,12 @@
if t.endswith('.wast') and not t.startswith('spec'):
print '..', t
t = os.path.join(options.binaryen_test, t)
f = t + '.from-wast'
cmd = WASM_OPT + [t, '--print']
actual = run_command(cmd)
actual = actual.replace('printing before:\n', '')

expected = open(t, 'rb').read()
expected = open(f, 'rb').read()
if actual != expected:
fail(actual, expected)

Expand Down
1 change: 1 addition & 0 deletions src/cfg/Relooper.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ static wasm::Expression* HandleFollowupMultiples(wasm::Expression* Ret, Shape* P
}
}
}
Curr->finalize();
return Curr;
}

Expand Down
7 changes: 6 additions & 1 deletion src/wasm-validator.h
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,9 @@ struct WasmValidator : public PostWalker<WasmValidator, Visitor<WasmValidator>>
}
}
if (!isConcreteWasmType(curr->type) && curr->list.size() > 0) {
shouldBeFalse(isConcreteWasmType(curr->list.back()->type), curr, "block with no value cannot have a last element with a value");
if (isConcreteWasmType(curr->list.back()->type)) {
shouldBeTrue(curr->type == unreachable, curr, "block with no value and a last element with a value must be unreachable");
}
}
}

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

Expand Down
40 changes: 38 additions & 2 deletions src/wasm/wasm-binary.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -461,24 +461,39 @@ void WasmBinaryWriter::recurse(Expression*& curr) {
if (debug) std::cerr << "zz recurse from " << depth-- << " at " << o.size() << std::endl;
}

static bool brokenTo(Block* block) {
return block->name.is() && BreakSeeker::has(block, block->name);
}

void WasmBinaryWriter::visitBlock(Block *curr) {
if (debug) std::cerr << "zz node: Block" << std::endl;
o << int8_t(BinaryConsts::Block);
o << binaryWasmType(curr->type != unreachable ? curr->type : none);
breakStack.push_back(curr->name);
size_t i = 0;
Index i = 0;
for (auto* child : curr->list) {
if (debug) std::cerr << " " << size_t(curr) << "\n zz Block element " << i++ << std::endl;
recurse(child);
}
breakStack.pop_back();
if (curr->type == unreachable) {
// an unreachable block is one that cannot be exited. We cannot encode this directly
// in wasm, where blocks must be none,i32,i64,f32,f64. Since the block cannot be
// exited, we can emit an unreachable at the end, and that will always be valid,
// and then the block is ok as a none
o << int8_t(BinaryConsts::Unreachable);
}
o << int8_t(BinaryConsts::End);
if (curr->type == unreachable) {
// and emit an unreachable *outside* the block too, so later things can pop anything
o << int8_t(BinaryConsts::Unreachable);
}
}

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

void WasmBinaryWriter::visitIf(If *curr) {
if (debug) std::cerr << "zz node: If" << std::endl;
if (curr->type == unreachable && curr->ifFalse) {
if (curr->condition->type == unreachable) {
// this if-else is unreachable because of the condition, i.e., the condition
// does not exit. So don't emit the if, but do consume the condition
recurse(curr->condition);
o << int8_t(BinaryConsts::Unreachable);
return;
}
// an unreachable if-else (with reachable condition) is one where both sides do not fall through.
// wasm does not allow this to be emitted directly, so we must do something more. we could do
// better, but for now we emit an extra unreachable instruction after the if, so it is not consumed itself
}
recurse(curr->condition);
o << int8_t(BinaryConsts::If);
o << binaryWasmType(curr->type != unreachable ? curr->type : none);
Expand All @@ -502,6 +529,11 @@ void WasmBinaryWriter::visitIf(If *curr) {
breakStack.pop_back();
}
o << int8_t(BinaryConsts::End);
if (curr->type == unreachable) {
// see explanation above - we emitted an if without a return type, so it must not be consumed
assert(curr->ifFalse); // otherwise, if without else, that is unreachable, must have an unreachable condition, which was handled earlier
o << int8_t(BinaryConsts::Unreachable);
}
}
void WasmBinaryWriter::visitLoop(Loop *curr) {
if (debug) std::cerr << "zz node: Loop" << std::endl;
Expand All @@ -511,6 +543,10 @@ void WasmBinaryWriter::visitLoop(Loop *curr) {
recursePossibleBlockContents(curr->body);
breakStack.pop_back();
o << int8_t(BinaryConsts::End);
if (curr->type == unreachable) {
// we emitted a loop without a return type, so it must not be consumed
o << int8_t(BinaryConsts::Unreachable);
}
}

int32_t WasmBinaryWriter::getBreakIndex(Name name) { // -1 if not found
Expand Down
31 changes: 24 additions & 7 deletions src/wasm/wasm.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -133,14 +133,30 @@ static WasmType mergeTypes(std::vector<WasmType>& types) {
return type;
}

// a block is unreachable if one of its elements is unreachable,
// and there are no branches to it
static void handleUnreachable(Block* block) {
if (block->type == unreachable) return; // nothing to do
for (auto* child : block->list) {
if (child->type == unreachable) {
// there is an unreachable child, so we are unreachable, unless we have a break
BreakSeeker seeker(block->name);
Expression* expr = block;
seeker.walk(expr);
if (!seeker.found) {
block->type = unreachable;
} else {
block->type = seeker.valueType;
}
return;
}
}
}

void Block::finalize(WasmType type_) {
type = type_;
if (type == none && list.size() > 0) {
if (list.back()->type == unreachable) {
if (!BreakSeeker::has(this, name)) {
type = unreachable; // the last element is unreachable, and this block truly cannot be exited, so it is unreachable itself
}
}
handleUnreachable(this);
}
}

Expand All @@ -150,18 +166,19 @@ void Block::finalize() {
if (list.size() > 0) {
type = list.back()->type;
} else {
type = unreachable;
type = none;
}
return;
}

TypeSeeker seeker(this, this->name);
type = mergeTypes(seeker.types);
handleUnreachable(this);
}

void If::finalize(WasmType type_) {
type = type_;
if (type == none && (condition->type == unreachable || (ifTrue->type == unreachable && (!ifFalse || ifFalse->type == unreachable)))) {
if (type == none && (condition->type == unreachable || (ifFalse && ifTrue->type && ifFalse->type == unreachable))) {
type = unreachable;
}
}
Expand Down
4 changes: 4 additions & 0 deletions test/empty_imported_table.wast.from-wast
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
(module
(import "env" "table" (table 0 0 anyfunc))
(memory $0 0)
)
4 changes: 4 additions & 0 deletions test/empty_table.wast.from-wast
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
(module
(table 0 0 anyfunc)
(memory $0 0)
)
14 changes: 13 additions & 1 deletion test/example/c-api-unused-mem.txt
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@
(call $main)
)
)
195
207
(module
(type $0 (func))
(type $1 (func))
Expand All @@ -67,15 +67,19 @@
)
(block $label$2
(br $label$1)
(unreachable)
)
(unreachable)
)
(block $label$3
(block $label$4
(block $label$5
)
(block $label$6
(br $label$4)
(unreachable)
)
(unreachable)
)
(block $label$7
(block $label$8
Expand All @@ -84,10 +88,18 @@
(get_local $var$0)
)
(return)
(unreachable)
)
(unreachable)
(unreachable)
)
(unreachable)
(unreachable)
)
(unreachable)
(unreachable)
)
(unreachable)
)
(func $__wasm_start (type $1)
(block $label$0
Expand Down
11 changes: 11 additions & 0 deletions test/hello_world.wast.from-wast
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
(module
(type $0 (func (param i32 i32) (result i32)))
(memory $0 256 256)
(export "add" (func $add))
(func $add (type $0) (param $x i32) (param $y i32) (result i32)
(i32.add
(get_local $x)
(get_local $y)
)
)
)
4 changes: 4 additions & 0 deletions test/imported_memory.wast.from-wast
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
(module
(import "env" "memory" (memory $0 256 256))
(import "env" "table" (table 256 256 anyfunc))
)
4 changes: 4 additions & 0 deletions test/imported_memory_growth.wast.from-wast
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
(module
(import "env" "memory" (memory $0 256))
(import "env" "table" (table 256 anyfunc))
)
Loading