Skip to content

Commit 5bf661b

Browse files
authored
Add full code for $scheduler1 and $scheduler2 examples (#83)
The third section of the explainer currently compares task scheduling using `resume`/`suspend` vs `switch` using two example modules, `$scheduler1` and `$scheduler2`. The explainer only contains a skeleton of the actual code. This PR adds `.wast` files containing full code for these modules and links to them in the explainer. The code is carefully written to follow the structure shown in the explainer.
1 parent c2f9449 commit 5bf661b

File tree

3 files changed

+365
-4
lines changed

3 files changed

+365
-4
lines changed

proposals/stack-switching/Explainer.md

+7-4
Original file line numberDiff line numberDiff line change
@@ -290,8 +290,8 @@ This approach is illustrated by the following skeleton code.
290290
291291
;; Entry point, becomes parent of all tasks.
292292
;; Also acts as scheduler when tasks yield or finish.
293-
(func $entry
294-
;; initialise $task_queue with initial task
293+
(func $entry (param $initial_task (ref $ft))
294+
;; initialise $task_queue with $initial_task
295295
...
296296
(loop $resume_next
297297
;; pick $next_task from queue, or return if no more tasks.
@@ -369,8 +369,8 @@ code.
369369
370370
;; Entry point, becomes parent of all tasks.
371371
;; Only acts as scheduler when tasks finish.
372-
(func $entry
373-
;; initialise $task_queue with initial task
372+
(func $entry (param $initial_task (ref $ft))
373+
;; initialise $task_queue with $initial_task
374374
...
375375
(loop $resume_next
376376
;; pick $next_task from queue, or return if no more tasks.
@@ -485,6 +485,9 @@ enqueued in the task list, but should instead be cancelled. Cancellation
485485
can be implemented using another instruction, `resume_throw`, which is
486486
described later in the document.
487487

488+
Full versions of `$scheduler1` and `$scheduler2` can be found
489+
[here](examples/scheduler1.wast) and [here](examples/scheduler2.wast).
490+
488491
## Instruction set extension
489492

490493
Here we give an informal account of the proposed instruction set
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
;; queue of threads
2+
(module $queue
3+
4+
(type $ft (func))
5+
(type $ct (cont $ft))
6+
7+
;; Table as simple queue (keeping it simple, no ring buffer)
8+
(table $task_queue 0 (ref null $ct))
9+
(global $qdelta i32 (i32.const 10))
10+
(global $qback (mut i32) (i32.const 0))
11+
(global $qfront (mut i32) (i32.const 0))
12+
13+
(func $queue_empty (export "queue-empty") (result i32)
14+
(i32.eq (global.get $qfront) (global.get $qback))
15+
)
16+
17+
(func $dequeue (export "dequeue") (result (ref null $ct))
18+
(local $i i32)
19+
(if (call $queue_empty)
20+
(then (return (ref.null $ct)))
21+
)
22+
(local.set $i (global.get $qfront))
23+
(global.set $qfront (i32.add (local.get $i) (i32.const 1)))
24+
(table.get $task_queue (local.get $i))
25+
)
26+
27+
(func $enqueue (export "enqueue") (param $k (ref null $ct))
28+
;; Check if queue is full
29+
(if (i32.eq (global.get $qback) (table.size $task_queue))
30+
(then
31+
;; Check if there is enough space in the front to compact
32+
(if (i32.lt_u (global.get $qfront) (global.get $qdelta))
33+
(then
34+
;; Space is below threshold, grow table instead
35+
(drop (table.grow $task_queue (ref.null $ct) (global.get $qdelta)))
36+
)
37+
(else
38+
;; Enough space, move entries up to head of table
39+
(global.set $qback (i32.sub (global.get $qback) (global.get $qfront)))
40+
(table.copy $task_queue $task_queue
41+
(i32.const 0) ;; dest = new front = 0
42+
(global.get $qfront) ;; src = old front
43+
(global.get $qback) ;; len = new back = old back - old front
44+
)
45+
(table.fill $task_queue ;; null out old entries to avoid leaks
46+
(global.get $qback) ;; start = new back
47+
(ref.null $ct) ;; init value
48+
(global.get $qfront) ;; len = old front = old front - new front
49+
)
50+
(global.set $qfront (i32.const 0))
51+
)
52+
)
53+
)
54+
)
55+
(table.set $task_queue (global.get $qback) (local.get $k))
56+
(global.set $qback (i32.add (global.get $qback) (i32.const 1)))
57+
)
58+
)
59+
(register "queue")
60+
61+
(module $scheduler1
62+
(type $ft (func))
63+
;; Continuation type of all tasks
64+
(type $ct (cont $ft))
65+
66+
67+
(func $task_enqueue (import "queue" "enqueue") (param (ref null $ct)))
68+
(func $task_dequeue (import "queue" "dequeue") (result (ref null $ct)))
69+
(func $task_queue-empty (import "queue" "queue-empty") (result i32))
70+
(func $print_i32 (import "spectest" "print_i32") (param i32))
71+
72+
;; Tag used to yield execution in one task and resume another one.
73+
(tag $yield)
74+
75+
;; Entry point, becomes parent of all tasks.
76+
;; Also acts as scheduler when tasks yield or finish.
77+
(func $entry (param $initial_task (ref $ft))
78+
(local $next_task (ref null $ct))
79+
80+
;; initialise $task_queue with initial task
81+
(call $task_enqueue (cont.new $ct (local.get $initial_task)))
82+
83+
(loop $resume_next
84+
;; pick $next_task from queue, or return if no more tasks.
85+
(if (call $task_queue-empty)
86+
(then (return))
87+
(else (local.set $next_task (call $task_dequeue)))
88+
)
89+
(block $on_yield (result (ref $ct))
90+
(resume $ct (on $yield $on_yield) (local.get $next_task))
91+
;; task finished execution
92+
(br $resume_next)
93+
)
94+
;; task suspended: put continuation in queue, then loop to determine next
95+
;; one to resume.
96+
(call $task_enqueue)
97+
(br $resume_next)
98+
)
99+
)
100+
101+
;; To simplify the example, all task_i functions execute this function. Each
102+
;; task has an $id, but this is only used for printing.
103+
;; $to_spawn represents another task that this function will add to the task
104+
;; queue, unless the reference is null.
105+
(func $task_impl
106+
(param $id i32)
107+
(param $to_spawn (ref null $ft))
108+
109+
(if (ref.is_null (local.get $to_spawn))
110+
(then)
111+
(else (call $task_enqueue (cont.new $ct (local.get $to_spawn)))))
112+
113+
(call $print_i32 (local.get $id))
114+
(suspend $yield)
115+
(call $print_i32 (local.get $id))
116+
)
117+
118+
;; The actual $task_i functions simply call $task_impl, with i as the value
119+
;; for $id, and $task_(i+1) as the task to spawn, except for $task_3, which
120+
;; does not spawn another task.
121+
;;
122+
;; The observant reader may note that all $task_i functions may be seen as
123+
;; partial applications of $task_impl.
124+
;; Indeed, we could obtain *continuations* running each $task_i from a
125+
;; continuation running $task_impl and cont.bind.
126+
127+
(func $task_3
128+
(i32.const 3)
129+
(ref.null $ft)
130+
(call $task_impl)
131+
)
132+
(elem declare func $task_3)
133+
134+
(func $task_2
135+
(i32.const 2)
136+
(ref.func $task_3)
137+
(call $task_impl)
138+
)
139+
(elem declare func $task_2)
140+
141+
(func $task_1
142+
(i32.const 1)
143+
(ref.func $task_2)
144+
(call $task_impl)
145+
)
146+
(elem declare func $task_1)
147+
148+
(func $task_0
149+
(i32.const 0)
150+
(ref.func $task_1)
151+
(call $task_impl)
152+
)
153+
(elem declare func $task_0)
154+
155+
156+
(func (export "main")
157+
(call $entry (ref.func $task_0))
158+
)
159+
)
160+
(invoke "main")
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
1+
;; queue of threads
2+
(module $queue
3+
(rec
4+
(type $ft (func (param (ref null $ct))))
5+
(type $ct (cont $ft)))
6+
7+
;; Table as simple queue (keeping it simple, no ring buffer)
8+
(table $task_queue 0 (ref null $ct))
9+
(global $qdelta i32 (i32.const 10))
10+
(global $qback (mut i32) (i32.const 0))
11+
(global $qfront (mut i32) (i32.const 0))
12+
13+
(func $queue_empty (export "queue-empty") (result i32)
14+
(i32.eq (global.get $qfront) (global.get $qback))
15+
)
16+
17+
(func $dequeue (export "dequeue") (result (ref null $ct))
18+
(local $i i32)
19+
(if (call $queue_empty)
20+
(then (return (ref.null $ct)))
21+
)
22+
(local.set $i (global.get $qfront))
23+
(global.set $qfront (i32.add (local.get $i) (i32.const 1)))
24+
(table.get $task_queue (local.get $i))
25+
)
26+
27+
(func $enqueue (export "enqueue") (param $k (ref null $ct))
28+
;; Check if queue is full
29+
(if (i32.eq (global.get $qback) (table.size $task_queue))
30+
(then
31+
;; Check if there is enough space in the front to compact
32+
(if (i32.lt_u (global.get $qfront) (global.get $qdelta))
33+
(then
34+
;; Space is below threshold, grow table instead
35+
(drop (table.grow $task_queue (ref.null $ct) (global.get $qdelta)))
36+
)
37+
(else
38+
;; Enough space, move entries up to head of table
39+
(global.set $qback (i32.sub (global.get $qback) (global.get $qfront)))
40+
(table.copy $task_queue $task_queue
41+
(i32.const 0) ;; dest = new front = 0
42+
(global.get $qfront) ;; src = old front
43+
(global.get $qback) ;; len = new back = old back - old front
44+
)
45+
(table.fill $task_queue ;; null out old entries to avoid leaks
46+
(global.get $qback) ;; start = new back
47+
(ref.null $ct) ;; init value
48+
(global.get $qfront) ;; len = old front = old front - new front
49+
)
50+
(global.set $qfront (i32.const 0))
51+
)
52+
)
53+
)
54+
)
55+
(table.set $task_queue (global.get $qback) (local.get $k))
56+
(global.set $qback (i32.add (global.get $qback) (i32.const 1)))
57+
)
58+
)
59+
(register "queue")
60+
61+
(module $scheduler2
62+
(rec
63+
(type $ft (func (param (ref null $ct))))
64+
;; Continuation type of all tasks
65+
(type $ct (cont $ft))
66+
)
67+
68+
(func $task_enqueue (import "queue" "enqueue") (param (ref null $ct)))
69+
(func $task_dequeue (import "queue" "dequeue") (result (ref null $ct)))
70+
(func $task_queue-empty (import "queue" "queue-empty") (result i32))
71+
(func $print_i32 (import "spectest" "print_i32") (param i32))
72+
73+
;; Tag used to yield execution in one task and resume another one.
74+
(tag $yield)
75+
76+
;; Entry point, becomes parent of all tasks.
77+
;; Only acts as scheduler when tasks finish.
78+
(func $entry (param $initial_task (ref $ft))
79+
(local $next_task (ref null $ct))
80+
81+
;; initialise $task_queue with initial task
82+
(call $task_enqueue (cont.new $ct (local.get $initial_task)))
83+
84+
(loop $resume_next
85+
;; pick $next_task from queue, or return if no more tasks.
86+
;; Note that there is no suspend handler for $yield
87+
(if (call $task_queue-empty)
88+
(then (return))
89+
(else (local.set $next_task (call $task_dequeue)))
90+
)
91+
(resume $ct (on $yield switch)
92+
(ref.null $ct) (local.get $next_task))
93+
;; task finished execution: loop to pick next one
94+
(br $resume_next)
95+
)
96+
)
97+
98+
;; To simplify the example, all task_i functions execute this function. Each
99+
;; task has an $id, but this is only used for printing.
100+
;; $to_spawn represents another task that this function will add to the task
101+
;; queue, unless the reference is null.
102+
;; $c corresponds to the continuation parameter of the original $task_i
103+
;; functions.
104+
;; This means that it is the previous continuation we just switch-ed away
105+
;; from, or a null reference if the task was resumed from $entry.
106+
(func $task_impl
107+
(param $id i32)
108+
(param $to_spawn (ref null $ft))
109+
(param $c (ref null $ct))
110+
111+
(if (ref.is_null (local.get $c))
112+
(then)
113+
(else (call $task_enqueue (local.get $c))))
114+
115+
(if (ref.is_null (local.get $to_spawn))
116+
(then)
117+
(else (call $task_enqueue (cont.new $ct (local.get $to_spawn)))))
118+
119+
(call $print_i32 (local.get $id))
120+
(call $yield_to_next)
121+
(call $print_i32 (local.get $id))
122+
)
123+
124+
;; The actual $task_i functions simply call $task_impl, with i as the value
125+
;; for $id, and $task_(i+1) as the task to spawn, except for $task_3, which
126+
;; does not spawn another task.
127+
;;
128+
;; The observant reader may note that all $task_i functions may be seen as
129+
;; partial applications of $task_impl.
130+
;; Indeed, we could obtain *continuations* running each $task_i from a
131+
;; continuation running $task_impl and cont.bind.
132+
133+
(func $task_3 (type $ft)
134+
(i32.const 3)
135+
(ref.null $ft)
136+
(local.get 0)
137+
(call $task_impl)
138+
)
139+
(elem declare func $task_3)
140+
141+
(func $task_2 (type $ft)
142+
(i32.const 2)
143+
(ref.func $task_3)
144+
(local.get 0)
145+
(call $task_impl)
146+
)
147+
(elem declare func $task_2)
148+
149+
(func $task_1 (type $ft)
150+
(i32.const 1)
151+
(ref.func $task_2)
152+
(local.get 0)
153+
(call $task_impl)
154+
)
155+
(elem declare func $task_1)
156+
157+
(func $task_0 (type $ft)
158+
(i32.const 0)
159+
(ref.func $task_1)
160+
(local.get 0)
161+
(call $task_impl)
162+
)
163+
(elem declare func $task_0)
164+
165+
166+
;; Determines next task to switch to directly.
167+
(func $yield_to_next
168+
(local $next_task (ref null $ct))
169+
(local $received_task (ref null $ct))
170+
171+
;; determine $next_task
172+
(local.set $next_task (call $task_dequeue))
173+
174+
(block $done
175+
(br_if $done (ref.is_null (local.get $next_task)))
176+
;; Switch to $next_task.
177+
;; The switch instruction implicitly passes a reference to the currently
178+
;; executing continuation as an argument to $next_task.
179+
(switch $ct $yield (local.get $next_task))
180+
;; If we get here, some other continuation switch-ed directly to us, or
181+
;; $entry resumed us.
182+
;; In the first case, we receive the continuation that switched to us here
183+
;; and we need to enqueue it in the task list.
184+
;; In the second case, the passed continuation reference will be null.
185+
(local.set $received_task)
186+
(if (ref.is_null (local.get $received_task))
187+
(then)
188+
(else (call $task_enqueue (local.get $received_task))))
189+
)
190+
;; Just return if no other task in queue, making the $yield_to_next call
191+
;; a noop.
192+
)
193+
194+
(func (export "main")
195+
(call $entry (ref.func $task_0))
196+
)
197+
)
198+
(invoke "main")

0 commit comments

Comments
 (0)