Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Optional Support for Multi-Shot Continuations #110

Open
GunpowderGuy opened this issue Feb 7, 2025 · 10 comments
Open

Optional Support for Multi-Shot Continuations #110

GunpowderGuy opened this issue Feb 7, 2025 · 10 comments

Comments

@GunpowderGuy
Copy link

This Stack Switching Proposal introduces one-shot continuations, which are efficient and align with a contiguous stack model. However, languages like Scheme, Racket, and other functional or dynamic languages rely on multi-shot continuations (e.g., call/cc), which can be invoked multiple times and require capturing and restoring the stack.

Would it be feasible to optionally support multi-shot continuations in this proposal? This could allow languages that depend on full continuation support to target WebAssembly without losing core features. Even if there’s a performance trade-off (e.g., using a linked-list stack or stack copying), having the option would broaden WebAssembly’s applicability.

Note : Racket supports both one shot and reusable continuations, both delimited. Scheme only supports undelinited reusable continuations, but they can be implemented over delimited ones

@rossberg
Copy link
Member

rossberg commented Feb 7, 2025

In principle, that is a possible future extension, which we envision would most likely be introduced through a clone instruction.

In practice, though, the problem is that most of the major Wasm implementations (especially Web engines), are fundamentally built on the assumption that stacks aren't movable, as they tend to be interleaved with native (C++) frames that may be pointed into, e.g., to stack-allocated GC roots. So introducing multi-shot would potentially require major design changes for these engines, with possible performance implications. That's why we very consciously left it out of the current design. And although I sympathise, it might be a tough sell in the future, too. But who knows, never say never.

@vouillon
Copy link
Contributor

vouillon commented Feb 7, 2025

most of the major Wasm implementations (especially Web engines), are fundamentally built on the assumption that stacks aren't movable, as they tend to be interleaved with native (C++) frames that may be pointed into, e.g., to stack-allocated GC roots

I'm not sure about that. For instance, V8 switches to a central stack when calling C++ functions.

@fgmccabe
Copy link
Collaborator

fgmccabe commented Feb 7, 2025

V8 does indeed switch to the central stack; furthermore, suspended computations may only contain wasm frames.

However, there are other issues. For example, CFI mitigation schemes based on shadow stacks are not compatible with cloning stack memories.

Overall, V8 is likely to be extremely skeptical of stack cloning.

@GunpowderGuy
Copy link
Author

Reusable continuations are typically implemented as linked lists. As persistent data structures, they eliminate the need for cloning, but this approach introduces its own complexities and trade-offs.

This method of implementing reusable continuations could be integrated as an extension to the Stack Switching Proposal, or achieved independently by generating CPS (Continuation-Passing Style) WebAssembly code—an approach that also works for one-shot continuations.

Alternatives like cloning a contiguous stack, using an additional linked list stack for reusable continuations, or leaving continuation management to the user each come with their own trade-offs in terms of performance, complexity, and memory usage.

Perhaps @wingo could offer insights here, given his work on a WebAssembly Scheme compiler.

@rossberg
Copy link
Member

Wasm implementations indeed use chained segmented stacks to represent continuations. But unfortunately, that does not eliminate the need for copying. As soon as you have stack-allocated mutable state, which is omnipresent in Wasm, e.g., due to locals, you cannot reenter a continuation without copying the respective stack chain. Reentrance would also wreak havoc with resource handles and other linear entities that implementations like to place on the stack.

A language runtime needs to make very different and careful design choices if it wants to support such reentrance. The problem is somewhat similar to introducing thread-safety after the fact.

@davexunit
Copy link

I talked to @wingo about this briefly. He challenged me to think of a time when I resumed a continuation more than once in a real world program. I couldn't think of an example. So, I think we feel that multi-shot is not a must-have for delimited continuations in the Hoot project. The more important thing is for the Wasm stack to be growable because Guile does not have a stack size limit and many Guile programs rely on this feature.

In summary, I think that one-shot continuations + growable stack would be sufficient for our needs and maybe even for some other functional languages that use a lot of recursion. (There's a question of how we'd handle showing Scheme backtraces if we moved from our own explicit stack made from tables to a Wasm stack but that's neither here nor there.)

@fgmccabe
Copy link
Collaborator

+1 to growable stacks.
Also, implicit should be some expectation for scale: e.g., how many suspended computations is reasonable to expect in a 'reasonable' resource.
For me, any 64bit system should be able to support 1million suspended computations, and a 32bit system should be able to support >10K suspended computations.

@titzer
Copy link
Contributor

titzer commented Feb 12, 2025

Wizard's implementation places all host->wasm calls onto a new stack segment, so Wasm code is always on the bottom of a stack segment. Its host call API forces callers to deal with suspensions, so it's not possible to suspend host frames. When Wasm code calls out to host code, the host code cannot suspend the calling stack. Thus suspended Wasm stacks in Wizard's design only contain Wasm frames. That would admit stack copying somewhat easily.

That said, I think to the other concerns (e.g. linear use of resources) make multi-shot continuations something that would have to be opted-into.

@rossberg
Copy link
Member

@titzer, isn't that assuming that no host function ever calls back into Wasm?

@titzer
Copy link
Contributor

titzer commented Feb 12, 2025

They always get a fresh stack when they do so. That also means that a return from Wasm to host can always recycle the stack back to a linked list, so usually getting a fresh stack is just taking an item from a linked list.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

6 participants