Skip to content

Commit 5dae9f4

Browse files
authored
[Stack Switching] Implement resume_throw in the interpreter (#7815)
Pretty straightforward: Add an exception tag to ContData, and throw if it is set. With this, the next big spec test passes. Throwing from the first resumption is not in the spec test, so add that manually (for us, but I guess not the spec interpreter, this checks another code path). This uncovered an existing bug with StackValueNoter. We added one of these during Resume, to ensure a stack value noting scope always existed. But we track those by ModuleRunner, so it doesn't help when we do a cross-module suspend. Just remove that extra logic, and handle the lack of a scope directly, which makes this all a lot simpler.
1 parent 9fe0d16 commit 5dae9f4

File tree

3 files changed

+144
-23
lines changed

3 files changed

+144
-23
lines changed

src/wasm-interpreter.h

Lines changed: 37 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -226,6 +226,9 @@ struct ContData {
226226
// suspend).
227227
Literals resumeArguments;
228228

229+
// If set, this is the exception to be thrown at the resume point.
230+
Tag* exceptionTag = nullptr;
231+
229232
// Whether we executed. Continuations are one-shot, so they may not be
230233
// executed a second time.
231234
bool executed = false;
@@ -528,9 +531,8 @@ class ExpressionRunner : public OverriddenVisitor<SubType, Flow> {
528531
}
529532
if (!hasValue) {
530533
// We must execute this instruction. Set up the logic to note the values
531-
// of children (we mainly need this for non-control flow structures,
532-
// but even control flow ones must add a scope on the value stack, to
533-
// not confuse the others).
534+
// of children. TODO: as an optimization, we could avoid this for
535+
// control flow structures, at the cost of more complexity
534536
StackValueNoter noter(this);
535537

536538
if (Properties::isControlFlowStructure(curr)) {
@@ -592,12 +594,16 @@ class ExpressionRunner : public OverriddenVisitor<SubType, Flow> {
592594
// values on the stack, not values sent on a break/suspend; suspending is
593595
// handled above).
594596
if (!ret.breaking() && ret.getType().isConcrete()) {
595-
assert(!valueStack.empty());
596-
auto& values = valueStack.back();
597-
values.push_back(ret.values);
597+
// The value stack may be empty, if we lack a parent that needs our
598+
// value. That is the case when we are the toplevel expression, etc.
599+
if (!valueStack.empty()) {
600+
auto& values = valueStack.back();
601+
values.push_back(ret.values);
598602
#if WASM_INTERPRETER_DEBUG
599-
std::cout << indent() << "added to valueStack: " << ret.values << '\n';
603+
std::cout << indent() << "added to valueStack: " << ret.values
604+
<< '\n';
600605
#endif
606+
}
601607
}
602608
}
603609

@@ -4863,6 +4869,12 @@ class ModuleRunnerBase : public ExpressionRunner<SubType> {
48634869
// restoredValues map.
48644870
assert(currContinuation->resumeInfo.empty());
48654871
assert(self()->restoredValuesMap.empty());
4872+
// Throw, if we were resumed by resume_throw;
4873+
if (auto* tag = currContinuation->exceptionTag) {
4874+
// XXX tag->name lacks cross-module support
4875+
throwException(WasmException{
4876+
self()->makeExnData(tag->name, currContinuation->resumeArguments)});
4877+
}
48664878
return currContinuation->resumeArguments;
48674879
}
48684880

@@ -4895,7 +4907,7 @@ class ModuleRunnerBase : public ExpressionRunner<SubType> {
48954907
new_->resumeExpr = curr;
48964908
return Flow(SUSPEND_FLOW, tag, std::move(arguments));
48974909
}
4898-
Flow visitResume(Resume* curr) {
4910+
template<typename T> Flow doResume(T* curr, Tag* exceptionTag = nullptr) {
48994911
Literals arguments;
49004912
Flow flow = self()->generateArguments(curr->operands, arguments);
49014913
if (flow.breaking()) {
@@ -4923,6 +4935,7 @@ class ModuleRunnerBase : public ExpressionRunner<SubType> {
49234935
// the immediate ones here. TODO
49244936
contData->resumeArguments = arguments;
49254937
}
4938+
contData->exceptionTag = exceptionTag;
49264939
self()->pushCurrContinuation(contData);
49274940
self()->continuationStore->resuming = true;
49284941
#if WASM_INTERPRETER_DEBUG
@@ -4931,15 +4944,8 @@ class ModuleRunnerBase : public ExpressionRunner<SubType> {
49314944
#endif
49324945
}
49334946

4934-
Flow ret;
4935-
{
4936-
// Create a stack value scope. This ensures that we always have a scope,
4937-
// and so the code that pushes/pops doesn't need to check if a scope
4938-
// exists. (We do not need the values in this scope, of course, as no
4939-
// expression is above them, so we cannot suspend and need these values).
4940-
typename ExpressionRunner<SubType>::StackValueNoter noter(this);
4941-
ret = func.getFuncData()->doCall(arguments);
4942-
}
4947+
Flow ret = func.getFuncData()->doCall(arguments);
4948+
49434949
#if WASM_INTERPRETER_DEBUG
49444950
if (!self()->isResuming()) {
49454951
std::cout << self()->indent() << "finished resuming, with " << ret
@@ -4995,7 +5001,11 @@ class ModuleRunnerBase : public ExpressionRunner<SubType> {
49955001
// No suspension; all done.
49965002
return ret;
49975003
}
4998-
Flow visitResumeThrow(ResumeThrow* curr) { return Flow(NONCONSTANT_FLOW); }
5004+
Flow visitResume(Resume* curr) { return doResume(curr); }
5005+
Flow visitResumeThrow(ResumeThrow* curr) {
5006+
// TODO: should the Resume and ResumeThrow classes be merged?
5007+
return doResume(curr, self()->getModule()->getTag(curr->tag));
5008+
}
49995009
Flow visitStackSwitch(StackSwitch* curr) { return Flow(NONCONSTANT_FLOW); }
50005010

50015011
void trap(const char* why) override {
@@ -5058,14 +5068,20 @@ class ModuleRunnerBase : public ExpressionRunner<SubType> {
50585068

50595069
if (self()->isResuming()) {
50605070
// The arguments are in the continuation data.
5061-
arguments = self()->getCurrContinuation()->resumeArguments;
5071+
auto currContinuation = self()->getCurrContinuation();
5072+
arguments = currContinuation->resumeArguments;
50625073

5063-
if (!self()->getCurrContinuation()->resumeExpr) {
5074+
if (!currContinuation->resumeExpr) {
50645075
// This is the first time we resume, that is, there is no suspend which
50655076
// is the resume expression that we need to execute up to. All we need
50665077
// to do is just start calling this function (with the arguments we've
5067-
// set), so resuming is done
5078+
// set), so resuming is done. (And throw, if resume_throw.)
50685079
self()->continuationStore->resuming = false;
5080+
if (auto* tag = currContinuation->exceptionTag) {
5081+
// XXX tag->name lacks cross-module support
5082+
throwException(WasmException{
5083+
self()->makeExnData(tag->name, currContinuation->resumeArguments)});
5084+
}
50695085
}
50705086
}
50715087

test/lit/exec/cont_simple.wast

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -844,4 +844,24 @@
844844
)
845845
)
846846
)
847+
848+
(func $never
849+
;; This will be resume_throw'd on the first execution, so this code is never
850+
;; reached.
851+
(call $log (i32.const 1337))
852+
)
853+
854+
;; CHECK: [fuzz-exec] calling resume_throw-never
855+
;; CHECK-NEXT: [LoggingExternalInterface logging 42]
856+
(func $resume_throw-never (export "resume_throw-never")
857+
(block $more
858+
(try_table (catch $more $more)
859+
(resume_throw $k $more
860+
(cont.new $k (ref.func $never))
861+
)
862+
)
863+
)
864+
;; This will be reached.
865+
(call $log (i32.const 42))
866+
)
847867
)

test/spec/cont.wast

Lines changed: 87 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -124,8 +124,8 @@
124124
(assert_return (invoke "handled"))
125125

126126
(assert_exception (invoke "uncaught-1"))
127-
;; TODO: resume_throw (assert_exception (invoke "uncaught-2"))
128-
;; TODO: resume_throw (assert_exception (invoke "uncaught-3"))
127+
(assert_exception (invoke "uncaught-2"))
128+
(assert_exception (invoke "uncaught-3"))
129129

130130
(assert_trap (invoke "non-linear-1") "continuation already consumed")
131131
(assert_trap (invoke "non-linear-2") "continuation already consumed")
@@ -641,3 +641,88 @@
641641
(unreachable))
642642
)
643643

644+
(module $co2
645+
(type $task (func (result i32))) ;; type alias task = [] -> []
646+
(type $ct (cont $task)) ;; type alias ct = $task
647+
(tag $pause (export "pause")) ;; pause : [] -> []
648+
(tag $cancel (export "cancel")) ;; cancel : [] -> []
649+
;; run : [(ref $task) (ref $task)] -> []
650+
;; implements a 'seesaw' (c.f. Ganz et al. (ICFP@99))
651+
(func $run (export "seesaw") (param $up (ref $ct)) (param $down (ref $ct)) (result i32)
652+
(local $result i32)
653+
;; run $up
654+
(loop $run_next (result i32)
655+
(block $on_pause (result (ref $ct))
656+
(resume $ct (on $pause $on_pause)
657+
(local.get $up))
658+
;; $up finished, store its result
659+
(local.set $result)
660+
;; next cancel $down
661+
(block $on_cancel
662+
(try_table (catch $cancel $on_cancel)
663+
;; inject the cancel exception into $down
664+
(resume_throw $ct $cancel (local.get $down))
665+
(drop) ;; drop the return value if it handled $cancel
666+
;; itself and returned normally...
667+
)
668+
) ;; ... otherwise catch $cancel and return $up's result.
669+
(return (local.get $result))
670+
) ;; on_pause clause, stack type: [(cont $ct)]
671+
(local.set $up)
672+
;; swap $up and $down
673+
(local.get $down)
674+
(local.set $down (local.get $up))
675+
(local.set $up)
676+
(br $run_next)
677+
)
678+
)
679+
)
680+
(register "co2")
681+
682+
(module $client
683+
(type $task-0 (func (param i32) (result i32)))
684+
(type $ct-0 (cont $task-0))
685+
(type $task (func (result i32)))
686+
(type $ct (cont $task))
687+
688+
(func $seesaw (import "co2" "seesaw") (param (ref $ct)) (param (ref $ct)) (result i32))
689+
(func $print-i32 (import "spectest" "print_i32") (param i32))
690+
(tag $pause (import "co2" "pause"))
691+
692+
(func $even (param $niter i32) (result i32)
693+
(local $next i32) ;; zero initialised.
694+
(local $i i32)
695+
(loop $print-next
696+
(call $print-i32 (local.get $next))
697+
(suspend $pause)
698+
(local.set $next (i32.add (local.get $next) (i32.const 2)))
699+
(local.set $i (i32.add (local.get $i) (i32.const 1)))
700+
(br_if $print-next (i32.lt_u (local.get $i) (local.get $niter)))
701+
)
702+
(local.get $next)
703+
)
704+
(func $odd (param $niter i32) (result i32)
705+
(local $next i32) ;; zero initialised.
706+
(local $i i32)
707+
(local.set $next (i32.const 1))
708+
(loop $print-next
709+
(call $print-i32 (local.get $next))
710+
(suspend $pause)
711+
(local.set $next (i32.add (local.get $next) (i32.const 2)))
712+
(local.set $i (i32.add (local.get $i) (i32.const 1)))
713+
(br_if $print-next (i32.lt_u (local.get $i) (local.get $niter)))
714+
)
715+
(local.get $next)
716+
)
717+
718+
(func (export "main") (result i32)
719+
(call $seesaw
720+
(cont.bind $ct-0 $ct
721+
(i32.const 5) (cont.new $ct-0 (ref.func $even)))
722+
(cont.bind $ct-0 $ct
723+
(i32.const 5) (cont.new $ct-0 (ref.func $odd)))))
724+
725+
(elem declare func $even $odd)
726+
)
727+
(assert_return (invoke "main") (i32.const 10))
728+

0 commit comments

Comments
 (0)