Skip to content

Commit f749c3f

Browse files
authored
[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.
1 parent 67efcdb commit f749c3f

File tree

1 file changed

+47
-43
lines changed

1 file changed

+47
-43
lines changed

proposals/bag-o-stacks/Explainer.md

+47-43
Original file line numberDiff line numberDiff line change
@@ -180,76 +180,80 @@ In the rest of this document we introduce the key instructions, give some worked
180180

181181
## Instructions
182182

183-
We introduce instructions for creating, switching between them and terminating coroutines.
183+
We introduce instructions for creating, switching between, and retiring stacks.
184184

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
186186

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 -> []
194191
```
195192

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:
197194

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 -> []`.
199197

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.
201199

202-
```wasm
203-
stack.new_switch $func $c0: t* -> (ref null $c0) st*
204-
```
205-
206-
where `$func` is the index of a function of type:
200+
### `switch` Switch to a stack
207201

208-
```wasm
209-
(type $func (func (param (ref $c1) t*)))
202+
```pseudo
203+
C |- switch x : t_1* (ref null x) -> t_2* rt
204+
-- expand(C.TYPES[x]) = stack t_1* (ref null? y)
205+
-- expand(C.TYPES[y]) = stack t_2* rt
210206
```
211207

212-
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:
213209

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`.
215212

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.
217214

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.
219216
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
221218

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.
225-
226-
```wasm
227-
switch $c0: (ref $c0) rt* -> (ref null $c0) st*
219+
```pseudo
220+
C |- stack.new_switch x_1 y : t_1* -> t_2* rt
221+
-- expand(C.TYPES[x_1]) = stack t_1* (ref null? x_2)
222+
-- expand(C.FUNCS[y]) = func t_1* (ref null? x_2) -> []
223+
-- expand(C.TYPES[x_2]) = stack t_2* rt
228224
```
229225

230-
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:
231227

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`
233231

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.
235233

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.
237235
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
239237

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)
242241
```
243242

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:
245244

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)`.
247246

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 stack reference. 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.
249248

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.
253257

254258
## Examples
255259

0 commit comments

Comments
 (0)