-
Notifications
You must be signed in to change notification settings - Fork 13
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
WasmFX direct switching w/ dynamic lookup #61
Comments
cc @fgmccabe, since we were wondering how the typing of symmetric switching in WasmFX should work. |
I think a different "kind" of tag isn't strictly necessary, but you're right that the way to do this soundly would be to express intent to switch directly on the handler rather than at the suspend point. I'm drawing from recent conversations with @rossberg and @slindley for the rest of this comment - my understanding is they've considered the possibility of this but IIUC it's not been documented. So a regular handler case attached to a At the suspend point, you would just use a regular This has the advantage that the runtime doesn't need to do two separate stack switches with user code in between, although you still need a "linear search" to find the parent handler from whose perspective the direct switch is occurring - this is fundamental to the delimited approach, so that you know where to go if the "switched to" continuation fully returns. In the case that the "direct switch" handler is the immediate parent, this is just a constant overhead (depending on implementation strategy). Sketched reduction rules (courtesy of @rossberg from an earlier conversation)
|
FWIW, here is the complete version of what I suggested at the time:
|
The trick here is that we use a tag to enforce the necessary type consistency across places in the code. |
I believe we would be able to further optimize the direct-switch instruction sequence if, as an additional validation rule: tag declarations could declare they were "switching", |
I think things end up being fairly unified - the code you have to generate at the EDIT: my instinct is against adding a distinguished tag "kind", because tags could show up in many other places (e.g. the types of continuations themselves) where "switching"-ness seems irrelevant. |
If we statically know at the |
I guess the inline code would be a fast path for the case where the innermost handler is the correct one, and if that's not the case, it would fall back to the more general procedure for looking up the handler then trapping or doing the switch? |
The point about inlining is fair. I guess we could interpret the "switching"-ness bit as just saying that it's a validation error for the tag to be part of any handler except a switch handler, so in other contexts it can still be interpreted as normal. This could be added later as an optimisation though based on our experiments - ordinary tags could go everywhere, and would always do the out-of-line thing, and then later the "switch-only" tags could be added on top. |
If we take as a requirement that the compiler must know at the suspend site when it is going to perform a switch1 and consider that we can signal a switch in some combination of the handler, the tag attribute, and the suspend/switch instruction, here are all of the possible designs:
Thankfully the "continuation-switched" and "handler-switched" designs are the two we've already identified. In the "continuation-switched" design, the tag attribute means that the tag may be used for direct switches, but since the continuation is in control of whether a switch or suspend will occur, there's no reason to disallow the special tags from being used for normal suspensions as well. This design is analogous to tail calls, where the callee (or the continuation) can perform a tail call (or a symmetric switch) as an implementation detail that the caller (or the handler) does not participate in. The handler still has to be aware of the possibility of a symmetric switch because it has to use one of the special tags, though, so the encapsulation is not complete. In the "handler-switched" design, the tag attribute means that the tag must be used only for direct switches so the compiler can know for sure at the switch point where a switch is occurring. As discussed above, we could allow the tag to be used for normal suspensions and force the inline fast paths for switches to have a bailout for normal suspensions, but that seems like an unnecessary complication. Similarly, it would be possible to allow switches with normal tags, but then the compiler would not know to inline a fast path at the switch site at all. It seems cleaner to enforce a strict division between tags for normal suspension and tags for switching. In this design, both the handler and continuation have to explicitly participate in the switch by using one of the special tags. Personally, I prefer the "continuation-switched" design because it allows more flexibility for the continuation to use symmetric switching or not at its own discretion and I like the similarities to tail calls. That being said, the two designs are equally expressive and I can't see any reason one would have performance benefits over the other. Footnotes
|
This design seems a little strange, because if the handler has a "switching" tag, IIUC it would still need to provide a body/label target just in case the tag is used for a suspend signal instead.
In the handler+tag attribute design, the So my preferred approach would be
The alternative (as @tlively suggested) is that we add switching tags right away, and only allow these to be used in switching handlers, which I'd be fine with if it's preferred. |
True. I imagine this would be useful in the same kind of situations that |
If I understand correctly, then I suspect that the "continuation-switched" design won't be type-sound. Note how the typing rules I sketched use the tag to tie the types from switch/suspend site and handler together. In particular, only the handler site can enforce that the return type of the target continuation has the right return type as required by the handler. If that wasn't enforced, the target continuation could later return (regularly) with a wrong type of value. So, the handler site has to be involved somehow. |
Right, I described a solution for that back in the opening post:
|
Wouldn't that essentially be the last row in your table? |
Kind of, except that with that design there would be nothing stopping the special tags from being used for asymmetric switching as well, so the handler only knows that symmetric may occur rather than knowing it must occur. I would imagine the last row of the table would have a firmer distinction at the handler. |
To make things more concrete, here's the typing I would propose for the continuation-switched design. Note that the continuation result type
In contrast, the handler-switched design would use a normal If we're serious about supporting programs that primarily use symmetric switching with a single, top-level handler, then we should choose the continuation-switched design and allow those programs to use just a single tag for their symmetric switching. Footnotes
|
This seems unsound because you don't preserve the information about the required return result of the switched-to continuation, which needs to match the output type associated the handler. Consider what happens if you return all the way from the switched-to continuation back to the EDIT: I might have missed some of the implications of the conversation above - apologies if I'm making an objection that's already been addressed - trying to understand now EDIT2: I'm not sure I understand how If you want to uniformly use one tag for all of your symmetric switching (which I think should be possible), you need to establish a uniform ABI so that each switch point has the same type. Side question for @rossberg - for principle types, does |
If I'm understanding correctly that this design assumes the existence of a distinguished "may switch" handler, this seems to lead to codegen inefficiencies on the handler side that are analogous to the potential codegen inefficiencies on the |
The missing piece is once again this part from the opening post:
I should have made this explicit, but the idea is that
But that would be an unnecessary source of inefficiency and loss of type precision. The continuation-switched design allows switching between continuations with any mutually compatible parameter types, just like in bag-o-stacks, as long as their result types match.
Maybe, but I would expect all the codegen to take place on the suspend/switch side and for the handler to only install some data somewhere marking the fact that the handler exists. If that's wrong, then I would suggest going with a combination of the continuation-switched and handler-switched designs where |
It needs a type annotation for the resumed continuation type, which will determine the rest of the input types and the output type. |
Ah I think I understand - so the point is that in This idea works if all the codegen is done at the site of the switch, and I agree that it would save the need to have different tags if you want to direct switch with different input types. It does rule out an implementation where search is performed by directly jumping execution to each handler, with the main logic at each handler site (at least if one wants to avoid additional dynamic checks/boxing on the shape of the "switched-to" payload), since the handler site won't know how many values to pass into the "switched-to" continuation. I don't know how much of a practical problem this is. The approach where all logic is on the handler is more "uniform" (because regular |
@tlively, I'm still a bit lost. Can you write out the typing rules for the handler side, and the reduction and/or desugaring defining execution? |
I realise that at the module level, @tlively's approach doesn't seem to save on the total number of declared tags, since you still need to declare separate tags for the actual continuation types. @tlively, how would you feel if in the "handler-switched" design, the switch handler could take a list of tags (which must all have matching output type)? Then you don't need separate switch handler declarations for each choice and we're just talking about handfuls of bytes (with the explicit Keeping the handler-switched approach preserves the ability for implementations to use the "logic-on-handler" compilation approach if they want (switch-site code still also works), and lets us spec switch handlers as syntactic sugar, and doesn't seem to have any runtime cost - just the requirement to explicitly list the tags you want to permit switching for on the switch handler (which can be weighed against the extra index needed in every |
@conrad-watt, doesn't the handler already take "a list of tags"? Or are you talking about the difference between (on $tag switch)* vs (on tag* switch)? The latter should save about one byte per extra tag, though it actually adds one for the singular case. |
Almost.
You only need one tag, but you do still need a separate continuation type for each type of continuation. These are not the same as tags.
I would strongly prefer not to have to have both a continuation type and a tag for every type of continuation involved in symmetric switching when we can get away with just having the continuation type, especially since tags are not structural and do not work like types w.r.t. composability.
The handler side is a normal
(edited to fix the constraint on the tag type) I'm not going to write out fully precise reduction rules here because that would require some new administrative instruction(s), but the idea is that a continuation is captured up to the delimiter, then passed as the final parameter when resuming the target continuation, just like in any other symmetric switching design. The difference here is that there is no possible desugaring because the handler does not know the type of the other values that are being passed to the target continuation. |
Ah, this was definitely a gap in my mental model. I think this leaves me ok with the idea of using a single tag to carry the necessary result type. The only thing I'd be a little concerned about with the general approach is the idea that an engine might want to do the "logic on handler" compilation strategy, but if at some point during the prototyping process we can get confirmation that this isn't necessary, that's just fine. The one specific part of the design I'd still push on would be that I think a handler with a Side-note: in this comment the result type of the resume is the input type of the symmetric tag, while in this comment the result type of the resume seems to be the output type of the symmetric tag. The input version makes more sense to me, because of the precedent with exception handling tags (in fact, if the label in the symmetric handler is dropped, I'd suggest requiring that the output type be empty, like with exceptions). |
Yes, that's fine with me! In the worst case you would have to have two tags with the same type if you wanted to switch and suspend interchangeably, but since the switch and suspend are already differentiated at both the switch/suspend point and the handler, that's not too burdensome.
Oops, that's a mistake in the second comment. The input version is correct. I've edited the comment to fix it.
That's fine with me too, since the output type is literally not used for anything if these tags cannot be used for normal |
I'm with @conrad-watt that the need for a fake handler is odd. Moreover, if we use a special switch handler, then we can also remove the annotation on the tag itself, can't we? That would be a simplification. I think there are more simplifications possible:
With that in mind we can compare the two versions more directly. Resume as such would be the same for both:
The handler-site version types as follows (omitting subtyping for simplicity):
The switch-site version types as follows:
What this shows is that, mostly, the side conditions move from handler to switch. Obviously, the latter version is a bit more complicated, because it requires a new instruction besides suspend. It's also a bit weird that the tag never materialises a tagged value semantically, but is used essentially as a typed prompt only. The continuation and its arguments are passed to the handler and the target through a kind of side channel. I think this could still be transformed into regular handlers, but only via a global transformation, since we need to expand each switch tag with multiple ones, one for each continuation type it's used with. What bothers me about that, though, is that the handler conceptually has to forward values whose type and shape it doesn't know. In an implementation that would generally require a uniform value representation on the handler side (and the continuation's arguments), unless the switching code is always inlinable at the switch side, where the types are known. That strikes me as a severe constraint on implementations. |
I'm also a bit concerned about this, although I am somewhat drawn to the idea (in @tlively's version) that "child" stacks wanting to switch using some handler as a base can just coordinate between themselves on their continuation types instead of needing to install all their types as tags in the handler. Do we have any precedent, either from EH, or from other language runtimes implementing continuations/coroutines/effects/exceptions regarding how common the inlining approach is? |
When I was playing around with SpecTec, it errored out whenever I tried to make
Originally I used the parameters because the tags could be used for normal suspends and resumes as well, so the types sent by the switch had to be in parameters just like they are in suspends, but now that we've agreed to keep tags for switching and suspending separate, either parameters or results would work. I agree that using the results is more intuitive.
This property is very important for use cases where there is a single top-level handler that is part of the generic language runtime (e.g. perhaps part of a precompiled system library), but then source-level code gets lowered to arbitrary symmetric continuation types depending on what types are being passed around in the source. Requiring that system library to know all the types is less modular and would be a regression in modularity for this use case compared to Bag-o-Stacks, which does not have any handlers to coordinate on.
Thread scheduling systems run the scheduler "inline" on the source thread in the implementation of |
I realized that if we’re distinguishing symmetric switches with both new handlers and a new switch instruction, then there’s no need to use a separate tag attribute. All the necessary type constraints at the resume can come from the handler rather than the tag. |
@tlively, yes, that was my question in my last comment, and why I omitted it from the rules. |
Closing this as resolved. Thanks, everyone! |
This was left over from when we had both 64-bit and 32-bit limits
The continuations explainer includes a section on direct (i.e. symmetric) switching: https://github.com/WebAssembly/stack-switching/blob/main/proposals/continuations/Explainer.md#direct-switching
The second design in that section, reproduced below, gives typing rules for direct switching with dynamic handler lookup:
My question is about the result type
t3*
of the new and previous continuations. For soundness,t3*
needs to be the result type expected by theresume
instruction where the handler was installed, but the proposed typing above has no way to constraint3*
to match that type. Is it expected that there would be a runtime check that they types match?Perhaps another solution would be to introduce a kind of special tag that can only be handled in
resume
s where the tag parameter type matches theresume
result type. We could then giveswitch
a tagidx immediate likeresume
, but constrain it so that the tag must be one of these special tags. That would allow us to know at validation time whatt3*
should be by inspecting the tag.Besides named handlers and
switch_to
, which don't have this problem, are there other ways we could ensure thatt3*
is the type theresume
instruction expects to receive?The text was updated successfully, but these errors were encountered: