-
Notifications
You must be signed in to change notification settings - Fork 694
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
On "let" and stack instructions #1381
Comments
I think As for type refinement, the |
Yeah, I spent some time investigating if there were a good way to make the types of locals flow-sensitively refinable, but I came to the conclusion that the stack really is the best place for such reasoning, and hence that there should be better instructions for manipulating/rearranging values on the stack. The one exception was initialization, which I found could be done just as well on the stack as with locals (with labels indicating which potentially uninitialized locals have been initialized), but that did not seem worth the complexity tradeoff given that it still left other flow-sensitive refinement unaddressed. |
Agreed that |
When
which would generalise |
Based on the use cases above, I see there being uses for 4 instructions (forgive the names):
|
a further generalisation (my own apologies for the names) could be something like
where
modulo off-by-one errors. I could also imagine a whole matrix of desirable behaviours "copy the I suppose a problem with these generalisations is that the more immediates are added, the slighter any code size benefit gets. I imagine producers are much more likely to want EDIT: thinking it through I far prefer @RossTate's 4 instructions (or just |
Oh hah, our 24-year-old JSOP_PICK opcode has move, not copy semantics. Probably an indication that "pick" isn't a clear mnemonic ;) I think both copying and moving stack instructions could be valuable. Agreed that move is extra-valuable for affine types; coincidentally that's something we've been discussing recently for interface types. |
A number of discussions on stack instructions (like
dup
) have referencedlet
as a forthcoming alternative. But I think that, evenhaving
let
, there are scenarios where stack instructions have unique advantages, as I discuss below.Weaknesses of
let
I'll start with going over weaknesses.
Size
let
is not a particularly small instruction. It specifies a list of variable types, a list of input types, a list of output types, and requires a matchingend
. That's a lot of infrastructure for simulating something likedup
orswap
.Initialization
The stated motivation for
let
has to do with defining locals for non-defaultable types (particularly non-null types). But let's consider a language like Kotlin that is well-suited to utilize non-null types. Kotlin allows programs like the following:In these programs,
s
is initialized in separate branches, and in Kotlin theString
type guarantees (here) thats
is not null. With flow-sensitive reasoning, it is easy to see thats
is initialized after theif
/else
, and indeed Kotlin recognizes this fact. Butlet
is not flow-sensitive. As such, this program would require three sets oflet
just to do stuff usings
because there are insufficient stack instructions (e.g. no way to use a value on the stack and keep that value around for after theif
/else
). And this program illustrates that initialization is not always so simple. WebAssembly's stack is already able to reason about this program correctly, so adding instructions for better accessing/manipulating the stack seems like a better (i.e. smaller binary and easier to generate) alternative.Type Refinement
Sticking with Kotlin a bit further, Kotlin employs flow-sensitive/occurrence typing, which means that the type of an (immutable) variable can be refined by control flow:
In the above program, because
x.length
occurs inside anif (x is String)
, Kotlin refines its type toString
rather than its original declared typeAny
. You can also write this program in the following manner and it'll type check:The stack can already reason about the correctness of this program. But without stack instructions, we need to use a
let
after the branch just because we usex
twice. Same would be true if we neededx
once but the instructions happened to be in the wrong order.In Kotlin this type refinement is apparent in the surface language, but it happens even more often in low-level systems, especially for dynamic languages. For languages and implementations relying more on flow-sensitive reasoning, it seems to me that they would be better served by having better stack instructions, since the stack is already flow-sensitive, rather than by
let
. (This gets even more problematic when one considers existential types, wherein flow-sensitive reasoning is extremely important and widespread, even for type-checking straight-line code, but I won't delve into that technical topic here.)Affinity
An affine type is one whose values cannot be duplicated. Stack instructions like
swap
respect affinity, and so you can useswap
and the like to shuffle around affine values on the stack. However, you cannot in general do the same withlet
. Withlet
, you can put the value into a local, but if you cannot duplicate the value then how can youget
it? Afterget
, the value is on the stack and in the local—get
fundamentally duplicates values, makinglet
useless for affine types.We ran into this in the proposal for first-class stacks. For reasons I won't repeat here, we recommend using an affine type for stack references. But this meant we could not use
let
and could not write a bunch of basic programs. So we took advantage of a back door: the null value is not affine. This meant that, instead ofget
, we could have an instructionget_clear
that both gets the value in the local and sets the local to null, thereby eliminating the duplication.That workaround was useful for other reasons as well (e.g. globals), but there is an irony in the fact that
let
was introduced for non-nullable types and the only way we could make use oflet
was to make our type nullable. If ever we want to have a non-nullable variant ofstackref
, then we will need a proper set of stack instructions because we will not be able to uselet
.Strengths of
let
This post should not be taken as advocating for removing
let
. It is just meant to point out thatlet
is not a cure-all for stack instructions. I believelet
has a number of uses, one of which is critical. But I do not get the impression that these uses line up with howlet
has been motivated, so I wanted to add them here.Organization
This first strength is the one that I believe people already recognize.
let
is useful for organizing simple code. Many systems do not need the features I mentioned above, and for those systemslet
provides a nice simple model in which surface-level local variables correspond to WebAssembly local variables.Even for systems that do need features above,
let
can be useful for values that are frequently referred to and remain fixed once initialized. One example of this comes from a pattern that will occur in the GC MVP. For technical reasons, the types of inputs to many functions will have to be less precise than they should be, and so inputs will have to first be cast to their expected types before the real body of the function can proceed.let
will enable these inputs to be bound to locals with their more refined types after these casts have occurred. That said, unless we want to have alet
/end
block for each input one by one, we will need stack instructions to perform the relevant casts on each of them before binding them all simultaneously in onelet
/end
block. So stack instructions would be helpful for setting uplet
blocks.Branching
let
has an annotation/validation cost, but it can also provide annotation/validation savings. Within alet
, block types do not need to specify the types of thelet
-bound variables, and similarly branches to those labels do not need to check compatibility with thoselet
-bound variables. So whereas "organization" is really more of a human-readability benefit, this is a real material benefit oflet
.Stack Inspection
Lastly,
let
has a real expressiveness benefit, though only when we get to stack inspection. With stack inspection, ananswer
provides a way to access and mutate the contents of an existing frame on the stack. For that to be sound, we need to know that the stack frame has values of particular types, and that theanswer
does not change the types of those values.let
ensures precisely that, both by providing a way to access a definitely initialized value (i.e.local.get
) and by providing a way to update the stack frame without changing its type (i.e.local.set
).Summary
let
and stack instructions are complementary, not competing, features. While they do overlap, neither subsumes nor conflicts with the other, and where they overlap they each have patterns they are better suited for. My recommendation is to continue advancinglet
, to also address the shortcomings of our current set of stack instructions, and to advance (the short-term portion of) #1379 as it seems to address the primary technical obstacle to adding stack instructions.The text was updated successfully, but these errors were encountered: