Skip to content

Commit 05e368e

Browse files
authored
[Interpreter] Implement StackSwitch execution (#9323)
`ModuleRunnerBase::visitStackSwitch` previously returned `Flow(NONCONSTANT_FLOW)`, which caused `ModuleRunnerBase::callFunction` to throw an uncaught `NonconstantException` when executing a `switch` instruction under `--fuzz-exec` or `wasm-shell`. Implement full interpreter support for `switch` instructions and `(on $tag switch)` handlers in `resume` and `resume_throw`, distinguishing label handlers from switch handlers and looping in `doResume` to transfer control directly to the target continuation.
1 parent b517628 commit 05e368e

5 files changed

Lines changed: 331 additions & 66 deletions

File tree

‎scripts/test/fuzzing.py‎

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -80,14 +80,6 @@
8080
# it removes unknown imports
8181
'string-lifting.wast',
8282
'string-lifting-custom-module.wast',
83-
# TODO: fuzzer support for remaining stack switching instructions: switch,
84-
# cont.bind
85-
'cont.wast',
86-
'precompute-stack-switching.wast',
87-
'coalesce-locals-stack-switching.wast',
88-
'stack_switching_switch_2.wast',
89-
'stack_switching_switch.wast',
90-
'unsubtyping-stack-switching.wast',
9183
# TODO: fix split_wast() on tricky escaping situations like a string ending
9284
# in \\" (the " is not escaped - there is an escaped \ before it)
9385
'string-lifting-section.wast',

‎src/wasm-interpreter.h‎

Lines changed: 165 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,8 @@ struct NonconstantException {};
7272

7373
// Utilities
7474

75-
extern Name RETURN_FLOW, RETURN_CALL_FLOW, NONCONSTANT_FLOW, SUSPEND_FLOW;
75+
extern Name RETURN_FLOW, RETURN_CALL_FLOW, NONCONSTANT_FLOW, SUSPEND_FLOW,
76+
STACK_SWITCH_FLOW;
7677

7778
// Stuff that flows around during executing expressions: a literal, or a change
7879
// in control flow.
@@ -88,13 +89,14 @@ class Flow {
8889
: values(std::move(values)), breakTo(breakTo) {}
8990
Flow(Name breakTo, Tag* suspendTag, Literals&& values)
9091
: values(std::move(values)), breakTo(breakTo), suspendTag(suspendTag) {
91-
assert(breakTo == SUSPEND_FLOW);
92+
assert(breakTo == SUSPEND_FLOW || breakTo == STACK_SWITCH_FLOW);
9293
}
9394

9495
Literals values;
9596
Name breakTo; // if non-null, a break is going on
96-
Tag* suspendTag = nullptr; // if non-null, breakTo must be SUSPEND_FLOW, and
97-
// this is the tag being suspended
97+
Tag* suspendTag = nullptr; // if non-null, breakTo must be SUSPEND_FLOW or
98+
// STACK_SWITCH_FLOW, and this is the tag being
99+
// suspended
98100

99101
// A helper function for the common case where there is only one value
100102
const Literal& getSingleValue() {
@@ -411,6 +413,16 @@ class ExpressionRunner : public OverriddenVisitor<SubType, Flow> {
411413
return Literal(allocation);
412414
}
413415

416+
// Same as makeGCData but for ContData.
417+
template<typename... Args>
418+
std::shared_ptr<ContData> makeContData(Args&&... args) {
419+
auto allocation = std::make_shared<ContData>(std::forward<Args>(args)...);
420+
#if __has_feature(leak_sanitizer) || __has_feature(address_sanitizer)
421+
__lsan_ignore_object(allocation.get());
422+
#endif
423+
return allocation;
424+
}
425+
414426
static Literal applyRMW(AtomicRMWOp op, Literal lhs, Literal rhs) {
415427
switch (op) {
416428
case RMWAdd:
@@ -5032,8 +5044,7 @@ class ModuleRunnerBase : public ExpressionRunner<SubType> {
50325044
if (funcValue.isNull()) {
50335045
trap("null ref");
50345046
}
5035-
return Literal(
5036-
std::make_shared<ContData>(funcValue, curr->type.getHeapType()));
5047+
return Literal(self()->makeContData(funcValue, curr->type.getHeapType()));
50375048
}
50385049
Flow visitContBind(ContBind* curr) {
50395050
Literals arguments;
@@ -5045,37 +5056,64 @@ class ModuleRunnerBase : public ExpressionRunner<SubType> {
50455056
trap("null ref");
50465057
}
50475058

5048-
// Create a new continuation, copying the old but with the new type +
5059+
// Create a new continuation, moving the old but with the new type +
50495060
// arguments.
50505061
auto old = contValue.getContData();
50515062
if (old->executed) {
50525063
trap("continuation already executed");
50535064
}
5054-
auto newData = *old;
5065+
auto newData = std::move(*old);
5066+
old->resumeInfo.clear();
5067+
old->resumeArguments.clear();
50555068
newData.type = curr->type.getHeapType();
50565069
for (auto arg : arguments) {
50575070
newData.resumeArguments.push_back(arg);
50585071
}
50595072
// The old one is done.
50605073
old->executed = true;
5061-
return Literal(std::make_shared<ContData>(newData));
5074+
return Literal(self()->makeContData(std::move(newData)));
50625075
}
50635076

50645077
void maybeThrowAfterResuming(std::shared_ptr<ContData>& currContinuation) {
50655078
// We may throw by creating a tag, or an exnref.
50665079
auto* tag = currContinuation->exceptionTag;
5067-
auto exnref = currContinuation->exception.type != Type::none;
5080+
currContinuation->exceptionTag = nullptr;
5081+
auto exn = currContinuation->exception;
5082+
currContinuation->exception = Literal();
5083+
auto exnref = exn.type != Type::none;
50685084
assert(!(tag && exnref));
50695085
if (tag) {
50705086
// resume_throw
5071-
throwException(WasmException{
5072-
self()->makeExnData(tag, currContinuation->resumeArguments)});
5087+
auto resumeArguments = std::move(currContinuation->resumeArguments);
5088+
currContinuation->resumeArguments.clear();
5089+
throwException(WasmException{self()->makeExnData(tag, resumeArguments)});
50735090
} else if (exnref) {
50745091
// resume_throw_ref
5075-
throwException(WasmException{currContinuation->exception});
5092+
throwException(WasmException{exn});
50765093
}
50775094
}
50785095

5096+
Flow finishResumingAt(Expression* curr) {
5097+
#if WASM_INTERPRETER_DEBUG
5098+
std::cout << self()->indent()
5099+
<< "returned to suspend; continuing normally\n";
5100+
#endif
5101+
// This is a resume, so we have found our way back to where we
5102+
// suspended.
5103+
auto currContinuation = self()->getCurrContinuation();
5104+
assert(curr == currContinuation->resumeExpr);
5105+
// We finished resuming, and will continue from here normally.
5106+
self()->continuationStore->resuming = false;
5107+
// We should have consumed all the resumeInfo and all the
5108+
// restoredValues map.
5109+
assert(currContinuation->resumeInfo.empty());
5110+
assert(self()->restoredValuesMap.empty());
5111+
maybeThrowAfterResuming(currContinuation);
5112+
auto resumeArguments = std::move(currContinuation->resumeArguments);
5113+
currContinuation->resumeArguments.clear();
5114+
return resumeArguments;
5115+
}
5116+
50795117
Flow visitSuspend(Suspend* curr) {
50805118
// Process the arguments, whether or not we are resuming. If we are resuming
50815119
// then we don't need these values (we sent them as part of the suspension),
@@ -5084,22 +5122,7 @@ class ModuleRunnerBase : public ExpressionRunner<SubType> {
50845122
VISIT_ARGUMENTS(flow, curr->operands, arguments)
50855123

50865124
if (self()->isResuming()) {
5087-
#if WASM_INTERPRETER_DEBUG
5088-
std::cout << self()->indent()
5089-
<< "returned to suspend; continuing normally\n";
5090-
#endif
5091-
// This is a resume, so we have found our way back to where we
5092-
// suspended.
5093-
auto currContinuation = self()->getCurrContinuation();
5094-
assert(curr == currContinuation->resumeExpr);
5095-
// We finished resuming, and will continue from here normally.
5096-
self()->continuationStore->resuming = false;
5097-
// We should have consumed all the resumeInfo and all the
5098-
// restoredValues map.
5099-
assert(currContinuation->resumeInfo.empty());
5100-
assert(self()->restoredValuesMap.empty());
5101-
maybeThrowAfterResuming(currContinuation);
5102-
return currContinuation->resumeArguments;
5125+
return finishResumingAt(curr);
51035126
}
51045127

51055128
// We were not resuming, so this is a new suspend that we must execute.
@@ -5120,7 +5143,7 @@ class ModuleRunnerBase : public ExpressionRunner<SubType> {
51205143
// Note we cannot update the type yet, so it will be wrong in debug
51215144
// logging. To update it, we must find the block that receives this value,
51225145
// which means we cannot do it here (we don't even know what that block is).
5123-
auto new_ = std::make_shared<ContData>();
5146+
auto new_ = self()->makeContData();
51245147

51255148
// Switch to the new continuation, so that as we unwind, we will save the
51265149
// information we need to resume it later in the proper place.
@@ -5142,7 +5165,6 @@ class ModuleRunnerBase : public ExpressionRunner<SubType> {
51425165
trap("null ref");
51435166
}
51445167
auto contData = cont.getContData();
5145-
auto func = contData->func;
51465168

51475169
// If we are resuming a nested suspend then we should just rewind the call
51485170
// stack, and therefore do not change or test the state here.
@@ -5175,31 +5197,46 @@ class ModuleRunnerBase : public ExpressionRunner<SubType> {
51755197
self()->pushCurrContinuation(contData);
51765198
self()->continuationStore->resuming = true;
51775199
#if WASM_INTERPRETER_DEBUG
5178-
std::cout << self()->indent() << "resuming func " << func.getFunc()
5179-
<< '\n';
5200+
std::cout << self()->indent() << "resuming func "
5201+
<< contData->func.getFunc() << '\n';
51805202
#endif
51815203
}
51825204

5183-
Flow ret = func.getFuncData()->doCall(arguments);
5205+
// Loop to handle (on $tag switch) clauses, which switch directly to a new
5206+
// target continuation under this same resume handler without growing the
5207+
// call stack.
5208+
while (true) {
5209+
Flow ret = contData->func.getFuncData()->doCall(arguments);
51845210

51855211
#if WASM_INTERPRETER_DEBUG
5186-
if (!self()->isResuming()) {
5187-
std::cout << self()->indent() << "finished resuming, with " << ret
5188-
<< '\n';
5189-
}
5212+
if (!self()->isResuming()) {
5213+
std::cout << self()->indent() << "finished resuming, with " << ret
5214+
<< '\n';
5215+
}
51905216
#endif
5191-
if (!ret.suspendTag) {
5192-
// No suspension: the coroutine finished normally. Mark it as no longer
5193-
// active.
5194-
self()->popCurrContinuation();
5195-
} else {
5217+
if (!ret.suspendTag) {
5218+
// No suspension: the coroutine finished normally. Mark it as no longer
5219+
// active.
5220+
self()->popCurrContinuation();
5221+
return ret;
5222+
}
5223+
51965224
// We are suspending. See if a suspension arrived that we support.
5225+
bool switched = false;
51975226
for (size_t i = 0; i < curr->handlerTags.size(); i++) {
51985227
auto* handlerTag = allTags[curr->handlerTags[i]];
5199-
if (handlerTag == ret.suspendTag) {
5228+
if (handlerTag != ret.suspendTag) {
5229+
continue;
5230+
}
5231+
auto handlerBlock = curr->handlerBlocks[i];
5232+
// A non-empty handlerBlock is an (on $tag $label) clause, which only
5233+
// handles suspend instructions (SUSPEND_FLOW). An empty handlerBlock is
5234+
// an (on $tag switch) clause, which only handles switch instructions
5235+
// (STACK_SWITCH_FLOW).
5236+
if (ret.breakTo == SUSPEND_FLOW && handlerBlock.is()) {
52005237
// Switch the flow from suspending to branching.
52015238
ret.suspendTag = nullptr;
5202-
ret.breakTo = curr->handlerBlocks[i];
5239+
ret.breakTo = handlerBlock;
52035240
// We can now update the continuation type, which was wrong until now
52045241
// (see comment in visitSuspend). The type is taken from the block we
52055242
// branch to (which we find in a quite inefficient manner).
@@ -5231,15 +5268,93 @@ class ModuleRunnerBase : public ExpressionRunner<SubType> {
52315268
self()->popCurrContinuation();
52325269
return ret;
52335270
}
5271+
if (ret.breakTo == STACK_SWITCH_FLOW && !handlerBlock.is()) {
5272+
// Finalize the suspended continuation from the switching coroutine
5273+
// (its type was already set in visitStackSwitch).
5274+
auto newCont = self()->getCurrContinuation();
5275+
newCont->func = contData->func;
5276+
self()->popCurrContinuation();
5277+
5278+
// Extract the target continuation that visitStackSwitch appended to
5279+
// the end of the flow's values.
5280+
assert(!ret.values.empty());
5281+
auto targetCont = ret.values.back();
5282+
ret.values.pop_back();
5283+
contData = targetCont.getContData();
5284+
assert(!contData->executed);
5285+
contData->executed = true;
5286+
// Append the forwarded payload arguments and the newly suspended
5287+
// sender continuation to the target continuation's arguments
5288+
// (preserving any arguments bound earlier via cont.bind).
5289+
for (auto arg : ret.values) {
5290+
contData->resumeArguments.push_back(arg);
5291+
}
5292+
contData->resumeArguments.push_back(Literal(newCont));
5293+
arguments = contData->resumeArguments;
5294+
// Switch the active continuation in this prompt to the target
5295+
// continuation and loop to immediately resume it.
5296+
self()->pushCurrContinuation(contData);
5297+
self()->continuationStore->resuming = true;
5298+
switched = true;
5299+
break;
5300+
}
5301+
}
5302+
if (switched) {
5303+
continue;
52345304
}
52355305
// No handler worked out, keep propagating.
5306+
return ret;
52365307
}
5237-
// No suspension; all done.
5238-
return ret;
52395308
}
52405309
Flow visitResume(Resume* curr) { return doResume(curr); }
52415310
Flow visitResumeThrow(ResumeThrow* curr) { return doResume(curr); }
5242-
Flow visitStackSwitch(StackSwitch* curr) { return Flow(NONCONSTANT_FLOW); }
5311+
Flow visitStackSwitch(StackSwitch* curr) {
5312+
// Process the operands and target continuation whether or not we are
5313+
// resuming so that saved child values are consumed when rewinding the
5314+
// stack.
5315+
Literals arguments;
5316+
VISIT_ARGUMENTS(flow, curr->operands, arguments)
5317+
VISIT_REUSE(flow, curr->cont);
5318+
5319+
if (self()->isResuming()) {
5320+
return finishResumingAt(curr);
5321+
}
5322+
5323+
// We are not resuming, so this is a new switch that we must execute.
5324+
auto cont = flow.getSingleValue();
5325+
if (cont.isNull()) {
5326+
trap("null ref");
5327+
}
5328+
auto contData = cont.getContData();
5329+
if (contData->executed) {
5330+
trap("continuation already executed");
5331+
}
5332+
5333+
// Send the target continuation as the final value on the unwinding flow so
5334+
// the matching (on $tag switch) handler in doResume can switch to it.
5335+
arguments.push_back(cont);
5336+
auto old = self()->getCurrContinuationOrNull();
5337+
auto* tag = allTags[curr->tag];
5338+
if (!old) {
5339+
return Flow(STACK_SWITCH_FLOW, tag, std::move(arguments));
5340+
}
5341+
assert(old->executed);
5342+
auto new_ = self()->makeContData();
5343+
// Unlike suspend, where the suspended continuation's type depends on the
5344+
// target block of the handler that catches it, a switch statically knows
5345+
// the type of the suspended continuation it creates: it is the last
5346+
// parameter of the target continuation's signature, which receives this
5347+
// suspended continuation when switched to.
5348+
auto sig =
5349+
curr->cont->type.getHeapType().getContinuation().type.getSignature();
5350+
new_->type = sig.params[sig.params.size() - 1].getHeapType();
5351+
// Replace the current continuation so unwinding saves resume state into the
5352+
// new continuation, and record this switch as the point to resume from.
5353+
self()->popCurrContinuation();
5354+
self()->pushCurrContinuation(new_);
5355+
new_->resumeExpr = curr;
5356+
return Flow(STACK_SWITCH_FLOW, tag, std::move(arguments));
5357+
}
52435358

52445359
void trap(std::string_view why) override {
52455360
// Traps break all current continuations - they will never be resumable.
@@ -5311,6 +5426,7 @@ class ModuleRunnerBase : public ExpressionRunner<SubType> {
53115426
// set), so resuming is done. (And throw, if resume_throw.)
53125427
self()->continuationStore->resuming = false;
53135428
maybeThrowAfterResuming(currContinuation);
5429+
currContinuation->resumeArguments.clear();
53145430
}
53155431
}
53165432

@@ -5440,7 +5556,7 @@ class ModuleRunnerBase : public ExpressionRunner<SubType> {
54405556
flow.breakTo = Name();
54415557
}
54425558

5443-
if (flow.breakTo != SUSPEND_FLOW) {
5559+
if (flow.breakTo != SUSPEND_FLOW && flow.breakTo != STACK_SWITCH_FLOW) {
54445560
// We are normally executing (not suspending), and therefore cannot still
54455561
// be breaking, which would mean we missed our stop.
54465562
assert(!flow.breaking() || flow.breakTo == RETURN_FLOW);

‎src/wasm/wasm.cpp‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ Name RETURN_FLOW("*return:)*");
4343
Name RETURN_CALL_FLOW("*return-call:)*");
4444
Name NONCONSTANT_FLOW("*nonconstant:)*");
4545
Name SUSPEND_FLOW("*suspend:)*");
46+
Name STACK_SWITCH_FLOW("*stack-switch:)*");
4647

4748
namespace BinaryConsts::CustomSections {
4849

0 commit comments

Comments
 (0)