-
Notifications
You must be signed in to change notification settings - Fork 14
[BoS] stack.new
takes a suffix rather than prefix of func args
#53
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
Conversation
This stack of pull requests is managed by Graphite. Learn more about stacking. |
I don't see how this change "allows us to relax the typing of stack.new_switch to be able to use stack In addition, did you mean: ? |
|
||
`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. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do you mean
(stack.new y)(switch x)
?
(Otherwise, there is an extraneous x floating around)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No, because stack.new
takes both the stack type index x
and the function index y
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
got it.
The full list of required arguments comes from the function type, and the types given in the stack type just need to match a prefix of those. |
The arguments to the coroutine function must be provided in total. However, those arguments can come from a combination of the arguments provided at stack.new and the remainder can come from those provided at switch. Is this also your understanding? |
There is something else that is worth mentioning: The normal (but not universal) ABI for a function call is for the arguments to be evaluated in a left-to-right order. This is currently strongly encouraged in wasm. However, the order of evaluation of arguments in a stack.new/switch is not expected to (does not) follow this pattern. |
Yes, I think that sounds right.
You're talking about ABIs on physical machines, right? How is this encouraged in Wasm?
Can you explain this in more detail? Is this something you think should be fixed, documented in the explainer, or just noted here in the comments? |
I think what @fgmccabe is saying (correct me if I'm wrong!) is that when we do partial application it is conventional to supply a prefix of the arguments (left-to-right) rather than a suffix (right-to-left) as you are doing with the form of partial application you're building into For instance in OCaml we can do something like this: # let f (x : int) (y : bool) = (x, y);;
val f : int -> bool -> int * bool = <fun>
# let g = f 42;;
val g : bool -> int * bool = <fun>
# g true;;;
- : int * bool = (42, true) The function Also see the typing rule for Speaking of which, what's the rationale for hard-wiring partial application into Perhaps your thinking is that the former can be easier to optimise (albeit at the cost of being less expressive)? I think questions over to what extent we support partial application / closures for stack-switching in Wasm are (like a number of other design questions) largely orthogonal to whether we're talking about BoS or WasmFX. |
Ah yes, I agree that it seems initially less surprising to bind the arguments from left to right (although I don't believe we have precedent in standardized Wasm). I also agree that whether we allow partial applications is a discussion we can have separately. If we do allow partial application, however, there would be strong benefits to doing it as described in this PR, even though it is counterintuitive. |
Can you expand on this (is this point related to conventions in Binaryen or LLVM)? My intuition is also that binding should be left-to-right, and if we consider partial binding of funcrefs in future, she should try to stay consistent with whatever we decide for stacks. EDIT: as a knee-jerk argument in favour of left-to-right binding, any source-level functional language with partial application will expect left-to-right binding, so making a different decision in Wasm will make it harder for these languages to map their functions down to Wasm functions+bind. |
It's not because of any conventions, it's because only right-to-left binding gives the property that Binding right-to-left might create a mismatch with the source that causes some issues for producers, but I anticipate that root functions will generally be provided by the producer-owned runtime rather than arbitrary source code, so I expect that producers will be able to use right-to-left binding without issue. |
In support of 'partial application': Without partial application, an instruction like stack.new (or cont.new) is arguably nearly useless. To see this, consider creating a generator; let's call the generator 'walk'. The walk function walks over a binary tree, yielding each element it finds in response to a 'next' prompt. The question is, how does walk know which tree to walk over: without partial application, it becomes extremely difficult to parameterize walk with the right tree. Note that this is not really an issue with stack.new_switch -- since this can be iconified as 'call a function on a new stack'. Using stack.new_switch it is straightforward to pass in to the walk function the correct tree to walk. |
@tlively apologies, I've re-read the OP more carefully
Am I correct in understanding that this is a special case of a general impedance involving Wasm's value stack? That is, you effectively want to optimise If I'm getting this correct, I'm not sure that supporting an "easy" version of this optimisation for this special case is worth going against the left-to-right binding convention that is pretty ubiquitous across other languages. Side question - can the "argument flipping" version of this optimisation still be performed in the left-to-right binding scenario, or is it not practical? EDIT: the "abstract setting" above is a little messier than I first thought because |
+1 to making sure we align any partial application for stack switching to what we might do for normal function partial application. In particular, we had |
@conrad-watt, yes, this funkiness is due to the behavior of the value stack. If we have left-to-right binding, then we could still optimize to
We could avoid having to synthesize a new stack type if we allowed the stack type immediate for I just figured it would be nicest if we arranged the types so the rewriting was always valid without any extra complication :) |
@conrad-watt: The argument order question can only be understood if you go significantly closer to what is happening on the machine in terms of register manipulation. It may also be that some of the issues are specific to stack switching: because passing arguments when switching stacks can strongly interfere with how arguments are passed to functions. In a normal use of the switch instruction, there is good reason to expect that the parameters of the switch are prepared in a way that is strongly reminiscent of how arguments are prepared for a function call -- probably in a left-to-right order since that is how wasm prefers it. So, at the machine level, a switch can be implemented as: prepare arguments in registers (if possible) In a perfect world, (i.e., not V8) this involves two additional operations over function call: prepare return stack reference and flip stack pointers. The first takes the place of 'prepare return address' of a normal function call. The net of this is that a stack switch should be executable in the same order of magnitude as a function call (perhaps 1.5x number of instructions). Implementing the register shuffle necessary to keep the overall left-to-right order could double this (it's linear in the number of arguments). (Apologies for the 'wall of text') |
To clarify, @fgmccabe and I are describing completely separate arguments for why we might prefer right-to-left partial application. I was describing how it makes things nicer in the spec and in producer-side optimizers like Binaryen. @fgmccabe is describing how it can make the engine implementation of switching to a newly allocated stack more efficient when |
Since EDIT: or put another way, can the input stack type annotation of
This is an interesting point that I need to think more about. I think I can see the potential benefits at the Wasm->asm level. There are still potentially counterbalancing impedances at the source->Wasm level - for example if a compiler targetting Wasm needs to roll its own "partially-applied function/continuation" abstraction using GC types, but could have avoided this with left-to-right binding, this would seem to outweigh any shuffling cost in the runtime. |
I guess |
Right, but this problem is not specific to the initial resumption of a stack. In our experience, the same situations arise rather frequently with non-initial resumptions when building interesting control abstractions. That is why the continuations proposal added cont.bind, which provides a general and orthogonal mechanism to deal with this. I'm not convinced there is any advantage in entangling partial application with stack creation, which appears both more complex and less useful. |
The `stack.new` instruction binds some argument to the function that will be called on the new stack, leaving the rest to be supplied by `stack.switch`. Previously the arguments bound by `stack.new` were a prefix of the function arguments, but change it so that they are a suffix of the function arguments (besides the return stack reference) instead. This allows us to relax the typing of `stack.new_switch` to be able to use stack types that only send a prefix of the function arguments while maintaining the property that `(stack.new_switch x y)` can be rewritten as `(stack.new x y) (stack.switch x)`. It also gives us the new property that `(stack.new x y) (stack.switch x)` can be rewritten as `(stack.new_switch x y)`; previously that was only true if the parameters of the stack type at `x` matched the parameters of the function `y` or if the stack type had no parameters besides the return stack reference.
Closing this as obsolete, although we might want to revisit this in an updated context. |
Add missing memory specifier to memory.init execution semantics
The
stack.new
instruction binds some argument to the function that will becalled on the new stack, leaving the rest to be supplied by
stack.switch
.Previously the arguments bound by
stack.new
were a prefix of the functionarguments, but change it so that they are a suffix of the function
arguments (besides the return stack reference) instead.
This allows us to relax the typing of
stack.new_switch
to be able to use stacktypes that only send a prefix of the function arguments while maintaining the
property that
(stack.new_switch x y)
can be rewritten as(stack.new x y) (stack.switch x)
. It also gives us the new property that(stack.new x y) (stack.switch x)
can be rewritten as(stack.new_switch x y)
; previouslythat was only true if the parameters of the stack type at
x
matched theparameters of the function
y
or if the stack type had no parameters besidesthe return stack reference.