Skip to content

Commit 7c49199

Browse files
authored
Flattening rewrite (#1201)
Rename flatten-control-flow to flatten, which now flattens everything, not just control flow, so e.g. (i32.add (call $x) (call $y) ) ==> (block (set_local $temp_x (call $x)) (set_local $temp_y (call $y)) (i32.add (get_local $x) (get_local $y) ) ) This uses more locals than before, but is much simpler and avoids a bunch of corner cases and fuzz bugs the old one hit. We can optimize later if necessary.
1 parent 47c37d0 commit 7c49199

26 files changed

+4625
-2245
lines changed

auto_update_tests.py

Lines changed: 17 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
ASM2WASM, MOZJS, S2WASM, WASM_SHELL, WASM_OPT, WASM_AS, WASM_DIS,
88
WASM_CTOR_EVAL, WASM_MERGE, WASM_REDUCE, WASM2ASM,
99
BINARYEN_INSTALL_DIR, has_shell_timeout)
10-
from scripts.test.wasm2asm import tests, spec_tests, extra_tests
10+
from scripts.test.wasm2asm import tests, spec_tests, extra_tests, assert_tests
1111

1212

1313
print '[ processing and updating testcases... ]\n'
@@ -64,19 +64,6 @@
6464
expected_file = os.path.join('test', dot_s_dir, wasm)
6565
with open(expected_file, 'w') as o: o.write(actual)
6666

67-
'''
68-
for wasm in ['address.wast']:#os.listdir(os.path.join('test', 'spec')):
69-
if wasm.endswith('.wast'):
70-
print '..', wasm
71-
asm = wasm.replace('.wast', '.2asm.js')
72-
proc = subprocess.Popen([os.path.join('bin', 'wasm2asm'), os.path.join('test', 'spec', wasm)], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
73-
actual, err = proc.communicate()
74-
assert proc.returncode == 0, err
75-
assert err == '', 'bad err:' + err
76-
expected_file = os.path.join('test', asm)
77-
open(expected_file, 'w').write(actual)
78-
'''
79-
8067
for t in sorted(os.listdir(os.path.join('test', 'print'))):
8168
if t.endswith('.wast'):
8269
print '..', t
@@ -281,6 +268,22 @@
281268
out = run_command(cmd)
282269
with open(expected_file, 'w') as o: o.write(out)
283270

271+
for wasm in assert_tests:
272+
print '..', wasm
273+
274+
asserts = os.path.basename(wasm).replace('.wast.asserts', '.asserts.js')
275+
traps = os.path.basename(wasm).replace('.wast.asserts', '.traps.js')
276+
asserts_expected_file = os.path.join('test', asserts)
277+
traps_expected_file = os.path.join('test', traps)
278+
279+
cmd = WASM2ASM + [os.path.join('test', wasm), '--allow-asserts']
280+
out = run_command(cmd)
281+
with open(asserts_expected_file, 'w') as o: o.write(out)
282+
283+
cmd += ['--pedantic']
284+
out = run_command(cmd)
285+
with open(traps_expected_file, 'w') as o: o.write(out)
286+
284287
if has_shell_timeout():
285288
print '\n[ checking wasm-reduce ]\n'
286289

