Replies: 3 comments 16 replies
-
My latest thinking on direct switch as the fundamental primitive is that it is still an interesting direction to explore, but for many use cases it seems it would be used to implement something that looks more like delimited continuations anyway. The ergonomics of having primitive delimited continuations that already play nicely with returns and exceptions (rather than trapping or doing something more complicated) may make specifying delimited continuations the better choice for the spec even though they are higher level than direct switch. It's hard to say without implementer feedback, though. |
Beta Was this translation helpful? Give feedback.
-
I have a couple questions from trying to work through an example with nested calls into the host. With this control restriction, how would a wasm suspendable stack switch back to the original host stack that first called into wasm? I'm assuming there would need to be a way to get a reference to the system stack, i.e. a How would this work with JS/host function frames that may need to only run on the system stack? In these cases, we'll need to dynamically switch back to the system stack upon call and then dynamically switch back to the caller stack upon return. In addition, the stack that called the import is no longer valid to switch to because it's expecting to be returned to by the import. This would mean the system stack could have multiple points that are waiting to be switched back to. Switching to the system stack would mean switching to the most recent point waiting to be switched to? Imagine the following timeline:
At (6) wasm can no longer switch back to What would happen if an exception/trap happens at (6)? We have options for the wasm stacks, but the system stacks must be unwound all the way to the root, in stack order, or else we break critical host invariants. In this example, that would mean that It seems like we'd need something close to a 'parent field' on a stack then to do that correctly, which makes this seem close to delimited continuations in practice? I suppose the real issue here is that the dynamic switch back to the system violates the 'no leaving the stack except from switch' rule. You could workaround this by trapping when calling an import that is JS/host. That would then require all calls to JS/host functions to have a switch back to the system stack to perform the call and then switch back to the wasm stack with the results. |
Beta Was this translation helpful? Give feedback.
-
This idea of using undelimited control as the basis for stack switching (really unrestricted jumps) was first conceived in the '60s by Peter Landin. The Scheme/Racket community put this idea into practice during following decades. Through experimentation they discovered a range of issues with this approach, and over time they have moved to using delimited control as the basis for stack switching. In Racket, even History repeats itself, so every so often this idea of using some form of undelimited control as the basis for stack switching is rediscovered along with the issues it entails. Around ten years ago Oleg Kiselyov wrote up an argument against using undelimited control as the basis for stack switching. To me, the key reason why we don't want to use undelimited control as the basis is that we compromise modularity. As you have pointed out yourself, it does not compose with exceptions --- you need to add some special support. However, this approach does not scale to multiple native effects. Personally, I am concerned about the interaction with other native effects in Wasm and the potentially many new native effects, e.g. threads. |
Beta Was this translation helpful? Give feedback.
-
As mentioned at the in-person meeting, existing native implementations of stack-switching constructs all reduce down to a simple
switch
pseudo-instruction.Indeed, all the current proposals are basically layered on top of a
switch
instruction.So, putting details of the instruction aside (e.g. typed or untyped), I would like to discuss why or why not to support
switch
.In particular, I want to discuss this instruction in the context of a design in which the only way for control to leave a stack is via a
switch
(which is how it works natively anyways).Why is that restriction important?
It addresses the composability issue with undelimited continuations.
With undelimited continuations,
callcc
"replaces the world" with the given continuation that computes and returns a value.But where does it return that value to?
The point is that undelimited continuations essentially assume some global return target, and that globalness is what causes their composability issue.
With delimited continuations, on the other hand, the answer of "where to return to" is (mutably) associated with each continuation.
With the control restriction, this distinction becomes immediately apparent in the implementation of the root function on an application-created stack.
For applications supporting a language with (surface-level) undelimited continuations, the root function of its stacks will use a
global
to store the "where to return to" target andswitch
to it at the appropriate time.For applications supporting a language with (surface-level) delimited continuations, the root function of its stacks will use a mutable reference or table entry to store the "where to return to" target for that stack and
switch
to it at the appropriate time.For applications supporting a language with both features, the root function will vary by stack depending on the semantics of the continuation at hand.
As a composability test case, we can consider a wasm instance with no imports and whose only exports are first-order (e.g. no
funcref
parameters) functions.Prior to stack-switching, the only way for such an instance to affect the outside world (besides trapping or not terminating) is to return values to its exports.
With the control restriction, the same is true for
switch
; although internally the application can switch between its own stacks, eventually one of these stacks has toswitch
back to the stack with the original function call into the instance in order to return a value.That is,
switch
cannot "replace the world"!I suspect the advantages of supporting
switch
are obvious.(If that suspicion is wrong, please start a thread here asking me to elaborate.)
So my question to the group for this discussion is: why should we not support
switch
?Details
For handling foreign exceptions, many root functions will need a
switch_rethrow
instruction, only usable withincatch_all
(andcatch
) blocks, in order to rethrow foreign exceptions from the appropriate "where to return to" target.Traps are complicated.
We ran out of time at the in-person meeting for me to give my presentation on traps, so for the sake of this discussion please assume applications can trap and there is a good semantics for what to do in such a situation.
Beta Was this translation helpful? Give feedback.
All reactions