See overview for background.
The functionality provided with this first version of GC support for Wasm is intentionally limited in the spirit of a "a minimal viable product" (MVP). As a rough guideline, it includes only essential functionality and avoids features that may provide better performance in some cases, but whose lack can be worked around in a reasonable manner.
In particular, it is expected that compiling to this minimal functionality will require a substantial number of runtime casts that may be eliminated by future extensions. A range of such extensions are discussed in the Post-MVP document. Most of them are expected to be added before GC support can be considered reasonably "complete".
Based on the following proposals:
-
reference types, which introduces reference types
-
typed function references, which introduces typed references
(ref null? $t)etc.
Both proposals are prerequisites.
Heap types classify reference types and are extended. There now are 3 disjoint hierarchies of heap types:
- Internal (values in Wasm representation)
- External (values in a host-specific representation)
- Functions
Heap types extern and func already exist via reference types proposal, and (ref null? $t) via typed references; extern and func are the common supertypes (a.k.a. top) of all external and function types, respectively.
The following additions are made to the hierarchies of heap types:
-
anyis a new heap typeheaptype ::= ... | any- the common supertype (a.k.a. top) of all internal types
-
noneis a new heap typeheaptype ::= ... | none- the common subtype (a.k.a. bottom) of all internal types
-
noexternis a new heap typeheaptype ::= ... | noextern- the common subtype (a.k.a. bottom) of all external types
-
nofuncis a new heap typeheaptype ::= ... | nofunc- the common subtype (a.k.a. bottom) of all function types
-
eqis a new heap typeheaptype ::= ... | eq- the common supertype of all referenceable types on which comparison (
ref.eq) is allowed (this may include host-defined external types)
-
structis a new heap typeheaptype ::= ... | struct- the common supertype of all struct types
-
arrayis a new heap typeheaptype ::= ... | array- the common supertype of all array types
-
i31is a new heap typeheaptype ::= ... | i31- the type of unboxed scalars
We distinguish these abstract heap types from concrete heap types $t that reference actual definitions in the type section.
Most abstract heap types are a supertype of a class of concrete heap types.
Moreover, they form several small subtype hierarchies among themselves.
New abbreviations are introduced for reference types in binary and text format, corresponding to funcref and externref:
-
anyrefis a new reference typeanyref == (ref null any)
-
nullrefis a new reference typenullref == (ref null none)
-
nullexternrefis a new reference typenullexternref == (ref null noextern)
-
nullfuncrefis a new reference typenullfuncref == (ref null nofunc)
-
eqrefis a new reference typeeqref == (ref null eq)
-
structrefis a new reference typestructref == (ref null struct)
-
arrayrefis a new reference typearrayref == (ref null array)
-
i31refis a new reference typei31ref == (ref null i31)
-
deftypeis the syntax for an entry in the type section, generalising the existing syntaxdeftype ::= rec <subtype>*module ::= {..., types vec(<deftype>)}- a
recdefinition defines a group of mutually recursive types that can refer to each other; it thereby defines several type indices at a time - a single type definition, as in Wasm before this proposal, is reinterpreted as a short-hand for a recursive group containing just one type
- Note that the number of type section entries is now the number of recursion groups rather than the number of individual types.
-
subtypeis a new category of type defining a single type, as a subtype of possible other typessubtype ::= sub final? <typeidx>* <comptype>- the preexisting syntax with no
subclause is redefined to be a shorthand for asubclause with emptytypeidxlist:<comptype> == sub final () <comptype> - Note: This allows multiple supertypes. For the MVP, it is restricted to at most one supertype.
-
comptypeis a new category of types covering the different forms of composite typescomptype ::= <functype> | <structtype> | <arraytype>
-
structtypedescribes a structure with statically indexed fieldsstructtype ::= struct <fieldtype>*
-
arraytypedescribes an array with dynamically indexed fieldsarraytype ::= array <fieldtype>
-
fieldtypedescribes a struct or array field and whether it is mutablefieldtype ::= <mutability> <storagetype>storagetype ::= <valtype> | <packedtype>packedtype ::= i8 | i16
TODO: Need to be able to use i31 as a type definition.
Validity of a module is checked under a context storing the definitions for each type. In the case of recursive types, this definition is given by a respective projection from the full type:
ctxtype ::= <deftype>.<i>
Both C.types and C.funcs in typing contexts C as defined by the spec now carry ctxtypes as opposed to functypes like before.
In the case of C.funcs, it is an invariant that all types expand to a function type.
-
Unpacking a storage type yields
i32for packed types, otherwise the type itselfunpacked(t) = tunpacked(pt) = i32
-
Unrolling a possibly recursive context type projects the respective item
unroll($t) = unroll(<ctxtype>)iff$t = <ctxtype>unroll((rec <subtype>*).i) = (<subtype>*)[i]
-
Expanding a type definition unrolls it and returns its plain definition
expand($t) = expand(<ctxtype>)iff$t = <ctxtype>expand(<ctxtype>) = <comptype>- where
unroll(<ctxttype>) = sub final? x* <comptype>
- where
-
Finality of a type just checks the flag
final($t) = final(<ctxtype>)iff$t = <ctxtype>final(<ctxtype>) = final? =/= empty- where
unroll(<ctxttype>) = sub final? x* <comptype>
- where
Unlike in the current spec, external function types need to be represented by a type index/address, in order to preserve the structure and equivalence of iso-recursive types:
externtype ::= func <typeidx> | ...
The type is then looked up and expanded as needed. (This was func <functype> before.)
Some of the rules define a type as ok for a certain index, written ok(x). This controls uses of type indices as supertypes inside a recursive group: the subtype hierarchy must not be cyclic, and hence any type index used for a supertype is required to be smaller than the index x of the current type.
-
a sequence of type definitions is valid if each item is valid within the context containing the prior items
<deftype0> <deftype>* ok- iff
<deftype0> okand extends the context accordingly - and
<deftype>* okunder the extended context
- iff
-
a group of recursive type definitions is valid if its types are valid under the context containing all of them
rec <subtype>* okand extends the context with<ctxtype>*- iff
<subtype>* ok($t)under the extended context(!) - where
$tis the next unused (i.e., current) type index - and
N = |<subtype>*|-1 - and
<ctxtype>* = (rec <subtype>*).0, ..., (rec <subtype>*).N
- iff
-
a sequence of subtype's is valid if each of them is valid for their respective index
<subtype0> <subtype>* ok($t)- iff
<subtype0> ok($t) - and
<subtype>* ok($t+1)
- iff
-
an individual subtype is valid if its definition is valid, matches every supertype, and no supertype is final or has an index higher than its own
sub final? $t* <comptype> ok($t')- iff
<comptype> ok - and
(<comptype> <: expand($t))* - and
(not final($t))* - and
($t < $t')*
- iff
- Note: the upper bound on the supertype indices ensures that subtyping hierarchies are never circular, because definitions need to be ordered.
-
as before, a comptype is valid if all the occurring value types are valid
- specifically, a concrete reference type
(ref $t)is valid when$tis defined in the context
- specifically, a concrete reference type
Example: Consider two mutually recursive types:
(rec
(type $t1 (struct (field i32 (ref $t2))))
(type $t2 (struct (field i64 (ref $t1))))
)
In the context, these will be recorded as:
$t1 = rect1t2.0
$t2 = rect1t2.1
where
rect1t2 = (rec
(struct (field i32 (ref $t2)))
(struct (field i64 (ref $t1)))
)
That is, the types are defined as projections from their respective recursion group, using their relative inner indices 0 and 1.
Type equivalence, written t == t' here, is essentially defined inductively. All rules are simply the canonical congruences, with the exception of the rule for recursive types.
For the purpose of defining recursive type equivalence, type indices are extended with a special form that distinguishes regular from recursive type uses.
rec.<i>is a new form of type indextypeidx ::= ... | rec.<i>
This form is only used during equivalence checking, to identify and represent "back edges" inside a recursive type. It is merely a technical device for formulating the rules and cannot appear in source code. It is introduced by the following auxiliary meta-function:
- Rolling a context type produces an iso-recursive representation of its underlying recursion group
tie($t) = tie_$t(<ctxtype>)iff$t = <ctxtype>tie_$t((rec <subtype>*).i) = (rec <subtype>*).i[$t':=rec.0, ..., $t'+N:=rec.N]iff$t' = $t-iandN = |<subtype>*|-1- Note: This definition assumes that all projections of the recursive type are bound to consecutive type indices, so that
$t-iis the first of them. - Note: If a type is not recursive,
tieis just the identity.
With that:
-
two regular type indices are equivalent if they define equivalent tied context types:
$t == $t'- iff
tie($t) == tie($t')
- iff
-
two recursive type indices are equivalent if they project the same index
rec.i == rec.i'- iff
i = i'
- iff
-
two recursive types are equivalent if they are equivalent pointwise
(rec <subtype>*) == (rec <subtype'>*)- iff
(<subtype> == <subtype'>)*
- iff
- Note: This rule is only used on types that have been tied, which prevents looping.
-
notably, two subtypes are equivalent if their structure is equivalent, they have equivalent supertypes, and their finality flag matches
(sub final1? $t* <comptype>) == (sub final2? $t'* <comptype'>)- iff
<comptype> == <comptype'> - and
($t == $t')* - and
final1? = final2?
- iff
Example: As explained above, the mutually recursive types
(rec
(type $t1 (struct (field i32 (ref $t2))))
(type $t2 (struct (field i64 (ref $t1))))
)
would be recorded in the context as
$t1 = (rec (struct (field i32 (ref $t2))) (struct (field i64 (ref $t1)))).0
$t2 = (rec (struct (field i32 (ref $t2))) (struct (field i64 (ref $t1)))).1
Consequently, if there was an equivalent pair of types,
(rec
(type $u1 (struct (field i32 (ref $u2))))
(type $u2 (struct (field i64 (ref $u1))))
)
recorded in the context as
$u1 = (rec (struct (field i32 (ref $u2))) (struct (field i64 (ref $u1)))).0
$u2 = (rec (struct (field i32 (ref $u2))) (struct (field i64 (ref $u1)))).1
then to check the equivalence $t1 == $u1, both types are tied into iso-recursive types first:
tie($t1) = (rec (struct (field i32 (ref rec.1))) (struct (field i64 (ref rec.0)))).0
tie($u1) = (rec (struct (field i32 (ref rec.1))) (struct (field i64 (ref rec.0)))).0
In this case, it is immediately apparent that these are equivalent types.
Note: In type-theoretic terms, these are higher-kinded iso-recursive types:
tie($t1) ~ (mu a. <(struct (field i32 (ref a.1))), (struct i64 (field (ref a.0)))>).0
tie($t2) ~ (mu a. <(struct (field i32 (ref a.1))), (struct i64 (field (ref a.0)))>).1
where <...> denotes a type tuple. However, in our case, a single syntactic type variable rec is enough for all types, because recursive types cannot nest by construction.
Note 2: This semantics implies that type equivalence checks can be implemented in constant-time by representing all types as trees in tied form and canonicalising them bottom-up in linear time upfront.
Note 3: It's worth noting that the only observable difference to the rules for a nominal type system is the equivalence rule on (non-recursive) type indices: instead of comparing the definitions of their recursive groups, a nominal system would require $t = $t' syntactically (at least as long as we ignore things like checking imports, where type indices become meaningless).
Consequently, using a single big recursion group in this system makes it behave like a nominal system.
In the existing rules, subtyping on type indices required equivalence. Now it can take declared supertypes into account.
- Type indices are subtypes if they either define equivalent types or a suitable (direct or indirect) subtype relation has been declared
$t <: $t'- if
$t = <ctxtype>and$t' = <ctxtype'>and<ctxtype> == <ctxtype'> - or
unroll($t) = sub final? $t1* $t'' $t2* comptypeand$t'' <: $t'
- if
- Note: This rule climbs the supertype hierarchy until an equivalent type has been found. Effectively, this means that subtyping is "nominal" modulo type canonicalisation.
In addition to the existing rules for heap types, the following are added:
-
every internal type is a subtype of
anyt <: any- if
t = any/eq/struct/array/i31ort = $tand$t = <structtype>or$t = <arraytype>
- if
-
every internal type is a supertype of
nonenone <: t- if
t <: any
- if
-
every external type is a subtype of
externt <: extern- if
t = extern
- if
- note: there may be other subtypes of
externin the future
-
every external type is a supertype of
noexternnoextern <: t- if
t <: extern
- if
-
every function type is a subtype of
funct <: func- if
t = funcort = $tand$t = <functype>
- if
-
every function type is a supertype of
nofuncnofunc <: t- if
t <: func
- if
-
structrefis a subtype ofeqrefstruct <: eq- TODO: provide a way to make aggregate types non-eq, especially immutable ones?
-
arrayrefis a subtype ofeqrefarray <: eq
-
i31refis a subtype ofeqrefi31 <: eq
-
Any concrete struct type is a subtype of
struct$t <: struct- if
$t = <structtype>
- if
-
Any concrete array type is a subtype of
array$t <: array- if
$t = <arraytype>
- if
-
Any concrete function type is a subtype of
func$t <: func- if
$t = <functype>
- if
Note: This creates a hierarchy of abstract Wasm heap types that looks as follows.
any extern func
|
eq
/ | \
i31 struct array
The hierarchy consists of several disjoint sub hierarchies, each starting from one of the top heap types any, extern, or func.
All concrete types (of the form $t) are situated below either struct, array, or func.
Not shown in the graph are none, noextern, and nofunc, which are below the other "leaf" types.
A host environment may introduce additional inhabitants of type any
that are are in neither of the above leaf type categories.
The interpretation of such values is defined by the host environment, they are opaque within Wasm code.
Note: In the future, this hierarchy could be refined, e.g., to distinguish aggregate types that are not subtypes of eq.
The subtyping rules for composite types are only invoked during validation of a sub type definition.
-
Function types are covariant on their results and contravariant on their parameters
func <valtype11>* -> <valtype12>* <: func <valtype21>* -> <valtype22>*- iff
(<valtype21> <: <valtype11>)* - and
(<valtype12> <: <valtype22>)*
- iff
-
Structure types support width and depth subtyping
struct <fieldtype1>* <fieldtype1'>* <: struct <fieldtype2>*- iff
(<fieldtype1> <: <fieldtype2>)*
- iff
-
Array types support depth subtyping
array <fieldtype1> <: array <fieldtype2>- iff
<fieldtype1> <: <fieldtype2>
- iff
-
Field types are covariant if they are immutable, invariant otherwise
const <storagetype1> <: const <storagetype2>- iff
<storagetype1> <: <storagetype2>
- iff
var <storagetype> <: var <storagetype>- Note: mutable fields are not subtypes of immutable ones, so
constreally means constant, not read-only
-
Storage types inherent subtyping from value types, packed types must be equivalent
<packedtype> <: <packedtype>
Subtyping is not defined on type definitions.
-
Runtime types (RTTs) are values representing concrete types at runtime. In the MVP, canonical RTTs are implicitly created by all instructions depending on runtime type information. In future versions, RTTs may become explicit values, and non-canonical versions of these instructions will be introduced.
-
An RTT value r1 is equal to another RTT value r2 iff they both represent the same static type.
-
An RTT value r1 is a subtype of another RTT value r2 iff they represent static types that are in a respective subtype relation.
Note: RTT values correspond to type descriptors or "shape" objects as they exist in various engines.
RTT equality can be implemented as a single pointer test by memoising RTT values.
More interestingly, runtime casts along the hierarchy 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).
A subtype check between two RTT values can be implemented as follows using such a representation.
Assume RTT value v1 represents static type $t1 and v2 type $t2.
Let n1 and n2 be the lengths of the respective supertype vectors.
To check whether v1 denotes a subtype 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 ($t1 and n1 are typically unknown during a cast),
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 subtype 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.)
Note: This assumes that there is at most one supertype. For hierarchies with multiple supertypes, more complex tests would be necessary.
Example: Consider three types and corresponding RTTs:
(type $A (sub (struct)))
(type $B (sub $A (struct (field i32))))
(type $C (sub $B (struct (field i32 i64))))
Assume the respective RTTs for types $A, $B, and $C are called $rttA, $rttB, and $rttC.
Then, $rttA would carry supertype vector [$rttA], $rttB has [$rttA, $rttB], and $rttC has [$rttA, $rttB, $rttC].
Now consider a function that casts a $B to a $C:
(func $castBtoC (param $x (ref $B)) (result (ref $C))
(ref.cast (ref $C) (local.get $x))
)
This can compile to machine code that (1) reads the RTT from $x, (2) checks that the length of its supertype table is >= 3, and (3) pointer-compares table[2] against $rttC.
- Reference values of aggregate or function type have an associated runtime type:
- for structures or arrays, it is the RTT value implictly produced upon creation,
- for functions, it is the RTT value for the function's type (which may be recursive).
Note: Instructions not mentioned here remain the same.
In particular, ref.null is typed as before, despite the introduction of none/nofunc/noextern.
ref.eqcompares two references whose types support equalityref.eq : [eqref eqref] -> [i32]
-
struct.new <typeidx>allocates a structure with canonical RTT and initialises its fields with given valuesstruct.new $t : [t'*] -> [(ref $t)]- iff
expand($t) = struct (mut t'')* - and
(t' = unpacked(t''))*
- iff
- this is a constant instruction
-
struct.new_default <typeidx>allocates a structure of type$twith canonical RTT and initialises its fields with default valuesstruct.new_default $t : [] -> [(ref $t)]- iff
expand($t) = struct (mut t')* - and all
t'*are defaultable
- iff
- this is a constant instruction
-
struct.get_<sx>? <typeidx> <fieldidx>reads fieldifrom a structurestruct.get_<sx>? $t i : [(ref null $t)] -> [t]- iff
expand($t) = struct (mut1 t1)^i (mut ti) (mut2 t2)* - and
t = unpacked(ti) - and
_<sx>present ifft =/= ti
- iff
- traps on
null
-
struct.set <typeidx> <fieldidx>writes fieldiof a structurestruct.set $t i : [(ref null $t) ti] -> []- iff
expand($t) = struct (mut1 t1)^i (var ti) (mut2 t2)* - and
t = unpacked(ti)
- iff
- traps on
null
-
array.new <typeidx>allocates an array with canonical RTTarray.new $t : [t' i32] -> [(ref $t)]- iff
expand($t) = array (mut t'') - and
t' = unpacked(t'')
- iff
- this is a constant instruction
-
array.new_default <typeidx>allocates an array with canonical RTT and initialises its fields with the default valuearray.new_default $t : [i32] -> [(ref $t)]- iff
expand($t) = array (mut t') - and
t'is defaultable
- iff
- this is a constant instruction
-
array.new_fixed <typeidx> <N>allocates an array with canonical RTT of fixed size and initialises it from operandsarray.new_fixed $t N : [t^N] -> [(ref $t)]- iff
expand($t) = array (mut t'') - and
t' = unpacked(t'')
- iff
- this is a constant instruction
-
array.new_data <typeidx> <dataidx>allocates an array with canonical RTT and initialises it from a data segmentarray.new_data $t $d : [i32 i32] -> [(ref $t)]- iff
expand($t) = array (mut t') - and
t'is numeric, vector, or packed - and
$dis a defined data segment
- iff
- the 1st operand is the
offsetinto the segment - the 2nd operand is the
sizeof the array - traps if
offset + |t'|*size > len($d) - note: for now, this is not a constant instruction, in order to side-step issues of recursion between binary sections; this restriction will be lifted later
-
array.new_elem <typeidx> <elemidx>allocates an array with canonical RTT and initialises it from an element segmentarray.new_elem $t $e : [i32 i32] -> [(ref $t)]- iff
expand($t) = array (mut t') - and
$e : rt - and
rt <: t'
- iff
- the 1st operand is the
offsetinto the segment - the 2nd operand is the
sizeof the array - traps if
offset + size > len($e) - note: for now, this is not a constant instruction, in order to side-step issues of recursion between binary sections; this restriction will be lifted later
-
array.get_<sx>? <typeidx>reads an element from an arrayarray.get_<sx>? $t : [(ref null $t) i32] -> [t]- iff
expand($t) = array (mut t') - and
t = unpacked(t') - and
_<sx>present ifft =/= t'
- iff
- traps on
nullor if the dynamic index is out of bounds
-
array.set <typeidx>writes an element to an arrayarray.set $t : [(ref null $t) i32 t] -> []- iff
expand($t) = array (var t') - and
t = unpacked(t')
- iff
- traps on
nullor if the dynamic index is out of bounds
-
array.leninquires the length of an arrayarray.len : [(ref null array)] -> [i32]- traps on
null
-
array.fill <typeidx>fills a slice of an array with a given valuearray.fill $t : [(ref null $t) i32 t i32] -> []- iff
expand($t) = array (mut t') - and
t = unpacked(t')
- iff
- the 1st operand is the
arrayto fill - the 2nd operand is the
offsetinto the array at which to begin filling - the 3rd operand is the
valuewith which to fill - the 4th operand is the
sizeof the filled slice - traps if
arrayis null oroffset + size > len(array)
-
array.copy <typeidx> <typeidx>copies a sequence of elements between two arraysarray.copy $t1 $t2 : [(ref null $t1) i32 (ref null $t2) i32 i32] -> []- iff
expand($t1) = array (mut t1) - and
expand($t2) = array (mut? t2) - and
t2 <: t1
- iff
- the 1st operand is the
destarray that will be copied to - the 2nd operand is the
dest_offsetat which the copy will begin indest - the 3rd operand is the
srcarray that will be copied from - the 4th operand is the
src_offsetat which the copy will begin insrc - the 5th operand is the
sizeof the copy - traps if
destis null orsrcis null - traps if
dest_offset + size > len(dest)orsrc_offset + size > len(src) - note:
destandsrcmay be the same array and the source and destination regions may overlap. This must be handled correctly just like it is formemory.copy.
-
array.init_elem <typeidx> <elemidx>copies a sequence of elements from an element segment to an arrayarray.init_elem $t $e : [(ref null $t) i32 i32 i32] -> []- iff
expand($t) = array (mut t) - and
$e : rt - and
rt <: t
- iff
- the 1st operand is the
arrayto be initialized - the 2nd operand is the
dest_offsetat which the copy will begin inarray - the 3rd operand is the
src_offsetat which the copy will begin in$e - the 4th operand is the
sizeof the copy - traps if
arrayis null - traps if
dest_offset + size > len(array)orsrc_offset + size > len($e)
-
array.init_data <typeidx> <dataidx>copies a sequence of values from a data segment to an arrayarray.init_data $t $d : [(ref null $t) i32 i32 i32] -> []- iff
expand($t) = array (mut t) - and
tis numeric, vector, or packed - and
$dis a defined data segment
- iff
- the 1st operand is the
arrayto be initialized - the 2nd operand is the
dest_offsetat which the copy will begin inarray - the 3rd operand is the
src_offsetat which the copy will begin in$d - the 4th operand is the
sizeof the copy in array slots - note: The size of the source region is
size * |t|. Iftis a packed type, the source is interpreted as packed in the same way. - traps if
arrayis null - traps if
dest_offset + size > len(array)orsrc_offset + size * |t| > len($d)
-
ref.i31creates ani31reffrom a 32 bit value, truncating high bitref.i31 : [i32] -> [(ref i31)]- this is a constant instruction
-
i31.get_<sx>extracts the value, zero- or sign-extendingi31.get_<sx> : [(ref null i31)] -> [i32]- traps if the operand is null
-
any.convert_externconverts an external value into the internal representationany.convert_extern : [(ref null1? extern)] -> [(ref null2? any)]- iff
null1? = null2?
- iff
- this is a constant instruction
- note: this succeeds for all values, composing this with
extern.convert_any(in either order) yields the original value
-
extern.convert_anyconverts an internal value into the external representationextern.convert_any : [(ref null1? any)] -> [(ref null2? extern)]- iff
null1? = null2?
- iff
- this is a constant instruction
- note: this succeeds for all values; moreover, composing this with
any.convert_extern(in either order) yields the original value
Casts work for both abstract and concrete types. In the latter case, they test if the operand's RTT is a sub-RTT of the target type.
-
ref.test <reftype>tests whether a reference has a given typeref.test rt : [rt'] -> [i32]- iff
rt <: rt'
- iff
- if
rtcontainsnull, returns 1 for null, otherwise 0
-
ref.cast <reftype>tries to convert a reference to a given typeref.cast rt : [rt'] -> [rt]- iff
rt <: rt'
- iff
- traps if reference is not of requested type
- if
rtcontainsnull, a null operand is passed through, otherwise traps on null - equivalent to
(block $l (param trt) (result rt) (br_on_cast $l rt) (unreachable))
-
br_on_cast <labelidx> <reftype> <reftype>branches if a reference has a given typebr_on_cast $l rt1 rt2 : [t0* rt1] -> [t0* rt1\rt2]- iff
$l : [t0* rt2] - and
rt2 <: rt1
- iff
- passes operand along with branch under target type, plus possible extra args
- if
rt2containsnull, branches on null, otherwise does not
-
br_on_cast_fail <labelidx> <reftype> <reftype>branches if a reference does not have a given typebr_on_cast_fail $l rt1 rt2 : [t0* rt1] -> [t0* rt2]- iff
$l : [t0* rt1\rt2] - and
rt2 <: rt1
- iff
- passes operand along with branch, plus possible extra args
- if
rt2containsnull, does not branch on null, otherwise does
where:
(ref null1? ht1)\(ref null ht2) = (ref ht1)(ref null1? ht1)\(ref ht2) = (ref null1? ht1)
Note: The reference types and typed function referencesalready introduce similar ref.is_null, br_on_null, and br_on_non_null instructions. These can now be interpreted as syntactic sugar:
-
ref.is_nullis equivalent toref.test null ht, wherehtis the suitable bottom type (none,nofunc, ornoextern) -
br_on_nullis equivalent tobr_on_cast null ht, wherehtis the suitable bottom type, except that it does not forward the null value -
br_on_non_nullis equivalent to(br_on_cast_fail null ht) (drop), wherehtis the suitable bottom type -
finally,
ref.as_non_nullis equivalent toref.cast ht, wherehtis the heap type of the operand
In order to allow RTTs to be initialised as globals, the following extensions are made to the definition of constant expressions:
ref.i31is a constant instructionstruct.newandstruct.new_defaultare constant instructionsarray.new,array.new_default, andarray.new_fixedare constant instructions- Note:
array.new_dataandarray.new_elemare not for the time being, see above
- Note:
any.convert_externandextern.convert_anyare constant instructionsglobal.getis a constant instruction and can access preceding (immutable) global definitions, not just imports as in the MVP
This extends the encodings from the typed function references proposal.
| Opcode | Type |
|---|---|
| -0x08 | i8 |
| -0x09 | i16 |
| Opcode | Type | Parameters | Note |
|---|---|---|---|
| -0x0d | nullfuncref |
shorthand | |
| -0x0e | nullexternref |
shorthand | |
| -0x0f | nullref |
shorthand | |
| -0x10 | funcref |
shorthand, from reftype proposal | |
| -0x11 | externref |
shorthand, from reftype proposal | |
| -0x12 | anyref |
shorthand | |
| -0x13 | eqref |
shorthand | |
| -0x14 | i31ref |
shorthand | |
| -0x15 | structref |
shorthand | |
| -0x16 | arrayref |
shorthand | |
| -0x1c | (ref ht) |
ht : heaptype (s33) |
from funcref proposal |
| -0x1d | (ref null ht) |
ht : heaptype (s33) |
from funcref proposal |
The opcode for heap types is encoded as an s33.
| Opcode | Type | Parameters | Note |
|---|---|---|---|
| i >= 0 | (type i) |
from funcref proposal | |
| -0x0d | nofunc |
||
| -0x0e | noextern |
||
| -0x0f | none |
||
| -0x10 | func |
from funcref proposal | |
| -0x11 | extern |
from funcref proposal | |
| -0x12 | any |
||
| -0x13 | eq |
||
| -0x14 | i31 |
||
| -0x15 | struct |
||
| -0x16 | array |
| Opcode | Type | Parameters | Note |
|---|---|---|---|
| -0x20 | func t1* t2* |
t1* : vec(valtype), t2* : vec(valtype) |
from Wasm 1.0 |
| -0x21 | struct ft* |
ft* : vec(fieldtype) |
|
| -0x22 | array ft |
ft : fieldtype |
| Opcode | Type | Parameters | Note |
|---|---|---|---|
| -0x20 | func t1* t2* |
t1* : vec(valtype), t2* : vec(valtype) |
shorthand |
| -0x21 | struct ft* |
ft* : vec(fieldtype) |
shorthand |
| -0x22 | array ft |
ft : fieldtype |
shorthand |
| -0x30 | sub $t* st |
$t* : vec(typeidx), st : comptype |
|
| -0x31 | sub final $t* st |
$t* : vec(typeidx), st : comptype |
| Opcode | Type | Parameters | Note |
|---|---|---|---|
| -0x20 | func t1* t2* |
t1* : vec(valtype), t2* : vec(valtype) |
shorthand |
| -0x21 | struct ft* |
ft* : vec(fieldtype) |
shorthand |
| -0x22 | array ft |
ft : fieldtype |
shorthand |
| -0x30 | sub $t* st |
$t* : vec(typeidx), st : comptype |
shorthand |
| -0x31 | sub final $t* st |
$t* : vec(typeidx), st : comptype |
shorthand |
| -0x32 | rec dt* |
dt* : vec(subtype) |
| Type | Parameters |
|---|---|
field t mut |
t : storagetype, mut : mutability |
| Opcode | Type | Parameters | Note |
|---|---|---|---|
| 0xd0 | ref.null ht |
ht : heap_type |
from Wasm 2.0 |
| 0xd1 | ref.is_null |
from Wasm 2.0 | |
| 0xd2 | ref.func $f |
$f : funcidx |
from Wasm 2.0 |
| 0xd3 | ref.eq |
||
| 0xd4 | ref.as_non_null |
from funcref proposal | |
| 0xd5 | br_on_null $l |
$l : u32 |
from funcref proposal |
| 0xd6 | br_on_non_null $l |
$l : u32 |
from funcref proposal |
| 0xfb00 | struct.new $t |
$t : typeidx |
|
| 0xfb01 | struct.new_default $t |
$t : typeidx |
|
| 0xfb02 | struct.get $t i |
$t : typeidx, i : fieldidx |
|
| 0xfb03 | struct.get_s $t i |
$t : typeidx, i : fieldidx |
|
| 0xfb04 | struct.get_u $t i |
$t : typeidx, i : fieldidx |
|
| 0xfb05 | struct.set $t i |
$t : typeidx, i : fieldidx |
|
| 0xfb06 | array.new $t |
$t : typeidx |
|
| 0xfb07 | array.new_default $t |
$t : typeidx |
|
| 0xfb08 | array.new_fixed $t N |
$t : typeidx, N : u32 |
|
| 0xfb09 | array.new_data $t $d |
$t : typeidx, $d : dataidx |
|
| 0xfb0a | array.new_elem $t $e |
$t : typeidx, $e : elemidx |
|
| 0xfb0b | array.get $t |
$t : typeidx |
|
| 0xfb0c | array.get_s $t |
$t : typeidx |
|
| 0xfb0d | array.get_u $t |
$t : typeidx |
|
| 0xfb0e | array.set $t |
$t : typeidx |
|
| 0xfb0f | array.len |
||
| 0xfb10 | array.fill $t |
$t : typeidx |
|
| 0xfb11 | array.copy $t1 $t2 |
$t1 : typeidx, $t2 : typeidx |
|
| 0xfb12 | array.init_data $t $d |
$t : typeidx, $d : dataidx |
|
| 0xfb13 | array.init_elem $t $e |
$t : typeidx, $e : elemidx |
|
| 0xfb14 | ref.test (ref ht) |
ht : heaptype |
|
| 0xfb15 | ref.test (ref null ht) |
ht : heaptype |
|
| 0xfb16 | ref.cast (ref ht) |
ht : heaptype |
|
| 0xfb17 | ref.cast (ref null ht) |
ht : heaptype |
|
| 0xfb18 | br_on_cast $l (ref null1? ht1) (ref null2? ht2) |
flags : u8, $l : labelidx, ht1 : heaptype, ht2 : heaptype |
|
| 0xfb19 | br_on_cast_fail $l (ref null1? ht1) (ref null2? ht2) |
flags : u8, $l : labelidx, ht1 : heaptype, ht2 : heaptype |
|
| 0xfb1a | any.convert_extern |
||
| 0xfb1b | extern.convert_any |
||
| 0xfb1c | ref.i31 |
||
| 0xfb1d | i31.get_s |
||
| 0xfb1e | i31.get_u |
Flag byte encoding for br_on_cast(_fail)?:
| Bit | Function |
|---|---|
| 0 | null1 present |
| 1 | null2 present |
See GC JS API document.
-
Enable
i31as a type definition. -
Should reference types be generalised to unions, e.g., of the form
(ref null? i31? struct? array? func? extern? $t?)? Perhaps even allowing multiple concrete types? -
Provide a way to make aggregate types non-eq, especially immutable ones?
C(x) = ct
---------
C |- x ok
-----------
C |- i32 ok
C |- x ok
-------------
C |- ref x ok
...and so on.
(C |- t1 ok)*
(C |- t2 ok)*
--------------------
C |- func t1* t2* ok
(C |- ft ok)*
------------------
C |- struct ft* ok
C |- ft ok
----------------
C |- array ft ok
C |- st ok
(C |- st <: expand(C(x)))*
(not final(C(x)))*
(x < x')*
----------------------------
C |- sub final? x* st ok(x')
C |- st ok(x)
C |- st'* ok(x+1)
-------------------
C |- st st'* ok(x)
x = |C| N = |st*|-1
C' = C,(rec st*).0,...,(rec st*).N
C' |- st* ok(x)
-------------------------------------
C |- rec st* -| C'
C |- dt -| C'
C' |- dt'* ok
---------------
C |- dt dt'* ok
expand(C(x)) = func t1* t2*
---------------------------------------
C |- func.call : [t1* (ref x)] -> [t2*]
expand(C(x)) = struct t1^i t t2*
------------------------------------
C |- struct.get i : [(ref x)] -> [t]
...and so on
C |- tie(x) == tie(x')
----------------------
C |- x == x'
--------------------
C |- rec.i == rec.i
---------------
C |- i32 == i32
C |- x == x'
null? = null'?
---------------------------------
C |- ref null? x == ref null'? x'
...and so on.
C |- t == t'
--------------------
C |- mut t == mut t'
(C |- t1 == t1')*
(C |- t2 == t2')*
------------------------------------
C |- func t1* t2* == func t1'* t2'*
(C |- ft == ft')*
----------------------------
C |- struct ft* == struct ft'*
C |- ft == ft'
--------------------------
C |- array ft == array ft'
(C |- x == x')*
C |- st == st'
final1? = final2?
---------------------------------------------
C |- sub final1? x* st == sub final2? x'* st'
C |- x == x'
------------
C |- x <: x'
unroll(C(x)) = sub final? (x1* x'' x2*) st
C |- x'' <: x'
------------------------------------------
C |- x <: x'
---------------
C |- i32 <: i32
C |- x <: x'
null? = epsilon \/ null'? = null
---------------------------------
C |- ref null? x <: ref null'? x'
...and so on.
C |- t == t'
--------------------
C |- mut t <: mut t'
(C |- t1' <: t1)*
(C |- t2 <: t2')*
-----------------------------------
C |- func t1* t2* <: func t1'* t2'*
(C |- ft1 <: ft1')*
-------------------------------------
C |- struct ft1* ft2* <: struct ft1'*
C |- ft <: ft'
--------------------------
C |- array ft <: array ft'