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

Proposal: First-Class Stacks #1360

Open
RossTate opened this issue Jul 29, 2020 · 25 comments
Open

Proposal: First-Class Stacks #1360

RossTate opened this issue Jul 29, 2020 · 25 comments
Labels
async stack switching, JSPI, async, green threads, coroutines

Comments

@RossTate
Copy link

RossTate commented Jul 29, 2020

There are a number of control-flow patterns that go beyond the classic "last-in first-out" form. Coroutining, iterators based on yield, asynchronous I/O and, more exotically, delimited continuations, are all examples of non-linear control flow. These patterns are becoming increasingly important in applications that are intended to be dynamically responsive.

While it is possible to emulate many of these control-flow patterns using standard core WASM features, there is significant cost in doing so — in terms of run-time efficiency, code size, and composability.

At the same time, the space of control-flow patterns is sufficiently diverse that it may not be appropriate to design special mechanisms for each of them. Even focusing on coroutining, for example, there are at least two common patterns of coroutines—so-called symmetric and asymmetric coroutines—with significantly different implementation requirements. Furthermore, the details of how data is exchanged between coroutines also varies greatly; reflecting choices made at the language-design level as well as application design.

This proposal focuses on a suite of low-level mechanisms that would allow language implementers to build different variations on cooperative multi-tasking. Specifically, we focus on enabling the execution of WebAssembly programs on multiple stacks that a WebAssembly application can switch between.

Although important, we do not directly provide the mechanisms necessary to exchange messages between coroutines, nor do we "take a stand" on issues such as whether to support asymmetric vs. symmetric coroutines. Instead we provide the primitives that—in conjunction with other functionality WebAssembly already provides—enables the language implementer to develop their own mechanisms. We do, however, establish the framework for communicating the status of a coroutine when switching to another stack. That status must encode whether the coroutine is terminating and may encode values as part of that event.

A detailed presentation of this proposal can be found here, including detailed examples of how it can be used to support cooperative lightweight threads, asynchronous I/O, and (coming soon) multi-shot tagged delimited continuations. Here we (@fgmccabe and I) present a summary of the key ideas.

Stacks

The proposal introduces a single type: stackref. A stackref is a reference to a complete stack. In particular, the stack never returns and is equipped with a "grounding" frame generated by the host that determines what to do if ever the stack completes execution through other means (e.g. traps). Depending on how the stack was created, that grounding frame might terminate the thread worker the stack is running on or might cause the next event in the event loop to be processed. Details aside, the point is the stack is self contained and can be executed on its own.

In terms of implementation, a stackref references the leaf or active frame of the stack, rather than its root. This is represented by an instruction pointer paired with a stack pointer.

Stack Switching

The key instruction in the proposal is stack.switch:

  • stack.switch $event : [t* stackref] -> unreachable
    • where event $event : [t* stackref]

This instruction transfers control to the designated stack. The event is repurposed from the exception-handling proposal. It is used to convey a message to the target stack. The t* values are the payload of that message plus a stackref. The stackref in the message is the reference to the stack that was just switched from, which is now in a suspended state.

