Skip to content

Commit 8d72140

Browse files
authored
[Stack Switching] Handle nested continuations (#7803)
The key fix here is that we don't know what the continuation function (the thing to call, to continue onwards) is, at the time of suspending. Rather, that function is called from a resume, to resume execution from there, so do so when we handle a suspend in a Resume. And, when we resume "through" a suspend (due to a nested suspend), then we should just rewind the call stack. With this, the next big spec testcase passes. Also fix a minor issue with trying to put breaking values on the value stack - only value stack values go there. The new testcase enabled here trips on that. Also improve some debug logging.
1 parent 12f88e3 commit 8d72140

File tree

3 files changed

+121
-29
lines changed

3 files changed

+121
-29
lines changed

src/wasm-interpreter.h

Lines changed: 64 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -230,6 +230,7 @@ struct ContData {
230230
// executed a second time.
231231
bool executed = false;
232232

233+
ContData() {}
233234
ContData(Literal func, HeapType type) : func(func), type(type) {}
234235
};
235236

@@ -338,8 +339,14 @@ class ExpressionRunner : public OverriddenVisitor<SubType, Flow> {
338339

339340
#if WASM_INTERPRETER_DEBUG
340341
std::string indent() {
341-
std::string ret =
342-
'[' + std::to_string(reinterpret_cast<size_t>(this)) + "] ";
342+
std::string id;
343+
if (auto* module = getModule()) {
344+
id = module->name.toString();
345+
}
346+
if (id.empty()) {
347+
id = std::to_string(reinterpret_cast<size_t>(this));
348+
}
349+
auto ret = '[' + id + "] ";
343350
for (Index i = 0; i < depth; i++) {
344351
ret += ' ';
345352
}
@@ -581,9 +588,10 @@ class ExpressionRunner : public OverriddenVisitor<SubType, Flow> {
581588
// Outside the scope of StackValueNoter, the scope of our own child values
582589
// has been removed (we don't need those values any more). What is now on
583590
// the top of |valueStack| is the list of child values of our parent,
584-
// which is the place our own value can go, if we have one (and if we are
585-
// not suspending - suspending is handled above).
586-
if (!ret.suspendTag && ret.getType().isConcrete()) {
591+
// which is the place our own value can go, if we have one (we only save
592+
// values on the stack, not values sent on a break/suspend; suspending is
593+
// handled above).
594+
if (!ret.breaking() && ret.getType().isConcrete()) {
587595
assert(!valueStack.empty());
588596
auto& values = valueStack.back();
589597
values.push_back(ret.values);
@@ -4841,6 +4849,10 @@ class ModuleRunnerBase : public ExpressionRunner<SubType> {
48414849
}
48424850

48434851
if (self()->isResuming()) {
4852+
#if WASM_INTERPRETER_DEBUG
4853+
std::cout << self()->indent()
4854+
<< "returned to suspend; continuing normally\n";
4855+
#endif
48444856
// This is a resume, so we have found our way back to where we
48454857
// suspended.
48464858
auto currContinuation = self()->getCurrContinuation();
@@ -4864,12 +4876,15 @@ class ModuleRunnerBase : public ExpressionRunner<SubType> {
48644876
if (!old) {
48654877
return Flow(SUSPEND_FLOW, tag, std::move(arguments));
48664878
}
4867-
// An old one exists, so we can create a proper new one.
48684879
assert(old->executed);
4869-
auto new_ = std::make_shared<ContData>(old->func, old->type);
4880+
// An old one exists, so we can create a proper new one. It starts out
4881+
// empty here, and as we unwind, info will be added to it (and the function
4882+
// to resume as well, once we find the right resume handler).
4883+
//
48704884
// Note we cannot update the type yet, so it will be wrong in debug
48714885
// logging. To update it, we must find the block that receives this value,
48724886
// which means we cannot do it here (we don't even know what that block is).
4887+
auto new_ = std::make_shared<ContData>();
48734888

48744889
// Switch to the new continuation, so that as we unwind, we will save the
48754890
// information we need to resume it later in the proper place.
@@ -4893,22 +4908,29 @@ class ModuleRunnerBase : public ExpressionRunner<SubType> {
48934908

48944909
// Get and execute the continuation.
48954910
auto contData = flow.getSingleValue().getContData();
4896-
if (contData->executed) {
4897-
trap("continuation already executed");
4898-
}
4899-
contData->executed = true;
4900-
if (contData->resumeArguments.empty()) {
4901-
// The continuation has no bound arguments. For now, we just handle the
4902-
// simple case of binding all of them, so that means we can just use all
4903-
// the immediate ones here. TODO
4904-
contData->resumeArguments = arguments;
4905-
}
49064911
auto func = contData->func;
4907-
self()->pushCurrContinuation(contData);
4908-
self()->continuationStore->resuming = true;
4912+
4913+
// If we are resuming a nested suspend then we should just rewind the call
4914+
// stack, and therefore do not change or test the state here.
4915+
if (!self()->isResuming()) {
4916+
if (contData->executed) {
4917+
trap("continuation already executed");
4918+
}
4919+
contData->executed = true;
4920+
if (contData->resumeArguments.empty()) {
4921+
// The continuation has no bound arguments. For now, we just handle the
4922+
// simple case of binding all of them, so that means we can just use all
4923+
// the immediate ones here. TODO
4924+
contData->resumeArguments = arguments;
4925+
}
4926+
self()->pushCurrContinuation(contData);
4927+
self()->continuationStore->resuming = true;
49094928
#if WASM_INTERPRETER_DEBUG
4910-
std::cout << self()->indent() << "resuming func " << func.getFunc() << '\n';
4929+
std::cout << self()->indent() << "resuming func " << func.getFunc()
4930+
<< '\n';
49114931
#endif
4932+
}
4933+
49124934
Flow ret;
49134935
{
49144936
// Create a stack value scope. This ensures that we always have a scope,
@@ -4919,7 +4941,10 @@ class ModuleRunnerBase : public ExpressionRunner<SubType> {
49194941
ret = func.getFuncData()->doCall(arguments);
49204942
}
49214943
#if WASM_INTERPRETER_DEBUG
4922-
std::cout << self()->indent() << "finished resuming, with " << ret << '\n';
4944+
if (!self()->isResuming()) {
4945+
std::cout << self()->indent() << "finished resuming, with " << ret
4946+
<< '\n';
4947+
}
49234948
#endif
49244949
if (!ret.suspendTag) {
49254950
// No suspension: the coroutine finished normally. Mark it as no longer
@@ -4953,11 +4978,14 @@ class ModuleRunnerBase : public ExpressionRunner<SubType> {
49534978
assert(finder.type.isConcrete());
49544979
assert(finder.type.size() >= 1);
49554980
// The continuation is the final value/type there.
4956-
auto cont = self()->getCurrContinuation();
4957-
cont->type = finder.type[finder.type.size() - 1].getHeapType();
4981+
auto newCont = self()->getCurrContinuation();
4982+
newCont->type = finder.type[finder.type.size() - 1].getHeapType();
4983+
// And we can set the function to be called, to resume it from here
4984+
// (the same function we called, that led to a suspension).
4985+
newCont->func = contData->func;
49584986
// Add the continuation as the final value being sent.
4959-
ret.values.push_back(Literal(cont));
4960-
// We are not longer processing that continuation.
4987+
ret.values.push_back(Literal(newCont));
4988+
// We are no longer processing that continuation.
49614989
self()->popCurrContinuation();
49624990
return ret;
49634991
}
@@ -5067,9 +5095,17 @@ class ModuleRunnerBase : public ExpressionRunner<SubType> {
50675095
for (Index i = 0; i < scope.locals.size(); i++) {
50685096
auto l = scope.locals.size() - 1 - i;
50695097
scope.locals[l] = self()->popResumeEntry("function");
5070-
// Must have restored valid data.
5071-
assert(Type::isSubType(scope.locals[l].getType(),
5072-
function->getLocalType(l)));
5098+
#ifndef NDEBUG
5099+
// Must have restored valid data. The type must match the local's
5100+
// type, except for the case of a non-nullable local that has not yet
5101+
// been accessed: that will contain a null (but the wasm type system
5102+
// ensures it will not be read by code, until a non-null value is
5103+
// assigned).
5104+
auto value = scope.locals[l];
5105+
auto localType = function->getLocalType(l);
5106+
assert(Type::isSubType(value.getType(), localType) ||
5107+
value == Literal::makeZeros(localType));
5108+
#endif
50735109
}
50745110
}
50755111

src/wasm/literal.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -735,7 +735,7 @@ std::ostream& operator<<(std::ostream& o, Literal literal) {
735735
auto data = literal.getContData();
736736
o << "cont(" << data->func << ' ' << data->type;
737737
if (data->resumeExpr) {
738-
o << " resumeExpr=" << getExpressionName(data->resumeExpr);
738+
o << " resumeExpr=" << reinterpret_cast<size_t>(data->resumeExpr);
739739
}
740740
if (!data->resumeInfo.empty()) {
741741
o << " |resumeInfo|=" << data->resumeInfo.size();

test/spec/cont.wast

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -508,3 +508,59 @@
508508
(assert_return (invoke "run" (i32.const 1) (i32.const 1)))
509509
(assert_return (invoke "run" (i32.const 3) (i32.const 4)))
510510

511+
;; Nested example: generator in a thread
512+
513+
(module $concurrent-generator
514+
(func $log (import "spectest" "print_i64") (param i64))
515+
516+
(tag $syield (import "scheduler" "yield"))
517+
(tag $spawn (import "scheduler" "spawn") (param (ref $cont)))
518+
(func $scheduler (import "scheduler" "scheduler") (param $main (ref $cont)))
519+
520+
(type $ghook (func (param i64)))
521+
(func $gsum (import "generator" "sum") (param i64 i64) (result i64))
522+
(global $ghook (import "generator" "hook") (mut (ref $ghook)))
523+
524+
(global $result (mut i64) (i64.const 0))
525+
(global $done (mut i32) (i32.const 0))
526+
527+
(elem declare func $main $bg-thread $syield)
528+
529+
(func $syield (param $i i64)
530+
(call $log (local.get $i))
531+
(suspend $syield)
532+
)
533+
534+
(func $bg-thread
535+
(call $log (i64.const -10))
536+
(loop $l
537+
(call $log (i64.const -11))
538+
(suspend $syield)
539+
(br_if $l (i32.eqz (global.get $done)))
540+
)
541+
(call $log (i64.const -12))
542+
)
543+
544+
(func $main (param $i i64) (param $j i64)
545+
(suspend $spawn (cont.new $cont (ref.func $bg-thread)))
546+
(global.set $ghook (ref.func $syield))
547+
(global.set $result (call $gsum (local.get $i) (local.get $j)))
548+
(global.set $done (i32.const 1))
549+
)
550+
551+
(type $proc (func))
552+
(type $pproc (func (param i64 i64)))
553+
(type $cont (cont $proc))
554+
(type $pcont (cont $pproc))
555+
(func (export "sum") (param $i i64) (param $j i64) (result i64)
556+
(call $log (i64.const -1))
557+
(call $scheduler
558+
(cont.bind $pcont $cont (local.get $i) (local.get $j) (cont.new $pcont (ref.func $main)))
559+
)
560+
(call $log (i64.const -2))
561+
(global.get $result)
562+
)
563+
)
564+
565+
(assert_return (invoke "sum" (i64.const 10) (i64.const 20)) (i64.const 165))
566+

0 commit comments

Comments
 (0)