Skip to content

Commit c0d13e2

Browse files
committed
Fix and improve unreachable parsing
The recent change (#8608) that reduced the number of scratch locals introduced by IRBuilder also inadvertently introduced a bug. That commit changed the conditions under which we package multiple expressions into a block to be popped together. We previously did this whenever there was a get of a scratch local to hoist a value to the top of the stack. After #8608, we started using blocks whenever there were multiple expressions hoisted. These conditions are usually identical, but we missed the case where there are multiple expressions and also no `get` because the value is unreachable. The introduction of a block where there was none before invalidated our logic for determining whether or not to pop back past an unreachable expression based on whether the expressions underneath it would satisfy the type constraints of the parent. Simplify that logic by calculating the stack size at which we must avoid popping past an unreachable rather than calculating the index of the unreachable itself. This makes the logic work correctly whether or not the unreachable will be popped as part of a block. That in turn lets us remove an extra unreachable we were conservatively pushing onto the stack when "hoisting" unreachable values. Add tests for the cases where we can and cannot pop past an unreachable that is followed by a none-typed expression. The former case previously parsed incorrectly and the latter case is now parsed without introducing extra unreachable instructions.
1 parent 1744c8e commit c0d13e2

File tree

2 files changed

+103
-32
lines changed

2 files changed

+103
-32
lines changed

src/wasm/wasm-ir-builder.cpp

Lines changed: 55 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -87,10 +87,7 @@ MaybeResult<IRBuilder::HoistedVal> IRBuilder::hoistLastValue(bool greedy) {
8787
}
8888
auto*& expr = stack[valIndex];
8989
if (expr->type == Type::unreachable) {
90-
// Make sure the top of the stack also has an unreachable expression.
91-
if (stack.back()->type != Type::unreachable) {
92-
pushSynthetic(builder.makeUnreachable());
93-
}
90+
// No need for a scratch local to hoist an unreachable.
9491
return HoistedVal{Index(hoistIndex), nullptr};
9592
}
9693
// Hoist with a scratch local. Normally the scratch local is the same type as
@@ -129,6 +126,11 @@ Result<> IRBuilder::packageHoistedValue(const HoistedVal& hoisted,
129126
};
130127

131128
auto type = scope.exprStack.back()->type;
129+
if (type == Type::none) {
130+
// If we did not have a value on top of the stack and did not add a scratch
131+
// local, then there must have been an unreachable.
132+
type = Type::unreachable;
133+
}
132134

133135
if (type.size() == sizeHint || type.size() <= 1) {
134136
if (hoisted.hoistIndex < scope.exprStack.size() - 1) {
@@ -280,6 +282,8 @@ void IRBuilder::dump() {
280282
if (tryy->name) {
281283
std::cerr << " " << tryy->name;
282284
}
285+
} else if (scope.getTryTable()) {
286+
std::cerr << "try_table";
283287
} else {
284288
WASM_UNREACHABLE("unexpected scope");
285289
}
@@ -303,7 +307,8 @@ void IRBuilder::dump() {
303307
std::cerr << ":\n";
304308

305309
for (auto* expr : scope.exprStack) {
306-
std::cerr << " " << ShallowExpression{expr} << "\n";
310+
std::cerr << " " << ShallowExpression{expr} << " (; "
311+
<< expr->type.toString() << " ;)\n";
307312
}
308313
}
309314
#endif // IR_BUILDER_DEBUG
@@ -356,32 +361,26 @@ struct IRBuilder::ChildPopper
356361
Result<> popConstrainedChildren(std::vector<Child>& children) {
357362
auto& scope = builder.getScope();
358363

359-
// The index of the shallowest unreachable instruction on the stack, found
360-
// by checkNeedsUnreachableFallback.
361-
std::optional<size_t> unreachableIndex;
362-
363-
// Whether popping the children past the unreachable would produce a type
364-
// mismatch or try to pop from an empty stack.
365-
bool needUnreachableFallback = false;
364+
// The stack size at which we are about to pop an unreachable instruction
365+
// that we must not pop past because the expressions underneath it do not
366+
// have the types we need.
367+
std::optional<size_t> unreachableFallbackSize;
366368

367369
// We only need to check requirements if there is an unreachable.
368370
// Otherwise the validator will catch any problems.
369371
if (scope.unreachable) {
370-
needUnreachableFallback =
371-
checkNeedsUnreachableFallback(children, unreachableIndex);
372+
unreachableFallbackSize = checkNeedsUnreachableFallback(children);
372373
}
373374

374375
// We have checked all the constraints, so we are ready to pop children.
375376
for (int i = children.size() - 1; i >= 0; --i) {
376-
if (needUnreachableFallback &&
377-
scope.exprStack.size() == *unreachableIndex + 1 && i > 0) {
377+
if (unreachableFallbackSize &&
378+
scope.exprStack.size() == *unreachableFallbackSize && i > 0) {
378379
// The next item on the stack is the unreachable instruction we must
379-
// not pop past. We cannot insert unreachables in front of it because
380-
// it might be a branch we actually have to execute, so this next item
381-
// must be child 0. But we are not ready to pop child 0 yet, so
382-
// synthesize an unreachable instead of popping. The deeper
383-
// instructions that would otherwise have been popped will remain on
384-
// the stack to become prior children of future expressions or to be
380+
// not pop past. We could pop it as child 0, but we are not ready to pop
381+
// child 0 yet, so synthesize an unreachable instead of popping. The
382+
// deeper instructions that would otherwise have been popped will remain
383+
// on the stack to become prior children of future expressions or to be
385384
// implicitly dropped at the end of the scope.
386385
*children[i].childp = builder.builder.makeUnreachable();
387386
continue;
@@ -397,9 +396,10 @@ struct IRBuilder::ChildPopper
397396
return Ok{};
398397
}
399398

400-
bool checkNeedsUnreachableFallback(const std::vector<Child>& children,
401-
std::optional<size_t>& unreachableIndex) {
399+
std::optional<size_t>
400+
checkNeedsUnreachableFallback(const std::vector<Child>& children) {
402401
auto& scope = builder.getScope();
402+
std::optional<size_t> unreachableFallbackSize;
403403

404404
// Two-part indices into the stack of available expressions and the vector
405405
// of requirements, allowing them to move independently with the granularity
@@ -409,6 +409,9 @@ struct IRBuilder::ChildPopper
409409
size_t childIndex = children.size();
410410
size_t childTupleIndex = 0;
411411

412+
// Whether we are deeper than some concrete expression.
413+
bool seenConcrete = false;
414+
412415
// Check whether the values on the stack will be able to meet the given
413416
// requirements.
414417
while (true) {
@@ -435,7 +438,7 @@ struct IRBuilder::ChildPopper
435438
// the input unreachable instruction is executed first. If we are
436439
// not reaching past an unreachable, the error will be caught when
437440
// we pop.
438-
return true;
441+
return unreachableFallbackSize;
439442
}
440443
--stackIndex;
441444
stackTupleIndex = scope.exprStack[stackIndex]->type.size() - 1;
@@ -450,22 +453,35 @@ struct IRBuilder::ChildPopper
450453
}
451454

452455
// We have an available type and a constraint. Only check constraints if
453-
// we are past an unreachable, since otherwise we can leave problems to be
454-
// caught by the validator later.
456+
// we are deeper than an unreachable, since otherwise we can leave
457+
// problems to be caught by the validator later.
455458
auto type = scope.exprStack[stackIndex]->type[stackTupleIndex];
456-
if (unreachableIndex) {
459+
if (unreachableFallbackSize) {
457460
auto constraint = children[childIndex].constraint[childTupleIndex];
458461
if (!PrincipalType::matches(type, constraint)) {
459-
return true;
462+
return unreachableFallbackSize;
460463
}
461464
}
462465

463-
// No problems for children after this unreachable.
466+
// We may need an unreachable fallback if we find violated constraints in
467+
// expressions deeper than this unreachable. Calculate the stack size at
468+
// which this unreachable will be the next thing popped. This size is
469+
// usually one more than the current index, but if there are no shallower
470+
// concrete expressions, then everything down to this unreachable will be
471+
// popped at once immediately at the current stack size.
464472
if (type == Type::unreachable) {
465-
unreachableIndex = stackIndex;
473+
unreachableFallbackSize =
474+
seenConcrete ? stackIndex + 1 : scope.exprStack.size();
475+
} else {
476+
// We skipped none-typed expressions and this isn't unreachable, so it
477+
// must be concrete.
478+
assert(type.isConcrete());
479+
seenConcrete = true;
466480
}
467481
}
468-
return false;
482+
483+
// No unreachable fallback necessary.
484+
return std::nullopt;
469485
}
470486

471487
// If `greedy`, then we will pop additional none-typed expressions that come
@@ -848,6 +864,13 @@ Result<Expression*> IRBuilder::finishScope(Block* block) {
848864
return Err{"popping from empty stack"};
849865
}
850866

867+
if (scope.exprStack.back()->type == Type::none) {
868+
// Nothing was hoisted, which means there must have been an unreachable
869+
// buried under none-type expressions. It is not valid to end a concretely
870+
// typed block with none-typed expressions, so add an extra unreachable.
871+
pushSynthetic(builder.makeUnreachable());
872+
}
873+
851874
if (type.isTuple()) {
852875
auto hoistedType = scope.exprStack.back()->type;
853876
if (hoistedType != Type::unreachable &&

test/lit/wat-kitchen-sink.wast

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5260,6 +5260,54 @@
52605260
)
52615261
)
52625262

5263+
;; CHECK: (func $stacky-unreachable-fallback (type $0)
5264+
;; CHECK-NEXT: (drop
5265+
;; CHECK-NEXT: (i32.const 0)
5266+
;; CHECK-NEXT: )
5267+
;; CHECK-NEXT: (block ;; (replaces unreachable StructSet we can't emit)
5268+
;; CHECK-NEXT: (drop
5269+
;; CHECK-NEXT: (block
5270+
;; CHECK-NEXT: (unreachable)
5271+
;; CHECK-NEXT: (nop)
5272+
;; CHECK-NEXT: )
5273+
;; CHECK-NEXT: )
5274+
;; CHECK-NEXT: (drop
5275+
;; CHECK-NEXT: (unreachable)
5276+
;; CHECK-NEXT: )
5277+
;; CHECK-NEXT: (unreachable)
5278+
;; CHECK-NEXT: )
5279+
;; CHECK-NEXT: )
5280+
(func $stacky-unreachable-fallback
5281+
;; i32 cannot be the struct.set's ref child, so we cannot pop past the
5282+
;; unreachable. The unreachable and the following nop are popped together.
5283+
i32.const 0
5284+
unreachable
5285+
nop
5286+
struct.set $pair 0
5287+
)
5288+
5289+
;; CHECK: (func $stacky-unreachable-ok (type $0)
5290+
;; CHECK-NEXT: (struct.set $pair $first
5291+
;; CHECK-NEXT: (struct.new_default $pair)
5292+
;; CHECK-NEXT: (block
5293+
;; CHECK-NEXT: (unreachable)
5294+
;; CHECK-NEXT: (nop)
5295+
;; CHECK-NEXT: )
5296+
;; CHECK-NEXT: )
5297+
;; CHECK-NEXT: )
5298+
(func $stacky-unreachable-ok
5299+
;; Now we can pop past the unreachable. The unreachable and nop are still
5300+
;; popped together.
5301+
struct.new_default $pair
5302+
unreachable
5303+
nop
5304+
struct.set $pair 0
5305+
)
5306+
5307+
;; CHECK: (func $paren-in-string (type $0)
5308+
;; CHECK-NEXT: (drop
5309+
;; CHECK-NEXT: (string.const ")")
5310+
;; CHECK-NEXT: )
52635311
(func $paren-in-string
52645312
;; We should not be tripped up by an extra close parenthesis inside a string.
52655313
(drop

0 commit comments

Comments
 (0)