-
Notifications
You must be signed in to change notification settings - Fork 74
Simplify RTTs and decompose casts #150
Conversation
proposals/gc/MVP.md
Outdated
It may contain additional host-defined types that are neither of the above three leaf type categories. | ||
It may also overlap with some or all of these categories, as would be observable by applying a classification instruction like `ref.is_func` to a value of type `externref`. | ||
The possible outcomes of such an operation hence depend on the host environment. | ||
(For example, in a JavaScript embedding, `externref` could be inhabited by all JS values -- which is a natural choice, because JavaScript is untyped; but some of its values are JS-side representations of Wasm values per the JS API, and those can also be observed as `data` or `func` references. Another possible interpretation could be that `data` is disjoint from `extern`, which would be determined by the coercions allowed by the JS API at the JS/Wasm boudary.) |
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.
This would cause problems for code migration. That is, I imagine we would like it to be easy for a program component written in JS to switch to GC-Wasm (possibly with JS-decorated rtt
s for JS interop). But the suggestion here would make it so that the switch would cause previously accepted coercions (in other program components) to externref
to become invalid. This means a converted module would have to create JS proxies (rather than rtt
decorations) around all of its escaping references in order to ensure backwards compatibility.
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.
You had my attention at
we would like it to be easy for a program component written in JS to switch to GC-Wasm
and I'd like to note that this isn't exactly what we are gravitating towards currently for other reasons, in part because of your earlier comments elsewhere. Rather seems like we are doing a slingshot around Jupiter while still hoping that there'll remain a faint signal of code migration in deep space, so we can point at it and say "look, it's still there". 🪐 🚀 💥
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.
@dcodeIO I have implemented systems with a typed language (similar to GC-Wasm) and an untyped OO language (similar to JS) that interop efficiently and in a manner that supports code migration. One reason I posed this extension to the Call Tags proposal is that we found this mechanism sped up method calls in untyped code when invoked on objects originating from typed code. I have not, however, written up how to add such efficient interop to GC-Wasm and imports/exports because some of the techniques are not compatible with the current MVP (or Post-MVP). So I share the goal you have in mind, but I am trying to achieve it by first getting the right foundations in place for supporting efficient interop.
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.
I see, thanks for doing that! Whenever you have something of interest to me in this domain, feel free to ping me :)
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.
But the suggestion here would make it so that the switch would cause previously accepted coercions (in other program components) to externref to become invalid.
There are no coercions to externref, so I don't think something like that can happen.
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.
Also to clarify; if a JS value containing a Wasm GC struct or array reference is passed to a Wasm function that defines an argument as type externref
, what happens? Does it trap now?
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.
Note that it makes a lot of sense to use externref
for JS objects, since you might have to call methods and access properties on them and this is a JS-specific interface that requires importing helper functions like Reflect.get
, Reflect.construct
, and Reflect.apply
from JavaScript, which will accept JavaScript values and so be imported using externref
as their arguments and return values. There's little benefit I can imagine to choosing anyref
over externref
there.
If your API doesn't involve properties or prototype-bound methods on the JS objects, but only involves passing the objects as arguments to functions which you can import in Wasm, then maybe you wouldn't want to make it externref
.
Or am I missing something and there's some other way to access regular JavaScript objects with properties and prototype-bound methods that doesn't involve calling out to JavaScript from Wasm?
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.
- If I should always use anyref, why does externref exist?
Externref is a wart that only came to be because we removed subtyping from the reference-types proposal at the last minute and thus anyref had to go.
- How do you reference JavaScript duck typing behavior in a type import?
Sorry, I don't understand the question. There is no duck typing in Wasm.
if a JS value containing a Wasm GC struct or array reference is passed to a Wasm function that defines an argument as type externref, what happens? Does it trap now?
The most likely interpretation would be that it is "coerced" in some form.
Note that it makes a lot of sense to use externref for JS objects, since you might have to call methods and access properties on them and this is a JS-specific interface that requires importing helper functions like Reflect.get, Reflect.construct, and Reflect.apply from JavaScript, which will accept JavaScript values and so be imported using externref as their arguments and return values. There's little benefit I can imagine to choosing anyref over externref there.
All this works just fine with anyref. However, if you really want to explicitly use JS objects in such an interface, then you are indeed not interested in abstraction or the ability to transition them to Wasm objects instead. Then externref seems fine, and datarefs being disjoint would not be relevant.
According to the current JS API for the MVP...
If you mean the GC MVP, then under a hypothetical design where dataref and externref were separate, it obviously would have to be adjusted accordingly.
Folks, before we waste more energy on this: please keep in mind that this parenthesis was meant as an illustrative remark. I am not actually proposing to do it that way. It merely illustrates the design space that exists for hosts to define the meaning of externref. I realise that JS is not the greatest example to use, but it is the one that everybody knows. I added a clarifying sentence to point that out and (hopefully) avoid possible confusion.
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.
All this works just fine with anyref. However, if you really want to explicitly use JS objects in such an interface, then you are indeed not interested in abstraction or the ability to transition them to Wasm objects instead. Then externref seems fine, and datarefs being disjoint would not be relevant.
The scenario is that we have two existing JavaScript modules which both provide JavaScript APIs that interact with each other.
So yes, the APIs need to remain JavaScript-compatible when implementations are changed from JavaScript to Wasm.
That is why there are questions about how to implement that kind of code migration behavior, since "code migration" is a significant goal.
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.
I should clarify that the APIs already exist in this scenario! They were not designed with anything Wasm in mind; they are JS code that already exists.
proposals/gc/MVP.md
Outdated
RTT-based casts can only be performed with respect to concrete types, and require a data or function reference as input, which are known to carry an RTT. | ||
|
||
* `ref.test <typeidx>` tests whether a reference value's [runtime type](#values) is a [runtime subtype](#runtime) of a given RTT | ||
- `ref.test $t : [(ref null ht) (rtt n $t)] -> [i32]` |
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.
I think this should be (rtt n? $t)
, i.e. the depth n
is optional.
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.
Yup, fixed here and below.
proposals/gc/MVP.md
Outdated
|
||
* `rtt.canon` is a constant instruction | ||
* `rtt.sub` is a constant instruction | ||
* `global.get` is a constant instruction and can access preceding global definitions, not just imports as in the MVP |
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.
Preceding immutable global definitions.
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.
Yeah, changed for conservativeness's sake. Though I wonder if there is a hard reason for restricting it, given that no global can actually be mutated yet in that phase?
- For `i31ref` references it is the RTT value for `i31ref`. | ||
* Reference values of data or function type have an associated runtime type: | ||
- for structures or arrays, it is the RTT value provided upon creation, | ||
- for functions, it is the RTT value for the function's type. |
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.
Does this mean that functions from the function references proposal would implicitly have (rtt.canon $t)
? How would one specify another RTT for a function?
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.
Yes, for backwards compatibility, functions have a canonical RTT. There was a brief discussion somewhere about a mechanism for picking custom RTTs for functions, but I can't find it anymore. Added a short section to Post-MVP.md. Do you see an immediate use case for it?
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.
I am more worried of needing to eagerly compute rtt.canon
for a function type in func.ref
, because keeping around enough metadata to lazily compute it might be even worse.
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.
Hm, at least uses of ref.func
need to be pre-declared, so you don't have to that for functions only used second-class. But for the others, what alternative would you suggest? I don't know how we could avoid that first-class functions carry their type information somehow, if we want to allow casts on them.
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.
I know, I don't have a good answer right now, other than a vague feeling that func.ref
and func.bind
are analogous to allocations that would need explicit RTT values.
* `ref.cast <heaptype1> <heaptype2>` casts a reference value down to a type given by a RTT representation | ||
- `ref.cast t1 t2 : [(ref null t1) (rtt n t2)] -> [(ref t2)]` | ||
- iff `t2 <: t1` | ||
* `ref.cast <typeidx>` casts a reference value down to a type given by a RTT representation |
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.
What about adding the (optional) depth of the RTT as an immediate here? Since there are now two kinds of RTT checks, one where the depth is known and one where the depths are unknown, which kind of check it is depends on the type of the incoming RTT value. An interpreter doesn't know that statically, so it needs to inspect the RTT value to know whether it is a "depth carrying" RTT or a "depth independent" RTT.
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.
Yeah, this is part of a wider discussion. In the light of recent discussions, it seems like we rather might want to remove the immediate altogether in cases like this. So I suggest leaving as is until we have resolved that discussion.
For an interpreter, I would assume that it simply ignores the static depth altogether and just reads it off the RTT value uniformly. The win of using the static value may already be tiny for a compiler, I doubt that it matters in an interpreter.
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.
I think you would get different results because isn't an RTT with depth a subtype of RTT without? And thus the static type and dynamic type of the RTT may be different, so an interpreter could not know the difference by inspecting the value alone.
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.
Yes, (rtt n $t) <: (rtt $t)
, see the definition of subtyping above. This is so that you can abstract from the depth where you don't want to expose or depend on it. It can't result in a different value, though. It's just the length of the supertype vector in the value, so presumably needs to be derivable from the value representation anyway.
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.
I agree. What I mean is that a cast that uses an RTT of static type (rtt n $t)
checks only that the RTT at index n
in the supertype vector, whereas a cast with RTT of static type (rtt $t)
checks all entries in the supertype vector. Because of subtyping on RTT values, an RTT value allocated as (rtt n $t)
is a subtype of (rtt $t)
and can end up at cast site of static type (rtt $t)
, so it is not possible to distinguish the two cases based on the RTT value's dynamic type alone.
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.
whereas a cast with RTT of static type (rtt $t) checks all entries in the supertype vector.
Hm, that's not the intention. Why should it have to do that? The only difference should be how it determines the slot index, everything else should be the same.
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.
The difference is that using the types (of RTTs) inferred from validation, a JIT would use the slot index inferred from the RTT's static type and emit a check only of that slot index, and not a search, whereas an interpreter would not have this information from the inferred RTT type, as it only has the RTT values.
Thus I think it's necessary to have the optional depth here, otherwise you force JITs to emit a search too.
(global $rttA (rtt 1 $A) (rtt.sub $A (rtt.canon any))) | ||
(global $rttB (rtt 2 $B) (rtt.sub $B (global.get $rttA))) | ||
(global $rttC (rtt 3 $C) (rtt.sub $C (global.get $rttB))) | ||
(global $rttA (rtt 0 $A) (rtt.canon $A)) |
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.
Is it possible to create a depth-0 RTT without using rtt.canon
? Otherwise, e.g. in a class-based language, two disjoint roots of different class hierarchies that just happen to be empty will get lumped together by using rtt.canon
. They'd have to declare a superfluous empty struct to use as their parent to get different hierarchies started.
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.
Both rtt.canon
and rtt.sub
are structural, so that's the case either way. See the TODO on L.364 about adding generative RTTs, which would address the use case you describe -- I plan to address that in a separate PR.
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.
Ok, well I misunderstood then. I was under the impression that rtt.sub
was generative--i.e. it would generate a new RTT value each time. If RTT's values are always structural, they are effectively useless for encoding a nominal class hierarchy. That's pretty much a deal breaker unless I am missing something else.
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.
Thus the TODO. Though let me point out the obvious: even with generative RTTs added, Wasm-level RTTs and casts will never be sufficient to directly implement the majority of source-language nominal hierarchies and casts -- for the same reason that Wasm's static type can never encode all source type systems. You can piggy-back source casts on Wasm casts in some limited cases, but don't expect that to be possible in every case. In the general case, you have to implement your own tagging system in user space. (And that makes me wonder whether generative RTTs are even worth adding to the MVP.)
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.
I'd argue that generative RTT values are actually cheaper and simpler to implement in an engine than structural RTT values, and I can verify that they are enough to implement the ubiquitous class casts in Java, i.e. a wasm-level cast implies a Java-level cast, even, and perhaps especially, with classloaders. (They are not sufficient for interface or array casts, however). Without generative RTTs a Java implementation is forced into doing both a wasm-level cast and a Java-level check, because with structural RTTs there is no way to make a wasm-level cast imply a Java-level check.
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.
Agreed, but let's keep generative RTTs for a different PR. This one does not change that aspect, so should be orthogonal.
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.
Ok.
@titzer, @jakobkummerow, any further thoughts on this change? |
@titzer, @jakobkummerow, ping. |
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.
I'm fine with these changes. I have a few comments. (Also, sorry for the delay.)
I'd like to point out that approving these changes to RTTs does not mean that I would be opposed to simplifying things even more by dropping RTTs entirely and folding them into the static types. That discussion is still ongoing, elsewhere.
Note to observers: approving these changes does not constitute a commitment to any timeline for updating the V8-based prototype accordingly. If you have an opinion on that (e.g., you're eagerly awaiting these changes, or on the contrary, you'd prefer if the prototype's behavior didn't change for a while), please let me know.
proposals/gc/MVP.md
Outdated
It may contain additional host-defined types that are neither of the above three leaf type categories. | ||
It may also overlap with some or all of these categories, as would be observable by applying a classification instruction like `ref.is_func` to a value of type `externref`. | ||
The possible outcomes of such an operation hence depend on the host environment. | ||
(For example, in a JavaScript embedding, `externref` could be inhabited by all JS values -- which is a natural choice, because JavaScript is untyped; but some of its values are JS-side representations of Wasm values per the JS API, and those can also be observed as `data` or `func` references. Another possible interpretation could be that `data` is disjoint from `extern`, which would be determined by the coercions allowed by the JS API at the JS/Wasm boudary. While such an interpretation is probably not attractive for JavaScript, it would be natural in other embeddings such as the C/C++ API, where different references are represented with different host types.) |
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.
nit: s/boudary/boundary/
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.
Done.
proposals/gc/MVP.md
Outdated
|
||
* At the same time, runtime subtyping forms a linear hierarchy such that the relation can be checked efficiently using standard implementation techniques (the runtime subtype hierarchy is a tree-shaped graph). | ||
|
||
Note: RTT values correspond to type descriptors or "shape" objects as they exist in various engines. Moreover, runtime casts along the hierachy encoded in these values can be implemented in an engine efficiently by including a vector of its (direct and indirect) super-RTTs in each RTT value (with itself as the last entry). The value `<n>` then denotes the length of this vector. A subtype check between two RTT values can be implemented as follows using such a representation. Assume RTT value v1 has static type `(rtt n1 t1)` and v2 has type `(rtt n2 t2)`. To check whether v1 denotes a sub-RTT of v2, first verify that `n1 >= n2`. Then compare v2 to the n2-th entry in v1's supertype vector. If they are equal, v1 is a sub-RTT. For casts, the static type of v1 (taken from the object to cast) is not known at compile time, so `n1 >= n2` becomes a dynamic check as well. | ||
Note: RTT values correspond to type descriptors or "shape" objects as they exist in various engines. Moreover, runtime casts along the hierachy encoded in these values can be implemented in an engine efficiently by using well-known techniques such as including a vector of its (direct and indirect) super-RTTs in each RTT value (with itself as the last entry). The value `<n>` then denotes the length of this vector. A subtype check between two RTT values can be implemented as follows using such a representation. Assume RTT value v1 has static type `(rtt n1? $t1)` and v2 has type `(rtt n2? $t2)`. To check whether v1 denotes a sub-RTT of v2, first verify that `n1 >= n2` -- if both `n1` and `n2` are known statically, this can be performed at compile time; if either is not statically known, it has to be read from the respective RTT value dynamically, and `n1 >= n2` becomes a dynamic check. Then compare v2 to the n2-th entry in v1's supertype vector. If they are equal, v1 is a sub-RTT. | ||
In the case of actual casts, the static type of RTT v1 (taken from the to cast) is not known at compile time, so `n1` is dynamic as well. |
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.
"taken from the to cast"? Missing "object" as in the old version?
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.
Fixed.
proposals/gc/MVP.md
Outdated
Note: RTT values correspond to type descriptors or "shape" objects as they exist in various engines. Moreover, runtime casts along the hierachy encoded in these values can be implemented in an engine efficiently by including a vector of its (direct and indirect) super-RTTs in each RTT value (with itself as the last entry). The value `<n>` then denotes the length of this vector. A subtype check between two RTT values can be implemented as follows using such a representation. Assume RTT value v1 has static type `(rtt n1 t1)` and v2 has type `(rtt n2 t2)`. To check whether v1 denotes a sub-RTT of v2, first verify that `n1 >= n2`. Then compare v2 to the n2-th entry in v1's supertype vector. If they are equal, v1 is a sub-RTT. For casts, the static type of v1 (taken from the object to cast) is not known at compile time, so `n1 >= n2` becomes a dynamic check as well. | ||
Note: RTT values correspond to type descriptors or "shape" objects as they exist in various engines. Moreover, runtime casts along the hierachy encoded in these values can be implemented in an engine efficiently by using well-known techniques such as including a vector of its (direct and indirect) super-RTTs in each RTT value (with itself as the last entry). The value `<n>` then denotes the length of this vector. A subtype check between two RTT values can be implemented as follows using such a representation. Assume RTT value v1 has static type `(rtt n1? $t1)` and v2 has type `(rtt n2? $t2)`. To check whether v1 denotes a sub-RTT of v2, first verify that `n1 >= n2` -- if both `n1` and `n2` are known statically, this can be performed at compile time; if either is not statically known, it has to be read from the respective RTT value dynamically, and `n1 >= n2` becomes a dynamic check. Then compare v2 to the n2-th entry in v1's supertype vector. If they are equal, v1 is a sub-RTT. | ||
In the case of actual casts, the static type of RTT v1 (taken from the to cast) is not known at compile time, so `n1` is dynamic as well. | ||
(Note that `$t2` and `$t2` are not relevant for the dynamic semantics, but merely for validation.) |
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.
s/$t2
and $t2
/$t1
and $t2
/?
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.
Fixed.
|
||
* The so-defined runtime type is the only type information that can be discovered about a reference value at runtime; a structure or array with RTT `any` thereby is fully opaque to runtime type checks (and an implementation may choose to optimize away its RTT). | ||
* Note: as a future extension, we could allow a value's RTT to be a supertype of the value's actual type. For example, a structure or array with RTT `any` would become fully opaque to runtime type checks, and an implementation may choose to optimize away its RTT. |
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.
AFAIR, we dropped this earlier idea because it would cause implicit RTT allocations, in contrast to "All RTTs are explicitly created" (line 184 above).
Another way to accomplish this behavior would be to introduce a generative "rtt.fresh
" instruction. Code can then create a non-deduplicated RTT for local (allocation) purposes, and by simply not making this RTT value available to other code, ensure that it cannot be used to downcast objects elsewhere.
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.
What we dropped was building this into the new
instruction. But we also considered the possibility of creating such super-RTTs explicitly, with a separate instruction, which would not have that problem -- the above intentionally doesn't say how the RTT is produced. A generative type would also work.
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.
I am confused by the comment as written. I thought the point of this PR was to eliminate the possibility of having an RTT for any, but you are suggesting adding it back in a future extension?
proposals/gc/MVP.md
Outdated
|
||
Note: There are no instructions to check for `externref`, since that can consist of a diverse set of different object representations that would be costly to check for exhaustively. | ||
|
||
TODO: Should we add `br_on_null` for completeness? |
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.
The "function-references" proposal already does this: https://github.com/WebAssembly/function-references/blob/master/proposals/function-references/Overview.md#optional-references
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.
Ah, right. :) Added comment with links for ref.is_null and br_on_null.
proposals/gc/MVP.md
Outdated
|
||
* `rtt.sub <n> <heaptype1> <heaptype2>` returns an RTT for `heaptype2` as a sub-RTT of a the parent RTT operand for `heaptype1` | ||
- `rtt.sub n t1 t2 : [(rtt n t1)] -> [(rtt (n+1) t2)]` | ||
* `rtt.sub <n> <typeidx1> <typeidx2>` returns an RTT for `typeidx2` as a sub-RTT of a the parent RTT operand for `typeidx1` |
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.
For the record, I think both the <n>
and <typeidx1>
immediates should be dropped, in the interest of reducing module binary size. Doesn't need to happen in this PR, but doing it here would be consistent with the changes to ref.test and ref.cast below.
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.
Fair enough, I included that change. Especially since the example already assumes it. :)
proposals/gc/MVP.md
Outdated
- only these types would be castable | ||
* Enable `i31` as a type definition. | ||
|
||
* Add `br_on_null`? |
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.
See above, exists already.
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.
Removed.
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.
Thanks for the comments!
proposals/gc/MVP.md
Outdated
It may contain additional host-defined types that are neither of the above three leaf type categories. | ||
It may also overlap with some or all of these categories, as would be observable by applying a classification instruction like `ref.is_func` to a value of type `externref`. | ||
The possible outcomes of such an operation hence depend on the host environment. | ||
(For example, in a JavaScript embedding, `externref` could be inhabited by all JS values -- which is a natural choice, because JavaScript is untyped; but some of its values are JS-side representations of Wasm values per the JS API, and those can also be observed as `data` or `func` references. Another possible interpretation could be that `data` is disjoint from `extern`, which would be determined by the coercions allowed by the JS API at the JS/Wasm boudary. While such an interpretation is probably not attractive for JavaScript, it would be natural in other embeddings such as the C/C++ API, where different references are represented with different host types.) |
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.
Done.
proposals/gc/MVP.md
Outdated
|
||
* At the same time, runtime subtyping forms a linear hierarchy such that the relation can be checked efficiently using standard implementation techniques (the runtime subtype hierarchy is a tree-shaped graph). | ||
|
||
Note: RTT values correspond to type descriptors or "shape" objects as they exist in various engines. Moreover, runtime casts along the hierachy encoded in these values can be implemented in an engine efficiently by including a vector of its (direct and indirect) super-RTTs in each RTT value (with itself as the last entry). The value `<n>` then denotes the length of this vector. A subtype check between two RTT values can be implemented as follows using such a representation. Assume RTT value v1 has static type `(rtt n1 t1)` and v2 has type `(rtt n2 t2)`. To check whether v1 denotes a sub-RTT of v2, first verify that `n1 >= n2`. Then compare v2 to the n2-th entry in v1's supertype vector. If they are equal, v1 is a sub-RTT. For casts, the static type of v1 (taken from the object to cast) is not known at compile time, so `n1 >= n2` becomes a dynamic check as well. | ||
Note: RTT values correspond to type descriptors or "shape" objects as they exist in various engines. Moreover, runtime casts along the hierachy encoded in these values can be implemented in an engine efficiently by using well-known techniques such as including a vector of its (direct and indirect) super-RTTs in each RTT value (with itself as the last entry). The value `<n>` then denotes the length of this vector. A subtype check between two RTT values can be implemented as follows using such a representation. Assume RTT value v1 has static type `(rtt n1? $t1)` and v2 has type `(rtt n2? $t2)`. To check whether v1 denotes a sub-RTT of v2, first verify that `n1 >= n2` -- if both `n1` and `n2` are known statically, this can be performed at compile time; if either is not statically known, it has to be read from the respective RTT value dynamically, and `n1 >= n2` becomes a dynamic check. Then compare v2 to the n2-th entry in v1's supertype vector. If they are equal, v1 is a sub-RTT. | ||
In the case of actual casts, the static type of RTT v1 (taken from the to cast) is not known at compile time, so `n1` is dynamic as well. |
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.
Fixed.
proposals/gc/MVP.md
Outdated
Note: RTT values correspond to type descriptors or "shape" objects as they exist in various engines. Moreover, runtime casts along the hierachy encoded in these values can be implemented in an engine efficiently by including a vector of its (direct and indirect) super-RTTs in each RTT value (with itself as the last entry). The value `<n>` then denotes the length of this vector. A subtype check between two RTT values can be implemented as follows using such a representation. Assume RTT value v1 has static type `(rtt n1 t1)` and v2 has type `(rtt n2 t2)`. To check whether v1 denotes a sub-RTT of v2, first verify that `n1 >= n2`. Then compare v2 to the n2-th entry in v1's supertype vector. If they are equal, v1 is a sub-RTT. For casts, the static type of v1 (taken from the object to cast) is not known at compile time, so `n1 >= n2` becomes a dynamic check as well. | ||
Note: RTT values correspond to type descriptors or "shape" objects as they exist in various engines. Moreover, runtime casts along the hierachy encoded in these values can be implemented in an engine efficiently by using well-known techniques such as including a vector of its (direct and indirect) super-RTTs in each RTT value (with itself as the last entry). The value `<n>` then denotes the length of this vector. A subtype check between two RTT values can be implemented as follows using such a representation. Assume RTT value v1 has static type `(rtt n1? $t1)` and v2 has type `(rtt n2? $t2)`. To check whether v1 denotes a sub-RTT of v2, first verify that `n1 >= n2` -- if both `n1` and `n2` are known statically, this can be performed at compile time; if either is not statically known, it has to be read from the respective RTT value dynamically, and `n1 >= n2` becomes a dynamic check. Then compare v2 to the n2-th entry in v1's supertype vector. If they are equal, v1 is a sub-RTT. | ||
In the case of actual casts, the static type of RTT v1 (taken from the to cast) is not known at compile time, so `n1` is dynamic as well. | ||
(Note that `$t2` and `$t2` are not relevant for the dynamic semantics, but merely for validation.) |
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.
Fixed.
|
||
* The so-defined runtime type is the only type information that can be discovered about a reference value at runtime; a structure or array with RTT `any` thereby is fully opaque to runtime type checks (and an implementation may choose to optimize away its RTT). | ||
* Note: as a future extension, we could allow a value's RTT to be a supertype of the value's actual type. For example, a structure or array with RTT `any` would become fully opaque to runtime type checks, and an implementation may choose to optimize away its RTT. |
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.
What we dropped was building this into the new
instruction. But we also considered the possibility of creating such super-RTTs explicitly, with a separate instruction, which would not have that problem -- the above intentionally doesn't say how the RTT is produced. A generative type would also work.
proposals/gc/MVP.md
Outdated
|
||
Note: There are no instructions to check for `externref`, since that can consist of a diverse set of different object representations that would be costly to check for exhaustively. | ||
|
||
TODO: Should we add `br_on_null` for completeness? |
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.
Ah, right. :) Added comment with links for ref.is_null and br_on_null.
proposals/gc/MVP.md
Outdated
|
||
* `rtt.sub <n> <heaptype1> <heaptype2>` returns an RTT for `heaptype2` as a sub-RTT of a the parent RTT operand for `heaptype1` | ||
- `rtt.sub n t1 t2 : [(rtt n t1)] -> [(rtt (n+1) t2)]` | ||
* `rtt.sub <n> <typeidx1> <typeidx2>` returns an RTT for `typeidx2` as a sub-RTT of a the parent RTT operand for `typeidx1` |
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.
Fair enough, I included that change. Especially since the example already assumes it. :)
proposals/gc/MVP.md
Outdated
- only these types would be castable | ||
* Enable `i31` as a type definition. | ||
|
||
* Add `br_on_null`? |
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.
Removed.
|
||
* All RTTs are explicitly created and all operations involving dynamic type information (like casts) operate on explicit RTT operands. | ||
* All RTTs are explicitly created and all operations involving dynamic type information (like casts) operate on explicit RTT operands. This allows maximum flexibility and custom choices wrt which RTTs to represent a source type. |
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.
I would argue to drop this additional sentence until we can agree on generative RTTs, because canonicalized RTTs do not grant the flexibility for custom choices wrt which RTTs represent a source type.
Note: RTT values correspond to type descriptors or "shape" objects as they exist in various engines. Moreover, runtime casts along the hierachy encoded in these values can be implemented in an engine efficiently by including a vector of its (direct and indirect) super-RTTs in each RTT value (with itself as the last entry). The value `<n>` then denotes the length of this vector. A subtype check between two RTT values can be implemented as follows using such a representation. Assume RTT value v1 has static type `(rtt n1 t1)` and v2 has type `(rtt n2 t2)`. To check whether v1 denotes a sub-RTT of v2, first verify that `n1 >= n2`. Then compare v2 to the n2-th entry in v1's supertype vector. If they are equal, v1 is a sub-RTT. For casts, the static type of v1 (taken from the object to cast) is not known at compile time, so `n1 >= n2` becomes a dynamic check as well. | ||
Note: RTT values correspond to type descriptors or "shape" objects as they exist in various engines. Moreover, runtime casts along the hierachy encoded in these values can be implemented in an engine efficiently by using well-known techniques such as including a vector of its (direct and indirect) super-RTTs in each RTT value (with itself as the last entry). The value `<n>` then denotes the length of this vector. A subtype check between two RTT values can be implemented as follows using such a representation. Assume RTT value v1 has static type `(rtt n1? $t1)` and v2 has type `(rtt n2? $t2)`. To check whether v1 denotes a sub-RTT of v2, first verify that `n1 >= n2` -- if both `n1` and `n2` are known statically, this can be performed at compile time; if either is not statically known, it has to be read from the respective RTT value dynamically, and `n1 >= n2` becomes a dynamic check. Then compare v2 to the n2-th entry in v1's supertype vector. If they are equal, v1 is a sub-RTT. | ||
In the case of actual casts, the static type of RTT v1 (obtained from the value to cast) is not known at compile time, so `n1` is dynamic as well. | ||
(Note that `$t1` and `$t2` are not relevant for the dynamic semantics, but merely for validation.) |
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.
I would reword this last note slightly. I think the main point is that the intention of including $t1 and $t2 is that validation rejects impossible casts.
(global $rttA (rtt 1 $A) (rtt.sub $A (rtt.canon any))) | ||
(global $rttB (rtt 2 $B) (rtt.sub $B (global.get $rttA))) | ||
(global $rttC (rtt 3 $C) (rtt.sub $C (global.get $rttB))) | ||
(global $rttA (rtt 0 $A) (rtt.canon $A)) |
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.
Ok.
|
||
* The so-defined runtime type is the only type information that can be discovered about a reference value at runtime; a structure or array with RTT `any` thereby is fully opaque to runtime type checks (and an implementation may choose to optimize away its RTT). | ||
* Note: as a future extension, we could allow a value's RTT to be a supertype of the value's actual type. For example, a structure or array with RTT `any` would become fully opaque to runtime type checks, and an implementation may choose to optimize away its RTT. |
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.
I am confused by the comment as written. I thought the point of this PR was to eliminate the possibility of having an RTT for any, but you are suggesting adding it back in a future extension?
* `ref.cast <heaptype1> <heaptype2>` casts a reference value down to a type given by a RTT representation | ||
- `ref.cast t1 t2 : [(ref null t1) (rtt n t2)] -> [(ref t2)]` | ||
- iff `t2 <: t1` | ||
* `ref.cast <typeidx>` casts a reference value down to a type given by a RTT representation |
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.
The difference is that using the types (of RTTs) inferred from validation, a JIT would use the slot index inferred from the RTT's static type and emit a check only of that slot index, and not a search, whereas an interpreter would not have this information from the inferred RTT type, as it only has the RTT values.
Thus I think it's necessary to have the optional depth here, otherwise you force JITs to emit a search too.
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.
Two more comments.
@@ -139,33 +179,35 @@ In addition to the [existing rules](https://github.com/WebAssembly/function-refe | |||
|
|||
#### Runtime Types | |||
|
|||
* Runtime types (RTTs) are explicit values representing types at runtime; a value of type `rtt <n> <heaptype>` is a dynamic representative of static type `<heaptype>`. | |||
* Runtime types (RTTs) are explicit values representing concrete types at runtime; a value of type `rtt <n>? <typeidx>` is a dynamic representative of the static type `<typeidx>`. |
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.
This needs an update to the binary encoding section. The decoder needs a way to know whether the <n>
is present or not. One possible solution is to have different encodings for rtt-with-n and rtt-without-n. Another possible solution is to use a sentinel value for absent <n>
, such as -1. I'm sure there are other possible solutions too.
(The binary encoding section should also be updated to reflect the heaptype->typeidx change.)
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.
Ah, indeed. I added opcode -0x18
for rtt without n.
|
||
* `i31.get_u` extracts the value, zero-extending | ||
- `i31.get_u : [i31ref] -> [i32]` | ||
|
||
* `i31.get_s` extracts the value, sign-extending | ||
- `i31.get_s : [i31ref] -> [i32]` | ||
|
||
Perhaps also the following short-hands: | ||
|
||
#### Classification |
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.
Is it intentional that there are no instructions for eqref, i.e. ref.is_eq
/ref.as_eq
/br_on_eq
? Previously it was possible to cast from anyref to eqref, though I'm not sure how relevant that is in practice. In particular, if we end up making i31 an attribute on references (like nullability), then ref.is_data
will effectively subsume ref.is_eq
.
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.
Yes. Eqref isn't really a basic category of values but rather a constraint on anyref. I can't think of a good use case to cast to it, and it doesn't seem like that would be a simple operation to implement either.
proposals/gc/MVP.md
Outdated
@@ -432,6 +432,7 @@ This extends the [encodings](https://github.com/WebAssembly/function-references/ | |||
| -0x15 | `(ref ht)` | `ht : heaptype (s33)` | from funcref proposal | | |||
| -0x16 | `i31ref` | | | | |||
| -0x17 | `(rtt n ht)` | `n : u32`, `ht : heaptype (s33)` | | | |||
| -0x18 | `(rtt ht)` | `ht : heaptype (s33)` | | |
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.
Here and in the line above: s/heaptype (s33)/typeidx (u32)/
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.
Oops, fixed.
@@ -354,7 +431,8 @@ This extends the [encodings](https://github.com/WebAssembly/function-references/ | |||
| -0x14 | `(ref null ht)` | `ht : heaptype (s33)` | from funcref proposal | | |||
| -0x15 | `(ref ht)` | `ht : heaptype (s33)` | from funcref proposal | | |||
| -0x16 | `i31ref` | | | | |||
| -0x17 | `(rtt n ht)` | `n : u32`, `ht : heaptype (s33)` | | | |||
| -0x17 | `(rtt n $t)` | `n : u32`, `$t : typeidx` | | |
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.
Since you're here: the "Heap Types" section right below has a typo in the i31
row (line 448), where it currently says -0x17. It should be consistent with line 433 and say -0x16.
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.
Thanks, fixed.
Following a recent discussion with @jakobkummerow and @tebbi, this PR suggests a simplification to the semantics of RTTs and casts: instead of representing arbitrary heap types, RTTs only denote concrete types, making casts simpler and more uniform.
Previously, an RTT could represent an arbitrary heap type, including abstract types like
anyref
orfuncref
. Such RTTs could also be used as targets of a cast. Consequently, casts were a complex operation that could involve multiple steps, the number and details of which differ by source/target type. For example, to cast fromanyref
to(ref $t)
, where$t
is a struct, the operational semantics ofref.cast
would need to(1) check that the reference isn’t null or a scalar;
(2) check that it points to a struct;
(3) do the actual check on the RTTs involved.
In contrast, a cast from, say,
anyref
to abstractfuncref
involves only the first two of these steps. A cast fromfuncref
to(ref $t)
OTOH only the latter. Consequently, the operational semantics (and costs_ of a cast were dependent on the types involved, and a consumer would need to distinguish various different combinations of cases.This PR decomposes the complex cast operation into simpler, lower-level primitives, and allows moving the case distinction to the producer, as is adequate for an “assembly” language.
Concretely, with this change,
an RTT always denotes a concrete type, i.e., it’s form is simplified from
(rtt <heaptype>)
to(rtt <typeidx>)
;accordingly, casts always target concrete types; moreover, they can only be applied to reference types that are statically known to carry an RTT (structs/arrays and funcs);
instructions like
ref.is_func/as_func
become independent operations, not just shorthands for casts;along these lines, a new abstract type
dataref
is introduced as the common supertype of compound data (structs & arrays), which carry an actual RTT and can be the source of casts, along with respective instructionsref.is_data/as_data
.The net effect is that RTT-based casts are simplified to performing step (3) above, while (1/2) are performed by separate “classification” instructions that distinguish abstract heap types.
The change also clarifies possible questions about the
externref
type, which now is naturally excluded from being the target of a cast.Other changes:
The depth count on RTTs only counts concrete supertypes now, as the abstract ones are no longer relevant for casts; this also makes it agnostic to possible future refinements in the abstract type hierarchy.
Furthermore, the depth count becomes optional, allowing to abstract over the depth of an inheritance chain where desired. When present, the static depth allows for slightly more efficient casts, otherwise it has to be loaded dynamically. (It remains to be seen how much of a performance benefit a static depth actually is. We could drop it entirely if it doesn’t turn out to matter.)
In order to avoid double testing in cases where classifications are expected to be fallible, introduce branching versions of
ref.is/as
instructions.Relax the use of
global.get
in constant expressions (fixes Will the restriction about global initializers be lifted? #140).Define
i31.new
to be a constant expression (fixes Should there be a constant i31ref initializer expression? #141).Add some missing binary encodings.