You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
[BoS] Add detailed typing rules for instructions (#52)
Including a new `stack.new` instruction. Also add new descriptions of the
semantics of each instruction and a section on potential future instructions we
might want to add.
Copy file name to clipboardexpand all lines: proposals/bag-o-stacks/Explainer.md
+47-43
Original file line number
Diff line number
Diff line change
@@ -180,76 +180,80 @@ In the rest of this document we introduce the key instructions, give some worked
180
180
181
181
## Instructions
182
182
183
-
We introduce instructions for creating, switching between them and terminating coroutines.
183
+
We introduce instructions for creating, switching between, and retiring stacks.
184
184
185
-
For the purposes of exposition, we assume the following schema for stack types used in the following instruction definitions:
185
+
### `stack.new` Create a new stack
186
186
187
-
188
-
```wasm
189
-
(rec
190
-
(type $c0
191
-
(stack (param rt*) (ref $c1)))
192
-
(type $c1
193
-
(stack (param st*) (ref $c0))))
187
+
```pseudo
188
+
C |- stack.new x y : t_1* -> (ref x)
189
+
-- expand(C.TYPES[x]) = stack t_2* rt
190
+
-- expand(C.FUNCS[y]) = func t_1* t_2* rt -> []
194
191
```
195
192
196
-
I.e., a recursive group of types, with at least two members. In general, there may be more members of this group, representing more complex scenarios; but we restrict ourselves for now to this simple world.
193
+
`stack.new` takes two immediates: a type index `x` and a function index `y`. It is valid with type `t_1* -> (ref x)` iff:
197
194
198
-
### `stack.new_switch` Create a new coroutine
195
+
- The expansion of the type at index `x` is a stack type with parameters `t_2* rt`.
196
+
- The expansion of the type of the function at index `y` is a function type `t_1* t_2* rt -> []`.
199
197
200
-
The `stack.new_switch` instruction initiates a new coroutine.
198
+
Let `f` be the function at index `y`. `stack.new` takes a prefix of the arguments necessary to call `f` and allocates a new suspended stack that expects to receive the remaining arguments, determined by the type of the allocated stack. Once the allocated stack is switched to, it will continue on to call `f` with the arguments provided to `stack.new`, the arguments provided to the instruction that performed the switch, and a reference to the previous active stack or a null value if the previous active stack has been retired.
The coroutine function has a distinguished first argument – this is a stack reference that identifies the caller coroutine: specifically, the stack reference is of the caller coroutine, in its current state, with a program counter that immediately follows the `stack.new_switch` instruction itself. The remaining arguments are specific to the coroutine function.
208
+
`switch` takes one immediate: a type index `x`. It is valid with type `t_1* (ref null x) -> t_2* rt` iff:
213
209
214
-
>Note that this caller stack reference is guaranteed to be non-null. On the other hand, the stack reference that the caller will receive, immediately following the stack.new_switch instruction may be null.
210
+
- The expansion of the type at index `x` is a stack type with parameters `t_1* (ref null? y)`.
211
+
- The expansion of the type at index `y` is a stack type with parameters `t_2* rt`.
215
212
216
-
The function's return type must be empty -- coroutine functions are not permitted to return normally. However, the generated `stack` value allows the callee to 'return' to the caller with appropriate results.
213
+
If its stack reference operand is null or detached, `switch` traps. Otherwise, `switch` switches to the stack denoted by its stack reference operand, popping and sending the expected values `t_1*` along with a reference of type `(ref y)` denoting the prior active stack. The parameters of the stack type at index `y` determine what types will be received when the prior active stack is switched back to.
217
214
218
-
The sequence of types (ref $c0) -> (ref $c1) -> (ref null $c0) approximately mimics a function call – using (ref $c0) – and function return – using (ref $c1).
215
+
> TODO: Describe checking whether a switch is allowed and trapping if it is not.
219
216
220
-
>Of course, functions don’t typically return themselves (which modeled with the (ref null $c0) in the sequence); but that is one difference between functions and coroutines.
217
+
### `stack.new_switch` Create and switch to a new stack
221
218
222
-
### `switch` Switch between coroutines
223
-
224
-
The `switch` instruction is used to switch between coroutines. The `switch` instruction is annotated with the type of a stack reference -- the type of any suspended computation it may switch to. The arguments of a `switch` instruction include the stack reference of the switch target and any argument values as indicated by the argument types of the stack reference.
The action of `switch` is to suspend the current coroutine, create a stack reference to the newly suspended coroutine of type `(ref $c1)` and pass the vector of values identified with `rt*` -- together with the newly created stack reference -- to the target coroutine, which must be in `suspended` state. The current coroutine is marked as `suspended` and the target is marked as `active`.
226
+
`stack.new_switch` takes two immmediates: a type index `x_1` and a function index `y`. It is valid with type `t_1* -> t_2* rt` iff:
231
227
232
-
Computation continues with the target coroutine, and the issuing coroutine will only be resumed when explicitly switched to. This may or may not be directly from the code of the target.
228
+
- The expansion of the type at index `x_1` is a stack type with parameters `t_1* (ref null? x_2)`.
229
+
- The expansion of the type of the function at index `y` is a function type `t_1* (ref null? x_2) -> []`
230
+
- The expansion of the type at index `x_2` is a stack type with parameters `t_2* rt`
233
231
234
-
When the issuing computation is resumed, on the stack will be a vector of values corresponding to `st*` together with a potentially null reference to a stack of the same type as was used -- `$c0`.
232
+
`stack.new_switch x_1 y` both allocates a new stack and switches to it. It is equivalent to `(stack.new x y) (switch x)`, but engines should be able to implement it more efficiently because it calls the function immediately without having to stage the arguments anywhere.
235
233
236
-
### `switch_retire` Return results from a coroutine
234
+
> TODO: Consider having `stack.new` take a suffix of the function arguments (except for the return stack reference) rather than a prefix, which would allow us to generalize the validation here to allow the stack type parameters to be a suffix of the function type parameters, while still maintaing the equivalence to `(stack.new x y) (switch x)`. This change may also have performance benefits because the arguments provided at switch time would be able to go in the initial argument registers no matter how many total arguments there are.
237
235
238
-
The`switch_retire`instruction is used to simultaneously terminate a coroutine and switch to another coroutine. It is also used to return a result from the coroutine.
236
+
###`switch_retire`Switch to a stack and retire the old stack
239
237
240
-
```wasm
241
-
switch_retire $c0: (ref $c0) rt* -> unreachable
238
+
```pseudo
239
+
C |- switch_retire x : t_1* (ref null x) -> t_2*
240
+
-- expand(C.TYPES[x]) = stack t_1* (ref null y)
242
241
```
243
242
244
-
As with `switch`, the target receives a vector of values on the stack. It also receives a null stack reference. The null stack reference should be ignored by the target coroutine's code. Again, as with `switch`, the target must be in `suspended` state; and becomes `active` as a result of this instruction.
243
+
`switch_retire` takes one immediate: a type index `x`. It is valid with type `t_1* (ref null x) -> t_2*` iff:
245
244
246
-
The returning coroutine is marked as `moribund` by this operation, and its resources may be released after this instruction. Any attempt to continue executing the moribund coroutine will result in a trap.[^moribund]
245
+
-The expansion of the type at index `x` is a stack type with parameters `t_1* (ref null y)`.
247
246
248
-
[^moribund]: In fact, since switch_retire returns null as its return stack, the only way that a moribund coroutine can be entered is by reusing a previously used stackreference. This would result in a trap regardless of the state of the target coroutine.
247
+
`switch_retire` is very much like `switch`, except that it requires the target stack to be expecting a nullable stack reference and that instead of sending a reference to the previous active stack, it sends a null reference. This makes the previous active stack unreachable and potentially allows the engine to reclaim its resources eagerly. Since the previous active stack can never be resumed and the instructions following the `switch_retire` can never be executed, this instruction is valid with any result type.
249
248
250
-
>Note that this instruction is the principal way by which a coroutine can return a result to another coroutine. The value of the returned result should be encoded in the vector of `rt*` values in such a way that the target coroutine can interpret the result.
251
-
>
252
-
>Typically this will include both normal forms of return and exceptional returns: exceptions are not propagated between coroutines; and so the application code should encode exceptional returns using the `switch_retire` mechanism.
249
+
### Other instructions
250
+
251
+
We may choose to add other instructions to the proposal to round out the instruction set or if there is specific demand for them. Potential additions include:
252
+
253
+
-`stack.new_ref` and `stack.new_switch_ref`: variants of `stack.new` and `stack.new_switch` that take function reference operands instead of function index immediates. The latter instructions can be specified in terms of the `*_ref` variants, but the `*_ref` variants would be less efficient in real implementations.
254
+
-`switch_throw`: Switch to a stack and throw an exception rather than sending the expected values.
255
+
-`return_switch`: Combines a `return_call` with a stack switch.
256
+
-`return_switch_throw`: Combines both of the above.
0 commit comments