This proposal adds typed stack-switching to WebAssembly, enabling a single WebAssembly instance to manage multiple execution stacks concurrently. The primary use-case for stack-switching is to add direct support for modular compilation of advanced non-local control flow idioms, e.g. coroutines, async/await, generators, lightweight threads, and so forth. This document outlines the new instructions and validation rules to facilitate stack-switching.
- Motivation
- Continuations
- Introduction to continuation-based stack-switching
- Instruction set extension
- Design considerations
- Specification changes
Non-local control flow features provide the ability to suspend the current execution context and later resume it. Many industrial-strength programming languages feature a wealth of non-local control flow features such as async/await, coroutines, generators/iterators, effect handlers, and so forth. For some programming languages non-local control flow is central to their identity, meaning that they rely on non-local control flow for efficiency, e.g. to support massively scalable concurrency.
Rather than build specific control flow mechanisms for all possible varieties of non-local control flow, our strategy is to build a single mechanism, continuations, that can be used by language providers to construct their own language specific features.
A continuation represents a snapshot of execution on a particular stack. Stack-switching is realised by instructions for suspending and resuming continuations. Continuations are composable, meaning that when a suspended continuation is resumed it is spliced onto the current continuation. This splicing establishes a parent-child relationship between the current and resumed continuation. In this respect the design provides a form of asymmetric switching.
When suspending, we provide a tag and payload, much like when raising an exception. Correspondingly, when resuming a suspended continuation a handler is installed which specifies different behaviours for the different kinds of tag with which the resumed continuation may subsequently be suspended. Unlike for a normal exception handler the handler is passed the suspended continuation as well as a payload.
We also offer an alternative to the interface based on suspending and resuming continuations by way of an instruction for direct switching between continuations. Direct switching combines suspending the current continuation with resuming a previously suspended peer continuation. Direct switching establishes a peer-to-peer relationship between the current continuation and its peer. In this respect the design provides a form of symmetric switching.
We illustrate the proposed stack-switching mechanism using two examples: generators and task scheduling. The generators example uses asymmetric stack-switching and the task scheduling example uses symmetric stack-switching.
The first example illustrates a generator-consumer pattern. Execution switches back and forth between a generator and a consumer execution stack. Whenever execution switches from the generator to the consumer the generator also passes a value to the consumer.
The stack-switching proposal reuses tags from the exception handling proposal. The following tag is used to coordinate between the generator and the consumer.
(tag $gen (param i32))
To switch execution to the consumer, the generator must suspend
execution. The suspend instruction takes a tag, here $gen
. The tag
is used at runtime to determine how to continue execution, by
identifying the active suspend handler for that tag. In our example,
this handler will be provided by the consumer.
Much like with exceptions parameter types specify the types of values
passed from the suspend site to the corresponding handler. In our
example, the tag's single i32
parameter value is the value created
by the generator that is passed to the consumer.
The proposal also extends tags with result types (not used in this example). These allow the types of values passed from a resume site to a previously suspended continuation when resuming it to be specified.
The overall module implementing our example has the following shape.
(module $generator
(type $ft (func))
;; Types of continuations used by the generator:
;; No need for param or result types: No data passed back to the
;; generator when resuming it, and $generator function has no return
;; values.
(type $ct (cont $ft))
;; Tag used to coordinate between generator and consumer: The i32 param
;; corresponds to the generated values passed; no values passed back from
;; generator to consumer.
(tag $gen (param i32))
(func $print (import "spectest" "print_i32") (param i32))
;; Simple generator yielding values from 100 down to 1
(func $generator ...)
(elem declare func $generator)
(func $consumer ...)
)
The module defines continuation type $ct
based on function type
$ft
. Suspended continuations of type (ref $ct)
take no arguments
and return no results. As we shall see, the generator and consumer
manipulate suspended continuations of type (ref $ct)
.
The generator is defined as follows.
;; Simple generator yielding values from 100 down to 1
(func $generator
(local $i i32)
(local.set $i (i32.const 100))
(loop $l
;; Suspend execution, pass current value of $i to consumer
(suspend $gen (local.get $i))
;; Decrement $i and exit loop once $i reaches 0
(local.tee $i (i32.sub (local.get $i) (i32.const 1)))
(br_if $l)
)
)
It executes 100 iterations of a loop and returns afterwards. Execution
is suspended on each iteration using the $gen
tag. The value passed
from the suspend
instruction to the handler (i.e., the value
produced by the generator) is just the current value of the loop
counter.
The consumer is defined as follows.
(func $consumer
(local $c (ref $ct))
;; Create continuation executing function $generator.
;; Execution only starts when resumed for the first time.
(local.set $c (cont.new $ct (ref.func $generator)))
(loop $loop
(block $on_gen (result i32 (ref $ct))
;; Resume continuation $c
(resume $ct (on $gen $on_gen) (local.get $c))
;; $generator returned: no more data
(return)
)
;; Generator suspended, stack now contains [i32 (ref $ct)]
;; Save continuation to resume it in the next iteration
(local.set $c)
;; Stack now contains the i32 value produced by $generator
(call $print)
(br $loop)
)
)
It uses cont.new
to create a continuation executing the
generator. This instruction creates a value of reference type (ref $ct)
, saved in $c
. It then runs a loop, where the resume
instruction is used to continue execution of the continuation
currently saved in $c
on each iteration.
In general, a resume
instruction may not only take a suspended
continuation as an argument, but also additional values to be passed
to the suspended continuation when it is resumed. These are specified
in the parameters of the continuation's type. In our example, $ct
has no parameters, indicating that no data is passed from the consumer
to the generator.
When a suspended continuation is resumed it is spliced onto the current continuation (which may in fact be the top-level continuation corresponding to the main stack). This splicing establishes a parent-child relationship between the current and resumed continuation. This asymmetric relationship affects execution in two ways, which we now discuss.
First, in the resume
instruction the handler clause (on $gen $on_gen)
installs a suspend handler for that tag while executing the
continuation. This means that if during execution of $c
, the
continuation executes the instruction suspend $gen
, execution
continues in the block $on_gen
. In general, executing an instruction
suspend $e
for some tag $e
means that execution continues at the
innermost ancestor whose resume
instruction installed a suspend
handler for $e
. This behaviour is directly analogous to the search
for a matching exception handler after raising an exception. However,
it is more general in that the handler is also passed the suspended
current continuation. The extent of a suspended continuation captures
execution from the instruction immediately following suspend $e
up
to the resume
instruction that handles $e
. In other words, as well
as resuming a suspended computation and installing a handler, the
resume
instruction also acts as a delimiter for new suspended
continuations created by performing suspend $e
in the scope of the
resumed continuation.
When the generator executes suspend $gen
, execution continues in the
$on_gen
block in $consumer
. In that case, two values are pushed
onto the Wasm value stack. The topmost value is a new suspended
continuation. It is the continuation of executing the generator
following the suspend
instruction (up to the handler). The other
value is the i32
value passed from the generator to the consumer, as
required by the tag's definition. In our example, the consumer simply
prints the generated value and saves the new continuation in $c
to
be resumed in the next iteration.
Second, the parent-child relationship determines where execution
continues after a continuation returns. Control simply transfers to
the next instruction after the resume
instruction that resumed the
continuation in the parent, just as a normal function call returns to
the instruction after its call site. For instance, in our example once
the loop counter $i
reaches 0, the $generator
function returns and
we have reached the end of the continuation. Execution then continues
at the parent immediately after the resume
instruction called by the
consumer, and the consumer also returns.
The full definition of this module can be found here.
The second example demonstrates how to implement task scheduling with
the stack-switching instructions. Specifically, suppose we want to
schedule a number of tasks, represented by functions $task_0
to
$task_n
, to be executed concurrently. Scheduling is cooperative,
meaning that tasks explicitly yield execution so that a scheduler may
pick the next task to run.
One approach is to use the asymmetric stack-switching approach we used
for the generator example. We define a function $entry
that resumes
the initial task and installs a handler for a $yield
tag inside an
event loop. In order to yield execution, tasks simply perform
(suspend $yield)
, transferring control back to the parent
continuation, which is the event loop. The event loop then selects the
next task (if any) from a task queue and resumes it.
This approach is illustrated by the following skeleton code.
(module $scheduler1
(type $ft (func))
;; Continuation type of all tasks
(type $ct (cont $ft))
;; Tag used to yield execution in one task and resume another one.
(tag $yield)
;; Used by scheduler to manage task continuations
(table $task_queue 1000 (ref null $ct))
;; Entry point, becomes parent of all tasks.
;; Also acts as scheduler when tasks yield or finish.
(func $entry (param $initial_task (ref $ft))
;; initialise $task_queue with $initial_task
...
(loop $resume_next
;; pick $next_task from queue, or return if no more tasks.
...
(block $on_yield (result (ref $ct))
(resume $ct (on $yield $on_yield) (local.get $next_task))
;; task finished execution
(br $resume_next)
)
;; task suspended: put continuation in queue, then loop to determine next
;; one to resume.
...
)
)
(func $task_0
...
;; To yield execution, simply suspend to scheduling logic in $entry.
(suspend $yield)
...
)
...
(func $task_n ...)
)
Note that $entry
performs all scheduling; it is responsible for
picking the next task to resume in two different circumstances: a) if
the most recent task suspended itself, and b) if it simply ran to
completion and returned. However, notice that this asymmetric approach
requires two stack switches in order to change execution from one task
to another: first when suspending from the a task to the event loop,
and second when the event loop resumes the next task.
Our proposal provides a mechanism to optimise the particular pattern
shown here, where suspending one continuation is followed by a handler
resuming another continuation $c
. We can use the switch
instruction, which also relies on tags, to transfer control from the
original continuation directly to $c
, thus avoiding the need for an
intermediate stack switch to the parent.
Concretely, executing switch $ct $yield (local.get $c)
in our
example behaves equivalently to (suspend $yield)
, assuming that the
active (ordinary) handler for $yield
immediately resumes $c
and
additionally passes the continuation obtained from handling $yield
along as an argument to $c
. However, as mentioned above, using a
switch
instruction here has the advantage that a Wasm engine can
implement it directly using only a single stack switch.
Each switch
instruction is annotated with the type of the
continuation switched to.
The key idea is to inline scheduling logic in the tasks themselves in order to reduce (or avoid altogether) the need to switch stacks to the event loop in order to implement switching tasks.
This alternative approach is illustrated by the following skeleton code.
(module $scheduler2
(rec
(type $ft (func (param (ref null $ct))))
;; Continuation type of all tasks
(type $ct (cont $ft))
)
;; Tag used to yield execution in one task and resume another one.
(tag $yield)
;; Used by scheduler to manage task continuations
(table $task_queue 1000 (ref null $ct))
;; Entry point, becomes parent of all tasks.
;; Only acts as scheduler when tasks finish.
(func $entry (param $initial_task (ref $ft))
;; initialise $task_queue with $initial_task
...
(loop $resume_next
;; pick $next_task from queue, or return if no more tasks.
;; Note that there is no suspend handler for $yield
...
(resume $ct (on $yield switch) (ref.null $ct) (local.get $next_task))
;; task finished execution: loop to pick next one
(br $resume_next)
...
)
)
(func $task_0 (type $ft)
;; If $c is not null, put in task_queue.
...
;; To yield execution, call $yield_to_next
(call $yield_to_next)
...
)
...
(func $task_n (type $ft) ...)
;; Determines next task to switch to directly.
(func $yield_to_next
;; determine $next_task
...
(block $done
(br_if $done (ref.is_null (local.get $next_task)))
;; Switch to $next_task.
;; The switch instruction implicitly passes a reference to the currently
;; executing continuation as an argument to $next_task.
(switch $ct $yield (local.get $next_task))
;; If we get here, some other continuation switch-ed directly to us, or
;; $entry resumed us.
;; In the first case, we receive the continuation that switched to us here
;; and we need to enqueue it in the task list.
;; In the second case, the passed continuation reference will be null.
...
)
;; Just return if no other task in queue, making the $yield_to_next call
;; a noop.
)
)
Here, the event loop is still responsible for resuming tasks from the
task queue, starting with some initial task. Thus, it will still be
the parent of all task continuations, as in the previous version.
However, it is no longer responsible for handling suspensions of
tasks. Instead, it only resumes the next task from the queue whenever
the previously running task returns. Yielding execution from one task
that merely wishes to suspend itself to another will be handled by the
tasks themselves, using the switch
instruction.
The fact that the event loop does not handle suspensions is reflected
by the fact that its resume
instruction does not install a suspend
handler for the yield
tag. Instead, the resume instruction installs
a switch handler for tag yield
. In order to yield execution a task
calls a separate function $yield_to_next
. The scheduling logic picks
the next task $next_task
and switches directly to it. Here, the
target continuation (i.e., $next_task
in $yield_to_next
) receives
the suspended current continuation (i.e., the one that just called
$switch_to_next
) as an argument. The payload passing mechanism used
for integer values in the generator example is now used to pass
continuation references. The task that we switched to is now
responsible for enqueuing the previous continuation (i.e., the one
received as a payload) in the task list.
As a minor complication, we need to encode the fact that the
continuation switched to receives the current one as an argument in
the type of the continuations handled by all scheduling logic. This
means the type $ct
must be recursive: a continuation of this type
takes a value of type (ref null $ct)
as a parameter. In order to
give the same type to continuations that have yielded execution (those
created by switch
) and those continuations that correspond to
beginning the execution of a $task_i
function (those created by
cont.new
), we add a (ref null $ct)
parameter to all of the
$task_i
functions. Finally, observe that the event loop passes a
null continuation to any continuation it resumes, indicating to the
resumed continuation that there is no previous continuation to enqueue
in the task list.
Note that installing a switch handler for $yield
in entry
is
strictly necessary. It acts as a delimiter, determining the extent of
the suspended continuation created when performing switch
with tag
$yield
. This form of stack-switching is symmetric in the following
sense. Rather than switching back to the parent (as suspend
would),
switch
effectively replaces the continuation under the handler for
yield
in the event loop with a different continuation.
The proposal also allows passing additional payloads when performing a
switch
instruction, besides the suspended current continuation. For
simplicity, our example does not make use of this feature, as we can
see from the type $ct
, which has no further parameters besides the
continuation argument required by switch
. However, this mechanism
could be used to optimise the implementation of task scheduling
further.
In $scheduler2
, if a $task_i
function finishes and therefore
returns, two stack switches are required to continue execution in the
next task in the queue. This is due to the fact that the returning
continuation switches to the parent (i.e., the event loop), which then
resumes the next task. To avoid this additional stack switch, we could
add boilerplate code to all of our task functions. Immediately before
a task function would ordinarily return, it should instead switch to
the next task. When doing so, it should pass a new flag to the target
continuation to indicate that the source continuation should not be
enqueued in the task list, but should instead be cancelled. Cancellation
can be implemented using another instruction, resume_throw
, which is
described later in the document.
Full versions of $scheduler1
and $scheduler2
can be found
here and here.
Here we give an informal account of the proposed instruction set extension. In the specification changes we give a more formal account of the validation rules and changes to the binary format.
For simplicity we ignore subtyping in this section, but in the specification changes we take full account of subtyping.
The proposal adds a new reference type for continuations.
(cont $ft)
A continuation type is specified in terms of a function type $ft
,
whose parameter types t1*
describe the expected stack shape prior to
resuming/starting the continuation, and whose return types t2*
describe the stack shape after the continuation has run to completion.
As a shorthand, we will often write the function type inline and write a continuation type as
(cont [t1*] -> [t2*])
Control tags generalise exception tags to include result types. Operationally, a control tag may be thought of as a resumable exception. A tag declaration provides the type signature of a control tag.
(tag $t (param t1*) (result t2*))
The $t
is the symbolic index of the control tag in the index space
of tags. The parameter types t1*
describe the expected stack layout
prior to invoking the tag, and the result types t2*
describe the
stack layout following an invocation of the operation.
We will often write $t : [t1*] -> [t2*]
as shorthand for indicating
that such a declaration is in scope.
The following instruction creates a suspended continuation from a function.
cont.new $ct : [(ref $ft)] -> [(ref $ct)]
where:
- $ft = func [t1*] -> [t2*]
- $ct = cont $ft
It takes a reference to a function of type [t1*] -> [t2*]
whose body
may perform non-local control flow.
There are three ways to invoke a suspended continuation.
The first way to invoke a continuation is to resume the suspended continuation under a handler. The handler specifies what to do when control is subsequently suspended again.
resume $ct hdl* : [t1* (ref $ct)] -> [t2*]
where:
- $ct = cont [t1*] -> [t2*]
The resume
instruction is parameterised by a continuation type and a
handler dispatch table hdl
. The shape of hdl
can be either:
-
(on $e $l)
mapping the control tag$e
to the label$l
. Intercepting$e
causes a branch to$l
. -
(on $e switch)
allowing a direct switch with control tag$e
.
The resume
instruction consumes its continuation argument, meaning
that a continuation may be resumed only once.
The second way to invoke a continuation is to raise an exception at the control tag invocation site which causes the stack to be unwound.
resume_throw $ct $exn hdl* : [te* (ref $ct)])] -> [t2*]
where:
- $ct = cont [t1*] -> [t2*]
- $exn : [te*] -> []
The resume_throw
instruction is parameterised by a continuation
type, the exception to be raised at the control tag invocation site,
and a handler dispatch table. As with resume
, this instruction also
fully consumes its continuation argument. This instruction raises the
exception $exn
with parameters of type te*
at the control tag
invocation point in the context of the supplied continuation. As an
exception is being raised (the continuation is not actually being
supplied a value) the parameter types for the continuation t1*
are
unconstrained.
The third way to invoke a continuation is to perform a symmetric switch.
switch $ct1 $e : [t1* (ref $ct1)] -> [t2*]
where:
- $e : [] -> [t*]
- $ct1 = cont [t1* (ref $ct2)] -> [t*]
- $ct2 = cont [t2*] -> [t*]
The switch
instruction is parameterised by the type of the
continuation argument ($ct1
) and a control tag
($e
). It suspends the current continuation (of type $ct2
), then
performs a direct switch to the suspended peer continuation (of type
$ct1
), passing in the required parameters (including the just
suspended current continuation, in order to allow the peer to switch
back again). As with resume
and resume_throw
, the switch
instruction fully consumes its suspended continuation argument.
The current continuation can be suspended.
suspend $e : [t1*] -> [t2*]
where:
- $e : [t1*] -> [t2*]
The suspend
instruction invokes the control tag $e
with arguments
of types t1*
. It suspends the current continuation up to the nearest
enclosing handler for $e
. This behaviour is similar to how raising
an exception transfers control to the nearest exception handler that
handles the exception. The key difference is that the continuation at
the suspension point expects to be resumed later with arguments of
types t2*
.
A suspended continuation can be partially applied to a prefix of its arguments yielding another suspended continuation.
cont.bind $ct1 $ct2 : [t1* (ref $ct1)] -> [(ref $ct2)]
where:
- $ct1 = cont [t1* t3*] -> [t2*]
- $ct2 = cont [t3*] -> [t2*]
The cont.bind
instruction binds a prefix of its arguments of type
t1*
to a suspended continuation of type $ct1
, yielding a modified
suspended continuation of type $ct2
. The cont.bind
instruction
also consumes its continuation argument, and yields a new continuation
that can be supplied to resume
,resume_throw
, switch
or
cont.bind
.
There are four different ways in which continuations may be produced
(cont.new,suspend,cont.bind,switch
). A fresh continuation object
is allocated with cont.new
and the current continuation is reused
with suspend
, cont.bind
, and switch
.
The cont.bind
instruction is similar to the func.bind
instruction
that was initially part of the function references proposal. However,
whereas the latter necessitates the allocation of a new closure, as
continuations are single-shot no allocation is necessary: all
allocation happens when the original continuation is created by
preallocating one slot for each continuation argument.
There are four different ways in which suspended continuations are
consumed (resume,resume_throw,switch,cont.bind
). A suspended
continuation may be resumed with a particular handler with resume
;
aborted with resume_throw
; directly switched to via switch
; or
partially applied with cont.bind
.
In order to ensure that continuations are one-shot, resume
,
resume_throw
, switch
, and cont.bind
destructively modify the
suspended continuation such that any subsequent use of the same
suspended continuation will result in a trap.
In this section we discuss some key design considerations.
Resuming a suspended continuation establishes a parent-child relationship which aligns with the caller-callee relationship for standard function calls meaning that no special plumbing is needed in order to compose the non-local control features we define with built-in non-local control features such as traps, exceptions, and embedder integration.
Direct switching to a suspended peer continuation is semantically equivalent to suspending the current continuation with a special switch tag whose payload is the suspended peer continuation in the context of a handler which resumes the peer continuation. However, direct switching can (and should) be optimised to avoid the need to switch control to the handler before switching control to the peer.
Partial application can be important in practice due to the block and
type structure of Wasm, as in order to return a continuation from a
block all branches within the block must agree on the type of
continuation. Using cont.bind
, a producer can ensure that the
branches within a block each produce a continuation with the same type.
Continuations in the current proposal are single-shot (aka linear),
meaning that they should be invoked exactly once. A continuation can
be invoked either by resuming it (with resume
); by aborting it (with
resume_throw
); or by switching to it (with switch
). An attempt to
invoke a continuation more than once results in a trap. Some
applications such as backtracking, probabilistic programming, and
process duplication exploit multi-shot continuations, but none of our
critical use-cases requires multi-shot continuations.
This proposal is based on the function references proposal and exception handling proposal.
We extend the structure of composite types and heap types as follows.
cont <typeidx>
is a new form of composite type(cont $ft) ok
iff$ft ok
and$ft = [t1*] -> [t2*]
We add two new continuation heap types and their subtyping hierarchy:
heaptypes ::= ... | cont | nocont
nocont ok
andcont ok
alwaysnocont
is the bottom type of continuation types, whereascont
is the top type, i.e.nocont <: cont
We change the wellformedness condition for tag types to be more liberal, i.e.
(tag $t (type $ft)) ok
iff$ft ok
and$ft = [t1*] -> [t2*]
In other words, the return type of tag types is allowed to be non-empty.
The new instructions and their validation rules are as follows. To simplify the presentation, we write this:
C.types[$ct] ~~ cont [t1*] -> [t2*]
where we really mean this:
C.types[$ct] ~~ cont $ft
C.types[$ft] ~~ func [t1*] -> [t2*]
This abbreviation will be formalised with an auxiliary function or other means in the spec.
-
cont.new <typeidx>
- Create a new continuation from a given typed funcref.
cont.new $ct : [(ref null $ft)] -> [(ref $ct)]
- iff
C.types[$ct] ~~ cont [t1*] -> [t2*]
- iff
-
cont.bind <typeidx> <typeidx>
- Partially apply a continuation.
cont.bind $ct $ct' : [t3* (ref null $ct)] -> [(ref $ct')]
- iff
C.types[$ct] ~~ cont [t3* t1*] -> [t2*]
- and
C.types[$ct'] ~~ cont [t1'*] -> [t2'*]
- and
[t1*] -> [t2*] <: [t1'*] -> [t2'*]
- iff
-
resume <typeidx> hdl*
- Execute a given continuation.
- If the executed continuation suspends with a control tag
$t
, the corresponding handler(on $t H)
is executed.
- If the executed continuation suspends with a control tag
resume $ct hdl* : [t1* (ref null $ct)] -> [t2*]
- iff
C.types[$ct] ~~ cont [t1*] -> [t2*]
- and
(hdl : t2*)*
- iff
- Execute a given continuation.
-
resume_throw <typeidx> <exnidx> hdl*
- Execute a given continuation, but force it to immediately throw the annotated exception.
- Used to abort a continuation.
resume_throw $ct $e hdl* : [te* (ref null $ct)] -> [t2*]
- iff
C.types[$ct] ~~ cont [t1*] -> [t2*]
- and
C.tags[$e] : tag $ft
- and
C.types[$ft] ~~ func [te*] -> []
- and
(hdl : t2*)*
- iff
-
hdl = (on <tagidx> <labelidx>) | (on <tagidx> switch)
- Handlers attached to
resume
andresume_throw
, handling control tags forsuspend
andswitch
, respectively. (on $e $l) : t*
- iff
C.tags[$e] = tag $ft
- and
C.types[$ft] ~~ func [t1*] -> [t2*]
- and
C.labels[$l] = [t1'* (ref null? $ct)]
- and
t1* <: t1'*
- and
C.types[$ct] ~~ cont [t2'*] -> [t'*]
- and
[t2*] -> [t*] <: [t2'*] -> [t'*]
- iff
(on $e switch) : t*
- iff
C.tags[$e] = tag $ft
- and
C.types[$ft] ~~ func [] -> [t*]
- iff
- Handlers attached to
-
suspend <tagidx>
- Use a control tag to suspend the current computation.
suspend $t : [t1*] -> [t2*]
- iff
C.tags[$t] = tag $ft
- and
C.types[$ft] ~~ func [t1*] -> [t2*]
- iff
-
switch <typeidx> <tagidx>
- Switch to executing a given continuation directly, suspending the current execution.
- The suspension and switch are performed from the perspective of a parent
(on $e switch)
handler, determined by the annotated control tag. switch $ct1 $e : [t1* (ref null $ct1)] -> [t2*]
- iff
C.tags[$e] = tag $ft
- and
C.types[$ft] ~~ func [] -> [t*]
- and
C.types[$ct1] ~~ cont [t1* (ref null? $ct2)] -> [te1*]
- and
te1* <: t*
- and
C.types[$ct2] ~~ cont [t2*] -> [te2*]
- and
t* <: te2*
- iff
The same control tag may be used simultaneously by throw
, suspend
,
switch
, and their associated handlers. When searching for a handler
for an event, only handlers for the matching kind of event are
considered, e.g. only (on $e $l)
handlers can handle suspend
events and only (on $e switch)
handlers can handle switch
events. The handler search continues past handlers for the wrong kind
of event, even if they use the correct tag.
We extend the binary format of composite types, heap types, and instructions.
Opcode | Type | Parameters | Note |
---|---|---|---|
-0x20 | func t1* t2* |
t1* : vec(valtype) t2* : vec(valtype) |
from Wasm 1.0 |
-0x23 | cont $ft |
$ft : typeidx |
new |
The opcode for heap types is encoded as an s33
.
Opcode | Type | Parameters | Note |
---|---|---|---|
i >= 0 | i | from function-references | |
-0x0b | nocont |
new | |
-0x18 | cont |
new |
We use the use the opcode space 0xe0-0xe5
for the seven new instructions.
Opcode | Instruction | Immediates |
---|---|---|
0xe0 | cont.new $ct |
$ct : u32 |
0xe1 | cont.bind $ct $ct' |
$ct : u32 , $ct' : u32 |
0xe2 | suspend $t |
$t : u32 |
0xe3 | resume $ct hdl* |
$ct : u32 (for hdl see below) |
0xe4 | resume_throw $ct $e hdl* |
$ct : u32 , $e : u32 (for hdl see below) |
0xe5 | switch $ct1 $e |
$ct1 : u32 , $e : u32 |
In the case of resume
and resume_throw
we use a leading byte to
indicate the shape of hdl
as follows.
Opcode | On clause shape | Immediates |
---|---|---|
0x00 | (on $t $h) |
$t : u32 , $h : u32 |
0x01 | (on $t switch) |
$t : u32 |