So if a lightweight thread (implemented as a stackref) wanted to yield control to another lightweight thread, it would do so with the following (where we use catch $yielding $yielded as shorthand for "catch only $resuming exceptions and branch to $resumed):

(event $resuming (param stackref))
(func $add_thread_to_schedule (param $thread stackref) ...)
(func $yield_to (param $thread stackref)
  (block $resumed
    (try
      stack.switch $resuming (local.get $thread)
    catch $resuming $resumed
    )
  ) ;; $resumed : [stackref]
  (call $add_thread_to_schedule)
)

The output type of stack.switch is unreachable. That is because the instruction leaves the current stack in a suspended state, and when control is transferred back, the conveyed message is conceptually thrown as an exception from that point. In practice, we expect engines to optimize for this pattern and only throw an exception (i.e. start an unwinding stack walk) if there isn't a statically known catcher for the event.

Notice that this design provides extremely fast switches between stacks. One essentially just swaps the instruction-pointer and stack-pointer registers with the registers storing the instruction pointer and stack pointer of the target stackref.

Stack Construction

Because the host needs contextual information to create a grounding frame (e.g. is this module instance running on a thread worker or on the event loop), we provide no instruction for creating stacks, instead expecting modules to import a function for creating stacks from the environment, e.g. (import "host" "new_stack" (func $new_stack (result stackref))).

Instead, we provide a process for extending stacks with additional stack frames. This is particularly useful for initializing a new stack, but can also be used to implement (at the application-level) multi-shot continuations.

For this, we need a way to describe stack frames, and in particular stack frames that are ready to be switched to receive events. The following is an example of a func one could use to describe the initial stack frame of a lightweight thread:

(event $completing : [i32 f64 stackref])
(func $do_work (param f64) (result f64) ...)
(func $get_thread_manager (result stackref) ...)

(rout $thread_root (param $id i32) (param $input f64) (local $output f64)
  (block $aborted
    (try
      (block $resumed
        (try
          stack.start
          unreachable
        catch $resuming $resumed
        )
      ) ;; $resumed : [stackref]
      (call $add_thread_to_schedule)
      (local.set $output (call $do_work (local.get $input)))
      (stack.switch $work_completed (local.get $id) (local.get $output) (call $get_thread_manager))
    catch $abort $aborted
    )
  ) ;; $aborted : [stackref]
  (stack.switch $resuming)
)

Notice the instruction stack.start. This is used only by stack.extend (below) to say where execution should start when the added (suspended) stack frame is resumed, and it can only be preceded by event-handling setup instructions (like block and try). So in this example, the thread is set up to so that the first time it is resumed it starts to $do_work. The implementation of which may in turn call $yield_to, but eventually all the work gets done, in which case the lightweight thread switches control to the thread manager, handing off the result of its work.

To extend a stack, say to add $thread_root to a newly created stack, one uses stack.extend:

  • stack.extend $func : [t* stackref] -> unreachable
    • where func $func (param t*)

Stack Destruction

This example also illustrates how one can implement their own thread-aborting mechanism. Given a stackref for a lightweight thread, you can send it an $aborting message. That message is not handled by $yield_to, and as such will be thrown and cause the stack to be unwound, until eventually $thread_rout is reached, in which case it will transfer control back to the stack that initiated the aborting (which in turn will drop the stackref in the payload).

Stack Redirection

Many applications "compose" stacks together. This composition, however, is an illusion. The stack components are of course distinct entities, but stack walks are redirected from one stack to another. In this manner, a WebAssembly application can run its entire body on its own stack, but have any exceptions thrown by host code called within that body be redirected to the host stack that initiated the application in the first place. Changing this redirection lets an application conceptually detach its stack from the current host stack, e.g. the current event handler, and attach it to another host stack, e.g. the handler for a promise. A full account of how to support asynchronous I/O with stack redirection can be found here.

Stack-walk redirection is supported by the following instruction:

  • stack.redirect $local instr* end : [ti*] -> [to*]
    • where local $local : stackref
    • and instr* : [ti*] -> [to*]

Whenever a stack walk (say due to an exception being thrown or due to a stack inspection) reaches stack.redirect, the walk is redirected to the stackref in the local variable $local at the time.

Summary

With a new stackref type, a new rout construct, and a few new instructions, this proposal enables applications to treat stacks as first-class values. Every instruction is constant time. The proposal is designed to make use of the existing exception-handling infrastructure, and is designed to compose well with stack inspection. The proposal is independent of garbage collection (e.g. introduces no cycles), and the only implicit memory management is that a stack must be implicitly freed when its stackref is implicitly dropped. But with the combination of these proposals, we have conducted case studies suggesting that this proposal can implement a wide variety of features in the same manner as many existing industry implementations. The only significant shortcoming we have found so far is that stack duplication, needed for multi-shot continuations, is substantially more complicated than would be necessary if the language had complete trusted control over the runtime.

@aardappel
Copy link

On first glance this seems more low level than #1359, which generally seems like a good direction. Can you give a summary of the major differences?

"a process for extending stacks with additional stack frame".. this is the least clear to me. Generally a stack receives a new stack frame on every function call.. what is the run-time cost of this "extension"? Does it have benefits (in terms of needed to pre-allocate less memory)?

@kayceesrk
Copy link

kayceesrk commented Jul 31, 2020

I should say that multi-shot continuations make programming with resources quite difficult. When a programmer writes a function, there is an implicit assumption that the function will return exactly once (either normally or exceptionally). Developers write code that defensively guards against these two cases. With multi-shot continuations, the functions may return more than once and this can break code that has nothing to do with continuations. Consider the hypothetical example,

   m = malloc ();
   try f () with e -> free m; throw e end;
   free m

f may yield to another lightweight thread, which may duplicate the captured continuation and resume both of them. The second resumption would lead to a double-free error. This is due to no fault of the above code snippet as it is unaware that f was going to capture the current continuation.

@RossTate
Copy link
Author

@aardappel I'll try to write that summary up for you shortly. As for extension, it lets you add a stack frame to a stack that is not being executed. I don't know if that answers your question though.

@kayceesrk Your concern is one of the reasons why we chose not to offer direct support for multi-shot continuations or stack duplication. That is, programs do not have to worry about their stacks being arbitrarily duplicated. But for programs that do want multi-shot continuations, they can combine stack inspection and stack extension to build duplicates of (just) their own stack frames. So if you want the functionality, you can implement it for yourself, but only for yourself.

@taralx
Copy link

taralx commented Aug 1, 2020

I'm not clear on why stack.switch injects an exception into the receiving stack instead of just resuming it. Is it so that the stackref doesn't have to be typed?

@taralx
Copy link

taralx commented Aug 1, 2020

I also don't understand the semantic difference between the rout that has a stack.start and a similar function that also accepts a stackref parameter.

@RossTate
Copy link
Author

RossTate commented Aug 1, 2020

Is it so that the stackref doesn't have to be typed?

Yes. We've found a number of more advanced examples have multiple receiving events, and we didn't find that types would save much (if any) performance, for reasons similar to call tags (switching on the event tag is easy, and then you can turn it into an exception if you don't recognize it). I also am writing up an example right now that happens to make use of the dynamic scoping of handlers.

I also don't understand the semantic difference between the rout that has a stack.start and a similar function that also accepts a stackref parameter.

Can you clarify what this similar function you are envisioning does?

@taralx
Copy link

taralx commented Aug 1, 2020

Hm, well, the example you have catches two different exceptions at that point, so I retract my earlier comment. It is different from the equivalent function.

@RossTate
Copy link
Author

RossTate commented Aug 1, 2020

@aardappel This proposal is certainly lower level than that of #1359. I've written up a translation of #1359 into this proposal in soil-initiative/stacks#10. The translation should illustrate both the versatility of this proposal and how #1359's instructions are each a combination of lower-level operations, some of which are not obviously cheap. I can give a summary of it and the takeaways later, but at the moment I need to go to bed. This was a surprisingly fun exercise that I probably should not have let keep me up so late 😄

@kayceesrk
Copy link

IIUC stack.switch is symmetric in the sense that it allows the transfer of control from one stack to another. In particular, switch has to name the stack to which the control has to switch to. In this respect, it is similar to other control operators like shift from shift/reset and throw from call/cc where one would specify the target of the control transfer. Explicitly naming the target hinders composability. Here's what I mean by that. Consider the compilation of a program that composes an asynchronous I/O scheduler and a python style generator? It is natural to have the asynchronous I/O scheduler wrapping the entire program, with the driver for the generator sitting at a higher level. Something like:

aio_scheduler (
   ....
   spawn (....
        generator_driver (... foo ...)
     ....)
  ....)

where foo can both yield a value to the generator and perform asynchronous I/O. When performing a blocking socket.accept, the continuation captured should include the generator driver as part of the continuation. The symmetric view of the stacks does not capture the hierarchical nature of the two schedulers. The captured continuation in foo would be delimited by the generator_driver. With #1359, this hierarchical nature is naturally captured leading to the easier compilation of such compositions. foo doesn't name the target stack when performing socket.accept and the current dynamic stack of handlers captures the intended hierarchical relationship. In particular, neither the generator nor the aio_scheduler needs to be aware of the other users of delimited control operators in the current dynamic scope, which is what I mean by composability.

I also am writing up an example right now that happens to make use of the dynamic scoping of handlers.

I suppose you are planning to show how something like this works in your example.

@RossTate
Copy link
Author

RossTate commented Aug 1, 2020

In this respect, it is similar to other control operators like shift from shift/reset and throw from call/cc where one would specify the target of the control transfer.

I see your reasoning, but the comparison is imprecise. For example, shift from shift/reset involves performing a stack inspection to find the closest containing reset in the evaluation context. That reset then informs you what stack to switch to.

Notice that this involves two steps: find the stack to switch to, and then switch to it. This is generally true for control operators in surface-level languages, and it is likewise true for #1359. But this proposal is much lower lever, and as such we have made those steps separate, and we have found that applications make use of them in a wide variety of manners.

The issue with composability you allude to has to do with how the stack is found. As you say, shift/reset has the problem that its semantics for finding the stack often finds the wrong stack (i.e. features interfere). But our proposal, being low level, does not prescribe in any way how the stack is found. It is up to the application to specify that process, and we have found that they do so in a wide variety of manners.

Part of the misunderstanding is my fault though. In the linked detailed write-up, I make clear that this is designed to complement a design for stack inspection (such as in #1356), which is the low-level feature used by most control operators to implement their stack-finding process. The design in #1356, and used by our examples, repurposes call tags in a manner that ensures composability in much the same way that the event in #1356 ensures composability.

If you want to understand this in more detail, I recommend reading the write up on how to implement composable stacks in this proposal provided here. Or, if you're up for a deep dive, you can read the write up on how to implement all of #1356 in this proposal here.

@kayceesrk
Copy link

Thanks for the response @RossTate.

If you want to understand this in more detail, I recommend reading the write up on how to implement composable stacks in this proposal provided here. Or, if you're up for a deep dive, you can read the write up on how to implement all of #1356 in this proposal here.

I'll have a read through this.

@aardappel
Copy link

@RossTate Thanks for https://github.com/soil-initiative/stacks/pull/10/files?short_path=4d5ad29#diff-4d5ad29d317b0ffd003c55a1b691f51e. The first thing I wonder is the try catch around the stack switching code in resume.. does that imply this functionality is inherent in #1359, and/or that it is compositional with your proposal and you wouldn't need it for most uses?

It seems obvious that your proposal is more low level / has smaller/simpler primitives, though typically if you express 2 language features with the other, they can both end up looking verbose from the perspective of the other :)

@RossTate
Copy link
Author

RossTate commented Aug 4, 2020

@aardappel Unfortunately, I am not quite sure what you are asking (as in, I can read it multiple ways, each of which is sensible, but among which I cannot tell which is your intention). Do you mind clarifying? If it helps in the meanwhile, I've added a second translation of #1359 to the pull request, this time using thread-local storage as was suggested here (supposing such an extension were made to WebAssembly).

though typically if you express 2 language features with the other, they can both end up looking verbose from the perspective of the other :)

Certainly. I tried to emphasize the points in the translation that are meaningful rather than those that are just shuffling. However, there do seem to be some important features missing from #1359. For example, this concern about the inability to inspect continuations, say for linear-memory GC roots, has yet to be addressed. So it seems likely that translating this proposal into #1359, while likely possible, would be unlikely to be faithfully represent a realistic implementation.

@aardappel
Copy link

@RossTate apologies, I am simply trying to get a sense of to what extend your translation of the #1359 primitives reflects the required semantics of #1359 or is simply an "impedance mismatch" between the two proposals. In particular, whether the use of a try catch in your resume is inherent in #1359.

@RossTate
Copy link
Author

RossTate commented Aug 4, 2020

Ah, okay. First, to make sure we're on the same page, I wanna check you didn't miss the last sentence of this note:

The output type of stack.switch is unreachable. That is because the instruction leaves the current stack in a suspended state, and when control is transferred back, the conveyed message is conceptually thrown as an exception from that point. In practice, we expect engines to optimize for this pattern and only throw an exception (i.e. start an unwinding stack walk) if there isn't a statically known catcher for the event.

So try/catch there is encoding the statically known event handlers. We could alternatively have had each of the switching instructions specify a list of event/label pairs, with absent events being thrown as exceptions.

So I suspect what you're noticing with try/catch is an encoding artifact more than anything. If, however, you're noticing that there are multiple handlers of distinct events with distinct types, that is not due to an impedance mismatch. Many examples have multiple handlers, which would not be easily expressible using a statically typed approach.

@kayceesrk
Copy link

kayceesrk commented Sep 1, 2020

Hi @RossTate, thanks for the presentation today. I liked the fact that the proposal is no longer tied to stack inspection.

I had the following observations regarding the two kinds of switch instructions switch_call and switch (I might be getting the names wrong; former switches and performs a function call, the latter just switches and hands the caller's stack to the target). IIUC, the asymmetry mirrors the asymmetry in effect handlers proposal in that switch is similar to suspend and switch_call is similar to resume? The target of the switch/suspend gets the continuation whereas switch_call/resume doesn't. Similar to switch_call, in Multicore OCaml, we implement resume/continue and abort/discontinue as functions that are called on top of the target stack [1]. resume simply returns a value and abort raises the given exception in the target stack.

At this point, the main differences between the two proposals are:

Regarding affine use of stackrefs, you'd mentioned the idea of consuming a resource explicitly, which sounds similar to what #1359/Multicore OCaml does with continuations. I suppose this is a feature that will be necessary in addition to what was proposed today.

Am I missing anything else that's important?

[1] https://github.com/ocaml-multicore/ocaml-multicore/blob/parallel_minor_gc/stdlib/stdlib.ml#L52-L55

@tlively
Copy link
Member

tlively commented Sep 1, 2020

I just want to say thanks for that great summary of the differences, @kayceesrk!

@conrad-watt
Copy link
Contributor

conrad-watt commented Sep 1, 2020

@kayceesrk would #1359 benefit equally from the implementation optimisations which #1360 aims to achieve through ensuring that stackrefs are unique/affine? Or are there fewer synchronization issues in #1359 because of the stack-of-stacks structure?

@kayceesrk
Copy link

As Ross mentioned in the call, using up the continuation is cheap compared to the cost of stack switching. A continuation is just a ref that points to the stack, which gets overwritten to null when the continuation is resumed such that subsequent resumptions are not possible. No synchronisation is necessary since there isn't any parallelism in Wasm (yet). Even when there is, it only needs a CAS. As for whether fewer synchronizations are necessary, yes, #1359 will require fewer since it only creates a single continuation even for a deeply nested stack-of-stacks, whereas #1360 will need to allocate one continuation per a level of nesting. In practice though, we've seen that the depth of the handler stack is quite small, and the difference shouldn't matter. Certainly, useful examples such as async/await and generators have small nesting depth.

@fgmccabe
Copy link

fgmccabe commented Sep 1, 2020

Actually, I would argue that the cost of stack switch in #1360 is low. We have tried to get it to where it is comparable to the cost of a function call. In fact, it should be possible to switch stacks with a very small number (<10?) x64 instructions.
Note: this is a guesstimate.

@RossTate
Copy link
Author

RossTate commented Sep 1, 2020

Thanks @kayceesrk! I'm glad you made the talk. I think your summary hits a number of the big points which I'll try to refine/append here:

  • Value passing when stack switching: Unless I'm misunderstanding what you mean by this, I believe both proposals let you pass values by switching. In Proposal: First-Class Stacks #1360, the payload can include other values.
  • Switching target: In Proposal: First-Class Stacks #1360, you explicitly specify which stack to switch to. In Typed continuations to model stacks #1359, you can only switch to a stack further up the stack of stacks.
  • Composition with stack inspection: Proposal: First-Class Stacks #1360 is design to compose with stack inspection. It would implement Typed continuations to model stacks #1359's cont.suspend operation with a call_stack that finds an answer that performs the relevant stack switch, suspending the stack until it is provided with the relevant answer. But in some cases the answer could be implemented in a more efficient manner, say because the effect handler it implements is tail-resumptive, and so could provide the answer to the call_stack without performing any stack switch. (The application could choose whether to perform that particular stack inspection using big hops or frame by frame.)

IIUC, the asymmetry mirrors the asymmetry in effect handlers proposal in that switch is similar to suspend and switch_call is similar to resume?

I don't find these two particularly analogous. switch and switch_call are very similar to each other (switch can be implemented with switch_call with an identity function, and switch_call can be implemented with a stack.extend (not in the talk) and a switch), whereas suspend and resume are two halves of an asymmetric operation. (switch and switch_call each seem symmetric to me.)

Similar to switch_call, in Multicore OCaml, we implement resume/continue and abort/discontinue as functions that are called on top of the target stack [1].

Cool!

would #1359 benefit equally from the implementation optimisations which #1360 aims to achieve through ensuring that stackrefs are unique/affine?

Just to echo @kayceesrk (at least that's what I think I'm doing), I think the optimizations would apply to both proposals roughly equally in practice. That's part of way I didn't emphasize it in the presentation—I didn't want to accidentally give the impression it was a meaningful difference.


@kayceesrk Have you had a chance to look at the translation in soil-initiative/stacks#10? I'm still happy to meet up to go over it with you.

@bvibber
Copy link

bvibber commented Sep 2, 2020

I'm not sure I understand the special linear type handling for stackref; if the only way to get a stackref is to call a host function, then in web embedding it's a JavaScript value and can be duplicated on the JavaScript side and passed into Wasm multiple times. Do the JS values get somehow tracked and neutered in this situation, like ArrayBuffers transferred across worker threads? Or have I misunderstood?

@RossTate
Copy link
Author

RossTate commented Sep 2, 2020

Thanks for the great question, @Brion. The JS API has coercions to and from WebAssembly types. I would expect that the coercion from a stackref to a JS value would allocate a reference with a mutable field that is initialized to the value of the stackref, and that the coercion from a JS value to a stackref would dereference this field and set it to null. That sounds like what you suggested.

@kayceesrk
Copy link

(The application could choose whether to perform that particular stack inspection using big hops or frame by frame.)

Ok. I thought you were not using stack inspection, but turns out I was wrong. I am a little bit confused about why you would want to do this by hops or frame by frame. Isn't a stackref just a pointer to a stack object which knows its own stack pointer? So a switch need only to load the stack object, load the stack pointer and set %rsp to this pointer and off you go. Given this is straightforward, why would an application do it any other way (frame-by-frame or multiple big hops)? I must be missing something. Perhaps #1360 has a different assumption about the layout of a stack?

I don't find these two particularly analogous. switch and switch_call are very similar to each other (switch can be implemented with switch_call with an identity function, and switch_call can be implemented with a stack.extend (not in the talk) and a switch), whereas suspend and resume are two halves of an asymmetric operation. (switch and switch_call each seem symmetric to me.)

In that case, do you need two primitives, or can you just squash it into one? Does switch_call also capture the current continuation and hand it over to the target? #1359 resume/abort doesn't and that is what brings in the asymmetry.

@kayceesrk Have you had a chance to look at the translation in soil-initiative/stacks#10? I'm still happy to meet up to go over it with you.

Still haven't had a chance. Apologies. I'll find time to do this in the coming week or two.

Another question I had is regarding the use of exnref for discriminating the reason for context switch. Would you need this at all? Couldn't you just encode that as part of the value that would be passed between the stacks when switching (through some other mechanism)?

Are your slides from the talk available somewhere? I'd like to understand how values are passed when switching stacks.

@RossTate
Copy link
Author

RossTate commented Sep 3, 2020

Ok. I thought you were not using stack inspection, but turns out I was wrong.

We are not using stack inspection to implement stack switching. But we would use stack inspection in combination with stack switching to implement #1359. In particular, because cont.suspend does not explicitly provide the stack to switch to, it has to be found in some manner by looking up the current stack (of stacks) to find the appropriate handler/stack. So we implement cont.suspend by performing a (presumably big-hop) stack inspection to find the appropriate handler/stack, and then by performing stack.switch to switch control to the found handler/stack.

In that case, do you need two primitives, or can you just squash it into one?

Yes, per the two translations I gave, but either translation requires additional infrastructure (i.e. an additional func) and we found each primitive had its uses. Of course, this would be an easy thing to change in Phase 1 should empirical data suggested one was really not so necessary.

Does switch_call also capture the current continuation and hand it over to the target?

Yes. The difference is switch_call hands it as the final argument to the specified func whereas switch hands it as the final argument to the specified exception event.

Still haven't had a chance. Apologies. I'll find time to do this in the coming week or two.

No worries, though it would help a lot in determining if #1360 serves your needs just as well as #1359, or if there is some opportunity for further improvement we should incorporate.

Another question I had is regarding the use of exnref for discriminating the reason for context switch. Would you need this at all? Couldn't you just encode that as part of the value that would be passed between the stacks when switching (through some other mechanism)?

We use exceptions but not exnref (due to technical difficulties, we missed the presentation suggesting to remove exnref among other changes to the EH proposal). The reason we do not use exnref is that it is a slow way to pass discriminable values (with different payloads). In particular, exnref requires the event information to be allocated on the heap rather than simply be put on the stack. As for using exceptions, we have come across many instances where having multiple handlers was appropriate, and we have come across some instances that were much better served by permitting handlers to be dynamically scoped. The alternative of encoding the protocol into the type seemed likely to have nearly no performance improvement while at the same time complicating type-checking and reducing flexibility.

Are your slides from the talk available somewhere? I'd like to understand how values are passed when switching stacks.

Sorry, I was waiting for the meeting notes to be added, but I've gone ahead and added them via WebAssembly/meetings#633.

@sunfishcode sunfishcode added the async stack switching, JSPI, async, green threads, coroutines label Dec 21, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
async stack switching, JSPI, async, green threads, coroutines
Projects
None yet
Development

No branches or pull requests

9 participants