Skip to content

Commit eca06cf

Browse files
authored
Merge pull request WebAssembly#111 from WebAssembly/further-examples
Add further examples to explainer
2 parents fbb55e6 + 4511b75 commit eca06cf

File tree

4 files changed

+522
-41
lines changed

4 files changed

+522
-41
lines changed

proposals/stack-switching/Explainer.md

Lines changed: 199 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,9 @@ validation rules to facilitate stack-switching.
2121
1. [Suspending continuations](#suspending-continuations)
2222
1. [Partial continuation application](#partial-continuation-application)
2323
1. [Continuation lifetime](#continuation-lifetime)
24+
1. [Further examples](#further-examples)
25+
1. [Extending the generator](#extending-the-generator)
26+
1. [Canceling tasks](#canceling-tasks)
2427
1. [Design considerations](#design-considerations)
2528
1. [Asymmetric switching](#asymmetric-switching)
2629
1. [Symmetric switching](#symmetric-switching)
@@ -78,12 +81,13 @@ design provides a form of *symmetric switching*.
7881

7982
We illustrate the proposed stack-switching mechanism using two
8083
examples: generators and task scheduling. The generators example uses
81-
asymmetric stack-switching and the task scheduling example uses
82-
symmetric stack-switching.
84+
asymmetric stack-switching. The task scheduling example has two
85+
variants: the first variant uses asymmetric stack-switching and the
86+
second variant uses symmetric stack-switching.
8387

8488
### Generators
8589

86-
The first example illustrates a generator-consumer pattern. Execution
90+
Our first example illustrates a generator-consumer pattern. Execution
8791
switches back and forth between a generator and a consumer execution
8892
stack. Whenever execution switches from the generator to the consumer
8993
the generator also passes a value to the consumer.
@@ -116,20 +120,19 @@ The overall module implementing our example has the following shape.
116120
```wat
117121
(module $generator
118122
(type $ft (func))
119-
;; Types of continuations used by the generator:
123+
;; Type of continuations used by the generator:
120124
;; No need for param or result types: No data passed back to the
121125
;; generator when resuming it, and $generator function has no return
122126
;; values.
123127
(type $ct (cont $ft))
124128
129+
(func $print (import "spectest" "print_i32") (param i32))
130+
125131
;; Tag used to coordinate between generator and consumer: The i32 param
126-
;; corresponds to the generated values passed; no values passed back from
127-
;; generator to consumer.
132+
;; corresponds to the generated values passed to consumer; no values passed
133+
;; back from generator to consumer.
128134
(tag $gen (param i32))
129135
130-
131-
(func $print (import "spectest" "print_i32") (param i32))
132-
133136
;; Simple generator yielding values from 100 down to 1
134137
(func $generator ...)
135138
(elem declare func $generator)
@@ -147,16 +150,16 @@ manipulate suspended continuations of type `(ref $ct)`.
147150
The generator is defined as follows.
148151

149152
```wat
150-
;; Simple generator yielding values from 100 down to 1
153+
;; Simple generator yielding values from 100 down to 1.
151154
(func $generator
152155
(local $i i32)
153156
(local.set $i (i32.const 100))
154-
(loop $l
155-
;; Suspend execution, pass current value of $i to consumer
157+
(loop $loop
158+
;; Suspend execution, pass current value of $i to consumer.
156159
(suspend $gen (local.get $i))
157-
;; Decrement $i and exit loop once $i reaches 0
160+
;; Decrement $i and exit loop once $i reaches 0.
158161
(local.tee $i (i32.sub (local.get $i) (i32.const 1)))
159-
(br_if $l)
162+
(br_if $loop)
160163
)
161164
)
162165
```
@@ -178,9 +181,9 @@ The consumer is defined as follows.
178181
179182
(loop $loop
180183
(block $on_gen (result i32 (ref $ct))
181-
;; Resume continuation $c
184+
;; Resume continuation $c.
182185
(resume $ct (on $gen $on_gen) (local.get $c))
183-
;; $generator returned: no more data
186+
;; $generator returned: no more data.
184187
(return)
185188
)
186189
;; Generator suspended, stack now contains [i32 (ref $ct)]
@@ -257,7 +260,7 @@ The full definition of this module can be found
257260

258261
### Task scheduling
259262

260-
The second example demonstrates how to implement task scheduling with
263+
Our second example demonstrates how to implement task scheduling with
261264
the stack-switching instructions. Specifically, suppose we want to
262265
schedule a number of tasks, represented by functions `$task_0` to
263266
`$task_n`, to be executed concurrently. Scheduling is cooperative,
@@ -276,12 +279,10 @@ This approach is illustrated by the following skeleton code.
276279

277280
```wat
278281
(module $scheduler1
279-
280282
(type $ft (func))
281283
;; Continuation type of all tasks
282284
(type $ct (cont $ft))
283285
284-
285286
;; Tag used to yield execution in one task and resume another one.
286287
(tag $yield)
287288
@@ -442,16 +443,16 @@ continuation references. The task that we switched to is now
442443
responsible for enqueuing the previous continuation (i.e., the one
443444
received as a payload) in the task list.
444445

445-
As a minor complication, we need to encode the fact that the
446-
continuation switched to receives the current one as an argument in
447-
the type of the continuations handled by all scheduling logic. This
448-
means the type `$ct` must be recursive: a continuation of this type
446+
As a minor complication, we must encode the fact that the continuation
447+
switched to receives the current continuation as an argument in the
448+
type of the continuations handled by all scheduling logic. This means
449+
that the type `$ct` must be recursive: a continuation of this type
449450
takes a value of type `(ref null $ct)` as a parameter. In order to
450451
give the same type to continuations that have yielded execution (those
451452
created by `switch`) and those continuations that correspond to
452453
beginning the execution of a `$task_i` function (those created by
453454
`cont.new`), we add a `(ref null $ct)` parameter to all of the
454-
`$task_i` functions. Finally, observe that the event loop passes a
455+
`$task_i` functions. Finally, observe that the event loop passes a
455456
null continuation to any continuation it resumes, indicating to the
456457
resumed continuation that there is no previous continuation to enqueue
457458
in the task list.
@@ -682,6 +683,180 @@ In order to ensure that continuations are one-shot, `resume`,
682683
suspended continuation such that any subsequent use of the same
683684
suspended continuation will result in a trap.
684685

686+
## Further examples
687+
688+
We now illustrate the use of tags with result values and the
689+
instructions `cont.bind` and `resume.throw`, by adapting and extending
690+
the examples of [Section
691+
3](#introduction-to-continuation-based-stack-switching).
692+
693+
### Extending the generator
694+
695+
The `$generator` function in [Section 3](#generators)
696+
produces the values 100 down to 1. It uses the tag `$gen`, defined as
697+
`(tag $gen (param i32))`, to send values to the `$producer` function.
698+
699+
We now adapt the producer to indicate to the generator when to reset
700+
(i.e., start counting down from 100 again). The producer is adapted to
701+
pass a boolean flag to the generator when resuming a continuation.
702+
Correspondingly, the `$gen` tag is adapted to include an `i32` result
703+
type for the flag:
704+
705+
```wat
706+
(tag $gen (param i32) (result i32))
707+
```
708+
709+
In the generator, the instruction `(suspend $gen)` now has type `[i32]
710+
-> [i32]`: the parameter type represents the generated value (as in
711+
the original version of the example) and the result type represents
712+
the flag obtained back from the producer. We adapt the generator to
713+
behave as follows, choosing between resetting or decrementing `$i`:
714+
715+
```wat
716+
(func $generator
717+
(local $i i32)
718+
(local.set $i (i32.const 100))
719+
(loop $loop
720+
;; Suspend execution, pass current value of $i to consumer
721+
(suspend $gen (local.get $i))
722+
;; We now have the flag on the stack given to us by the consumer, telling
723+
;; us whether to reset the generator or not.
724+
(if (result i32)
725+
(then (i32.const 100))
726+
(else (i32.sub (local.get $i) (i32.const 1)))
727+
)
728+
(local.tee $i)
729+
(br_if $loop)
730+
)
731+
)
732+
```
733+
734+
In the producer, we add logic to select the value of the flag, and
735+
pass it to the generator continuation on `resume`. However, this poses
736+
a challenge: the continuation created with `(cont.new $ct0 (ref.func
737+
$generator)))` has the same type as before: a continuation type with
738+
no parameter or return types. In contrast, the type of the
739+
continuation received in a handler block for tag `$gen` expects an
740+
`i32` due to the result type we added to `$gen`. This means that the
741+
producer must now manipulate two different continuation types:
742+
743+
```wat
744+
(type $ft0 (func))
745+
(type $ft1 (func (param i32)))
746+
;; Types of continuations used by the generator:
747+
;; No param or result types for $ct0: $generator function has no
748+
;; parameters or return values.
749+
(type $ct0 (cont $ft0))
750+
;; One param of type i32 for $ct1: An i32 is passed back to the
751+
;; generator when resuming it, and $generator function has no return
752+
;; values.
753+
(type $ct1 (cont $ft1))
754+
```
755+
756+
In order to avoid making the producer function unnecessarily
757+
complicated, we ensure that there is a single local variable that
758+
contains the next continuation to resume; its type will be `(ref
759+
$ct0)`. We can then use `cont.bind` to turn the continuations received
760+
in the handler block from type `(ref $ct1)` into `(ref $ct0)` by
761+
binding the value of the flag to be passed.
762+
763+
The overall function is then defined as follows:
764+
765+
```wat
766+
(func $consumer (export "consumer")
767+
;; The continuation of the generator.
768+
(local $c0 (ref $ct0))
769+
;; For temporarily storing the continuation received in handler.
770+
(local $c1 (ref $ct1))
771+
(local $i i32)
772+
;; Create continuation executing function $generator.
773+
;; Execution only starts when resumed for the first time.
774+
(local.set $c0 (cont.new $ct0 (ref.func $generator)))
775+
;; Just counts how many values we have received so far.
776+
(local.set $i (i32.const 1))
777+
778+
(loop $loop
779+
(block $on_gen (result i32 (ref $ct1))
780+
;; Resume continuation $c0
781+
(resume $ct0 (on $gen $on_gen) (local.get $c0))
782+
;; $generator returned: no more data
783+
(return)
784+
)
785+
;; Generator suspended, stack now contains [i32 (ref $ct0)]
786+
;; Save continuation to resume it in next iteration
787+
(local.set $c1)
788+
;; Stack now contains the i32 value yielded by $generator
789+
(call $print)
790+
791+
;; Calculate flag to be passed back to generator:
792+
;; Reset after the 42nd iteration
793+
(i32.eq (local.get $i) (i32.const 42))
794+
;; Create continuation of type (ref $ct0) by binding flag value.
795+
(cont.bind $ct1 $ct0 (local.get $c1))
796+
(local.set $c0)
797+
798+
(local.tee $i (i32.add (local.get $i) (i32.const 1)))
799+
(br_if $loop)
800+
)
801+
)
802+
```
803+
804+
Here, we set the flag for resetting the generator exactly once (after
805+
it has returned 42 values).
806+
807+
The full version of the extended generator example can be found
808+
[here](examples/generator-extended.wast).
809+
810+
### Canceling tasks
811+
812+
The task scheduling examples from [Section 3](#task-scheduling) yield
813+
either by suspending to the scheduler running in the parent
814+
(asymmetric variant) or by calling a scheduling function that uses
815+
`switch` (symmetric variant).
816+
817+
Suppose we wish to adapt our schedulers to impose a limit on the
818+
number of tasks that can exist at the same time. A simple way to
819+
enforce the limit is to cancel the task at the head of the queue
820+
whenever an attempt is made to add a continuation to a full queue. We
821+
can implement this behaviour with a small modification to our previous
822+
schedulers. Instead of adding tasks to be scheduled directly to a
823+
queue, we call the following function.
824+
825+
```wat
826+
(func $schedule_task (param $c (ref null $ct))
827+
;; If the task queue is too long, cancel a task in the queue
828+
(if (i32.ge_s (call $task_queue-count) (global.get $concurrent_task_limit))
829+
(then
830+
(block $exc_handler
831+
(try_table (catch $abort $exc_handler)
832+
(resume_throw $ct $abort (call $task_dequeue))
833+
)
834+
)
835+
)
836+
)
837+
(call $task_enqueue (local.get $c))
838+
)
839+
```
840+
841+
The `$schedule_task` function checks if the current number of elements
842+
in the queue has already reached the limit. If so, the function takes
843+
an existing continuation from the queue and calls `resume_throw` on
844+
it.
845+
846+
The `resume_throw` instruction is annotated with a newly defined tag,
847+
`$abort`. This tag denotes an exception that will be raised at the
848+
suspension point of the continuation. We then wrap the `resume_throw`
849+
instruction in a `try_table`, which installs an exception handler for
850+
`$abort`. This exception handler simply swallows the exception, which
851+
means that the exception raised at the suspension point cannot escape
852+
the `$schedule_task` function. The old continuation is deallocated and
853+
the function proceeds to enqueue the new continuation.
854+
855+
The full version of the symmetric scheduling example using the
856+
`$schedule_task` function can be found
857+
[here](examples/scheduler2-throw.wast). The changes to the asymmetric
858+
scheduling example are analogous.
859+
685860
## Design considerations
686861

687862
In this section we discuss some key design considerations.
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
(module $generator
2+
(type $ft0 (func))
3+
(type $ft1 (func (param i32)))
4+
;; Type of continuations used by the generator:
5+
;; No param or result types for $ct0: $generator function has no
6+
;; parameters or return values.
7+
(type $ct0 (cont $ft0))
8+
9+
;; One param of type i32 for $ct1: An i32 is passed back to the
10+
;; generator when resuming it, and $generator function has no return
11+
;; values.
12+
(type $ct1 (cont $ft1))
13+
14+
(func $print (import "spectest" "print_i32") (param i32))
15+
16+
;; Tag used to coordinate between generator and consumer: The i32 param
17+
;; corresponds to the generated values passed to consumer, and the i32 result
18+
;; corresponds to the value passed from the consumer back to the generator.
19+
(tag $gen (param i32) (result i32))
20+
21+
22+
;; Simple generator yielding values from 100 down to 1.
23+
;; If non-zero value received back from consumer, resets counter to 100.
24+
(func $generator
25+
(local $i i32)
26+
(local.set $i (i32.const 100))
27+
(loop $loop
28+
;; Suspend execution, pass current value of $i to consumer.
29+
(suspend $gen (local.get $i))
30+
;; We now have the flag on the stack given to us by the consumer, telling
31+
;; us whether to reset the generator or not.
32+
(if (result i32)
33+
(then (i32.const 100))
34+
(else (i32.sub (local.get $i) (i32.const 1)))
35+
)
36+
(local.tee $i)
37+
(br_if $loop)
38+
)
39+
)
40+
(elem declare func $generator)
41+
42+
(func $consumer (export "consumer")
43+
;; The continuation of the generator.
44+
(local $c0 (ref $ct0))
45+
;; For temporarily storing the continuation received in handler.
46+
(local $c1 (ref $ct1))
47+
(local $i i32)
48+
;; Create continuation executing function $generator.
49+
;; Execution only starts when resumed for the first time.
50+
(local.set $c0 (cont.new $ct0 (ref.func $generator)))
51+
;; Just counts how many values we have received so far.
52+
(local.set $i (i32.const 1))
53+
54+
(loop $loop
55+
(block $on_gen (result i32 (ref $ct1))
56+
;; Resume continuation $c0
57+
(resume $ct0 (on $gen $on_gen) (local.get $c0))
58+
;; $generator returned: no more data
59+
(return)
60+
)
61+
;; Generator suspended, stack now contains [i32 (ref $ct0)]
62+
;; Save continuation to resume it in next iteration
63+
(local.set $c1)
64+
;; Stack now contains the i32 value yielded by $generator
65+
(call $print)
66+
67+
;; Calculate flag to be passed back to generator:
68+
;; Reset after the 42nd iteration
69+
(i32.eq (local.get $i) (i32.const 42))
70+
;; Create continuation of type (ref $ct0) by binding flag value.
71+
(cont.bind $ct1 $ct0 (local.get $c1))
72+
(local.set $c0)
73+
74+
(local.tee $i (i32.add (local.get $i) (i32.const 1)))
75+
(br_if $loop)
76+
)
77+
)
78+
79+
)
80+
81+
(invoke "consumer")

0 commit comments

Comments
 (0)