src/passes/CMakeLists.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ SET(passes_SOURCES
77
DeadCodeElimination.cpp
88
DuplicateFunctionElimination.cpp
99
ExtractFunction.cpp
10-
FlattenControlFlow.cpp
10+
Flatten.cpp
1111
Inlining.cpp
1212
LegalizeJSInterface.cpp
1313
LocalCSE.cpp

src/passes/Flatten.cpp

Lines changed: 337 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,337 @@
1+
/*
2+
* Copyright 2017 WebAssembly Community Group participants
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
//
18+
// Flattens code, removing nesting.e.g. an if return value would be
19+
// converted to a local
20+
//
21+
// (i32.add
22+
// (if (..condition..)
23+
// (..if true..)
24+
// (..if false..)
25+
// )
26+
// (i32.const 1)
27+
// )
28+
// =>
29+
// (if (..condition..)
30+
// (set_local $temp
31+
// (..if true..)
32+
// )
33+
// (set_local $temp
34+
// (..if false..)
35+
// )
36+
// )
37+
// (i32.add
38+
// (get_local $temp)
39+
// (i32.const 1)
40+
// )
41+
//
42+
// Formally, this pass flattens in the precise sense of
43+
// making the AST have these properties:
44+
//
45+
// 1. The operands of an instruction must be a get_local or a const.
46+
// anything else is written to a local earlier.
47+
// 2. Disallow block, loop, and if return values, i.e., do not use
48+
// control flow to pass around values.
49+
//
50+
51+
#include <wasm.h>
52+
#include <pass.h>
53+
#include <wasm-builder.h>
54+
#include <ast_utils.h>
55+
#include <ast/effects.h>
56+
57+
namespace wasm {
58+
59+
// We use the following algorithm: we maintain a list of "preludes", code
60+
// that runs right before an expression. When we visit an expression we
61+
// must handle it and its preludes. If the expression has side effects,
62+
// we reduce it to a get_local and add a prelude for that. We then handle
63+
// the preludes, by moving them to the parent or handling them directly.
64+
// we can move them to the parent if the parent is not a control flow
65+
// structure. Otherwise, if the parent is a control flow structure, it
66+
// will incorporate the preludes of its children accordingly.
67+
// As a result, when we reach a node, we know its children have no
68+
// side effects (they have been moved to a prelude), or we are a
69+
// control flow structure (which allows children with side effects,
70+
// e.g. a return as a block element).
71+
// Once exception is that we allow an (unreachable) node, which is used
72+
// when we move something unreachable to another place, and need a
73+
// placeholder. We will never reach that (unreachable) anyhow
74+
struct Flatten : public WalkerPass<ExpressionStackWalker<Flatten, UnifiedExpressionVisitor<Flatten>>> {
75+
bool isFunctionParallel() override { return true; }
76+
77+
Pass* create() override { return new Flatten; }
78+
79+
// For each expression, a bunch of expressions that should execute right before it
80+
std::unordered_map<Expression*, std::vector<Expression*>> preludes;
81+
82+
// Break values are sent through a temp local
83+
std::unordered_map<Name, Index> breakTemps;
84+
85+
void visitExpression(Expression* curr) {
86+
std::vector<Expression*> ourPreludes;
87+
Builder builder(*getModule());
88+
89+
if (isControlFlowStructure(curr)) {
90+
// handle control flow explicitly. our children do not have control flow,
91+
// but they do have preludes which we need to set up in the right place
92+
assert(preludes.find(curr) == preludes.end()); // no one should have given us preludes, they are on the children
93+
if (auto* block = curr->dynCast<Block>()) {
94+
// make a new list, where each item's preludes are added before it
95+
ExpressionList newList(getModule()->allocator);
96+
for (auto* item : block->list) {
97+
auto iter = preludes.find(item);
98+
if (iter != preludes.end()) {
99+
auto& itemPreludes = iter->second;
100+
for (auto* prelude : itemPreludes) {
101+
newList.push_back(prelude);
102+
}
103+
itemPreludes.clear();
104+
}
105+
newList.push_back(item);
106+
}
107+
block->list.swap(newList);
108+
// remove a block return value
109+
auto type = block->type;
110+
if (isConcreteWasmType(type)) {
111+
// if there is a temp index for breaking to the block, use that
112+
Index temp;
113+
auto iter = breakTemps.find(block->name);
114+
if (iter != breakTemps.end()) {
115+
temp = iter->second;
116+
} else {
117+
temp = builder.addVar(getFunction(), type);
118+
}
119+
auto*& last = block->list.back();
120+
if (isConcreteWasmType(last->type)) {
121+
last = builder.makeSetLocal(temp, last);
122+
}
123+
block->finalize(none);
124+
// and we leave just a get of the value
125+
auto* rep = builder.makeGetLocal(temp, type);
126+
replaceCurrent(rep);
127+
// the whole block is now a prelude
128+
ourPreludes.push_back(block);
129+
}
130+
// the block now has no return value, and may have become unreachable
131+
block->finalize(none);
132+
} else if (auto* iff = curr->dynCast<If>()) {
133+
// condition preludes go before the entire if
134+
auto* rep = getPreludesWithExpression(iff->condition, iff);
135+
// arm preludes go in the arms. we must also remove an if value
136+
auto* originalIfTrue = iff->ifTrue;
137+
auto* originalIfFalse = iff->ifFalse;
138+
auto type = iff->type;
139+
Expression* prelude = nullptr;
140+
if (isConcreteWasmType(type)) {
141+
Index temp = builder.addVar(getFunction(), type);
142+
if (isConcreteWasmType(iff->ifTrue->type)) {
143+
iff->ifTrue = builder.makeSetLocal(temp, iff->ifTrue);
144+
}
145+
if (iff->ifFalse && isConcreteWasmType(iff->ifFalse->type)) {
146+
iff->ifFalse = builder.makeSetLocal(temp, iff->ifFalse);
147+
}
148+
// the whole if (+any preludes from the condition) is now a prelude
149+
prelude = rep;
150+
// and we leave just a get of the value
151+
rep = builder.makeGetLocal(temp, type);
152+
}
153+
iff->ifTrue = getPreludesWithExpression(originalIfTrue, iff->ifTrue);
154+
if (iff->ifFalse) iff->ifFalse = getPreludesWithExpression(originalIfFalse, iff->ifFalse);
155+
iff->finalize();
156+
if (prelude) {
157+
ReFinalizeNode().visit(prelude);
158+
ourPreludes.push_back(prelude);
159+
}
160+
replaceCurrent(rep);
161+
} else if (auto* loop = curr->dynCast<Loop>()) {
162+
// remove a loop value
163+
Expression* rep = loop;
164+
auto* originalBody = loop->body;
165+
auto type = loop->type;
166+
if (isConcreteWasmType(type)) {
167+
Index temp = builder.addVar(getFunction(), type);
168+
loop->body = builder.makeSetLocal(temp, loop->body);
169+
// and we leave just a get of the value
170+
rep = builder.makeGetLocal(temp, type);
171+
// the whole if is now a prelude
172+
ourPreludes.push_back(loop);
173+
loop->type = none;
174+
}
175+
loop->body = getPreludesWithExpression(originalBody, loop->body);
176+
loop->finalize();
177+
replaceCurrent(rep);
178+
} else {
179+
WASM_UNREACHABLE();
180+
}
181+
} else {
182+
// for anything else, there may be existing preludes
183+
auto iter = preludes.find(curr);
184+
if (iter != preludes.end()) {
185+
ourPreludes.swap(iter->second);
186+
}
187+
// special handling
188+
if (auto* br = curr->dynCast<Break>()) {
189+
if (br->value) {
190+
auto type = br->value->type;
191+
if (isConcreteWasmType(type)) {
192+
// we are sending a value. use a local instead
193+
Index temp = getTempForBreakTarget(br->name, type);
194+
ourPreludes.push_back(builder.makeSetLocal(temp, br->value));
195+
if (br->condition) {
196+
// the value must also flow out
197+
ourPreludes.push_back(br);
198+
if (isConcreteWasmType(br->type)) {
199+
replaceCurrent(builder.makeGetLocal(temp, type));
200+
} else {
201+
assert(br->type == unreachable);
202+
replaceCurrent(builder.makeUnreachable());
203+
}
204+
}
205+
br->value = nullptr;
206+
br->finalize();
207+
} else {
208+
assert(type == unreachable);
209+
// we don't need the br at all
210+
replaceCurrent(br->value);
211+
}
212+
}
213+
} else if (auto* sw = curr->dynCast<Switch>()) {
214+
if (sw->value) {
215+
auto type = sw->value->type;
216+
if (isConcreteWasmType(type)) {
217+
// we are sending a value. use a local instead
218+
Index temp = builder.addVar(getFunction(), type);
219+
ourPreludes.push_back(builder.makeSetLocal(temp, sw->value));
220+
// we don't know which break target will be hit - assign to them all
221+
std::set<Name> names;
222+
for (auto target : sw->targets) {
223+
names.insert(target);
224+
}
225+
names.insert(sw->default_);
226+
for (auto name : names) {
227+
ourPreludes.push_back(builder.makeSetLocal(
228+
getTempForBreakTarget(name, type),
229+
builder.makeGetLocal(temp, type)
230+
));
231+
}
232+
sw->value = nullptr;
233+
sw->finalize();
234+
} else {
235+
assert(type == unreachable);
236+
// we don't need the br at all
237+
replaceCurrent(sw->value);
238+
}
239+
}
240+
}
241+
}
242+
// continue for general handling of everything, control flow or otherwise
243+
curr = getCurrent(); // we may have replaced it
244+
// we have changed children
245+
ReFinalizeNode().visit(curr);
246+
// handle side effects and control flow, things we need to be
247+
// in the prelude. note that we must handle anything here, not just
248+
// side effects, as a sibling after us may have side effect for us,
249+
// and thus we need to move in anticipation of that (e.g., we are
250+
// a get, and a later sibling is a tee - if just the tee moves,
251+
// that is bade) TODO optimize
252+
if (isControlFlowStructure(curr) || EffectAnalyzer(getPassOptions(), curr).hasAnything()) {
253+
// we need to move the side effects to the prelude
254+
if (curr->type == unreachable) {
255+
ourPreludes.push_back(curr);
256+
replaceCurrent(builder.makeUnreachable());
257+
} else if (curr->type == none) {
258+
if (!curr->is<Nop>()) {
259+
ourPreludes.push_back(curr);
260+
replaceCurrent(builder.makeNop());
261+
}
262+
} else {
263+
// use a local
264+
auto type = curr->type;
265+
Index temp = builder.addVar(getFunction(), type);
266+
ourPreludes.push_back(builder.makeSetLocal(temp, curr));
267+
replaceCurrent(builder.makeGetLocal(temp, type));
268+
}
269+
}
270+
// next, finish up: migrate our preludes if we can
271+
if (!ourPreludes.empty()) {
272+
auto* parent = getParent();
273+
if (parent && !isControlFlowStructure(parent)) {
274+
auto& parentPreludes = preludes[parent];
275+
for (auto* prelude : ourPreludes) {
276+
parentPreludes.push_back(prelude);
277+
}
278+
} else {
279+
// keep our preludes, parent will handle them
280+
preludes[getCurrent()].swap(ourPreludes);
281+
}
282+
}
283+
}
284+
285+
void visitFunction(Function* curr) {
286+
auto* originalBody = curr->body;
287+
// if the body is a block with a result, turn that into a return
288+
if (isConcreteWasmType(curr->body->type)) {
289+
curr->body = Builder(*getModule()).makeReturn(curr->body);
290+
}
291+
// the body may have preludes
292+
curr->body = getPreludesWithExpression(originalBody, curr->body);
293+
}
294+
295+
private:
296+
bool isControlFlowStructure(Expression* curr) {
297+
return curr->is<Block>() || curr->is<If>() || curr->is<Loop>();
298+
}
299+
300+
// gets an expression, either by itself, or in a block with its
301+
// preludes (which we use up) before it
302+
Expression* getPreludesWithExpression(Expression* curr) {
303+
return getPreludesWithExpression(curr, curr);
304+
}
305+
306+
// gets an expression, either by itself, or in a block with some
307+
// preludes (which we use up) for another expression before it
308+
Expression* getPreludesWithExpression(Expression* preluder, Expression* after) {
309+
auto iter = preludes.find(preluder);
310+
if (iter == preludes.end()) return after;
311+
// we have preludes
312+
auto& thePreludes = iter->second;
313+
auto* ret = Builder(*getModule()).makeBlock(thePreludes);
314+
thePreludes.clear();
315+
ret->list.push_back(after);
316+
ret->finalize();
317+
return ret;
318+
}
319+
320+
// get the temp local to be used for breaks to that target. allocates
321+
// one if there isn't one yet
322+
Index getTempForBreakTarget(Name name, WasmType type) {
323+
auto iter = breakTemps.find(name);
324+
if (iter != breakTemps.end()) {
325+
return iter->second;
326+
} else {
327+
return breakTemps[name] = Builder(*getModule()).addVar(getFunction(), type);
328+
}
329+
}
330+
};
331+
332+
Pass *createFlattenPass() {
333+
return new Flatten();
334+
}
335+
336+
} // namespace wasm
337+

0 commit comments

Comments
 (0)