Skip to content

Commit ca66aad

Browse files
authored
Move exactness to heap types (#18)
Rather than having exactness as a new attribute of reference types, make it a new attribute of heap types. By restricting exact heap types to using type indices, we make it impossible to construct an exact abstract heap type and thereby avoid introducing new uninhabitable types. This change also dramatically simplifies the handling of exact types in existing instructions; existing heap type immediates can now serve to encode exactness rather than needing new encodings for the base instructions.
1 parent 38cad33 commit ca66aad

File tree

1 file changed

+60
-132
lines changed

1 file changed

+60
-132
lines changed

proposals/custom-descriptors/Overview.md

+60-132
Original file line numberDiff line numberDiff line change
@@ -227,7 +227,7 @@ This may be relaxed in the future.
227227
)
228228
```
229229

230-
## Exact Reference Types
230+
## Exact Types
231231

232232
Allocating an instance of a type with a custom descriptor necessarily
233233
requires supplying a custom descriptor value.
@@ -264,59 +264,65 @@ describes precisely the allocated type.
264264
A better solution would be to allow userspace to perform that check if necessary,
265265
but also be able to statically prove via the type system that it is not necessary.
266266

267-
To facilitate that we introduce exact reference types,
268-
which are inhabited by references to a particular heap type (and possibly the null value),
269-
but not any of the heap type's strict subtypes.
267+
To facilitate that we introduce exact heap types,
268+
which are subtypes of their base heap types
269+
but not supertypes of their base heap types' declared subtypes.
270270

271-
Exact reference types can be nullable with the form `(ref null exact ht)`
272-
or non-nullable with the form `(ref exact ht)`.
271+
```
272+
heaptype ::= absheaptype | exact typeidx | typeidx
273+
```
274+
275+
The subtyping rules for heap types are extended:
273276

274-
For any heap type `ht`:
275277

276278
```
277-
(ref ht) <: (ref null ht)
278-
(ref null exact ht) <: (ref null ht)
279-
(ref exact ht) <: (ref ht)
280-
(ref exact ht) <: (ref null exact ht)
279+
C |- (exact typeidx_1) <: typeidx_1
280+
281281
```
282282

283-
Furthermore, to ensure that each type hierarchy remains a lattice,
284-
we have:
283+
Notably, by the existing, unmodified rules for `none`, `nofunc`, etc.
284+
it is the case that e.g. `none <: (exact $some_struct)`.
285+
Given these types:
285286

286287
```
287-
(ref null exact bot) <: (ref null exact ht)
288-
(ref exact bot) <: (ref exact ht)
288+
(type $super (sub (struct)))
289+
(type $sub (sub $super (struct)))
289290
```
290291

291-
where `bot` is the bottom type
292-
(e.g. `none`, `nofunc`, `noextern`, `noexnt`, or `nocont`) of `ht`'s hierarchy.
292+
We have the following relationships:
293+
294+
```
295+
none <: (exact $sub) <: $sub <: $super <: struct <: eq <: any
296+
(exact $super) <: $super <: ...
297+
```
298+
299+
But no version of `$sub` is in a subtyping relation with `(exact $super)`.
300+
293301

294-
All instructions that create reference to a particular defined heap type
302+
All instructions that create references to a particular defined heap type
295303
(e.g. `ref.func`, `struct.new`, `array.new`, etc.)
296-
are refined to produce exact references to that heap type.
297-
In contrast, instructions that produce references to abstract heap types
298-
generally do not produce exact references.
299-
This preserves our ability to assign them more specific types in the future.
300-
For example, `ref.i31` produces `(ref i31)`, not `(ref exact i31)`,
301-
and casting an `i31` reference (or anything else) to `(ref exact i31)` will fail.
302-
The only exception to this rule is that `ref.null ht` is typed `(ref null exact ht)`.
303-
This allows `ref.null none` to be used where a `(ref null exact $Foo)` is expected.
304-
As a general rule, non-nullable exact references to abstract heap types are uninhabited.
304+
are refined to produce references to the exact version of that heap type.
305+
306+
Since only defined types have exact versions,
307+
instructions like `ref.i31` or `any.convert_extern` that produce
308+
references to abstract heap types do not produce references to exact types.
305309

306310
When allocating types with custom descriptors,
307-
`struct.new` and `struct.new_default` take exact references to the descriptors
311+
`struct.new` and `struct.new_default` take references to the exact descriptors
308312
as their last operands.
309313
This makes the unsound program above invalid.
310314

311315
```
312316
struct.new x
313-
C |- struct.new x : t* (ref null exact y) -> (ref exact x)
317+
318+
C |- struct.new x : t* (ref null (exact y)) -> (ref (exact x))
314319
-- C.types[x] ~ descriptor y (struct (field t)*)
315320
```
316321

317322
```
318323
struct.new_default x
319-
C |- struct.new_default x : (ref null exact y) -> (ref exact x)
324+
325+
C |- struct.new_default x : (ref null (exact y)) -> (ref (exact x))
320326
-- C.types[x] ~ descriptor y (struct (field t)*)
321327
-- defaultable(t)*
322328
```
@@ -335,16 +341,16 @@ a reference to the custom descriptor value can be retrieved with `ref.get_desc`.
335341
```
336342
ref.get_desc typeidx
337343
338-
C |- ref.get_desc x : (ref exact_1 null x) -> (ref exact_1 y)
344+
C |- ref.get_desc x : (ref null (exact_1 x)) -> (ref (exact_1 y))
339345
-- C.types[x] ~ descriptor y ct
340346
```
341347

342-
If the provided reference is exact,
348+
If the provided reference is to an exact heap type,
343349
then the type of the custom descriptor is known precisely,
344350
so the result can be exact as well.
345351
Otherwise, the subtyping rules described above ensure that there will be some custom descriptor value
346352
and that it will be a subtype of the custom descriptor type for `x`,
347-
so the result can be a non-null inexact reference.
353+
so the result can be a non-null reference to the inexact descriptor type.
348354

349355
Being able to retrieve a custom descriptor means you can then compare it for equality
350356
with an expected custom descriptor value.
@@ -362,31 +368,31 @@ then the type of the cast output can also be exact.
362368
```
363369
ref.cast_desc reftype
364370
365-
C |- ref.cast_desc rt : (ref null ht) (ref null exact_1 y) -> rt
366-
-- rt = (ref null? exact_1 x)
371+
C |- ref.cast_desc rt : (ref null ht) (ref null (exact_1 y)) -> rt
372+
-- rt = (ref null? (exact_1 x))
367373
-- C |- C.types[x] <: ht
368374
-- C.types[x] ~ descriptor y ct
369375
```
370376

371377
```
372378
br_on_cast_desc labelidx reftype reftype
373379
374-
C |- br_on_cast_desc l rt_1 rt_2 : t* rt_1 (ref null exact_1 y) -> t* (rt_1 \ rt_2)
380+
C |- br_on_cast_desc l rt_1 rt_2 : t* rt_1 (ref null (exact_1 y)) -> t* (rt_1 \ rt_2)
375381
-- C.labels[l] = t* rt
376382
-- C |- rt_2 <: rt
377383
-- C |- rt_2 <: rt_1
378-
-- rt_2 = (ref null? exact_1 x)
384+
-- rt_2 = (ref null? (exact_1 x))
379385
-- C.types[x] ~ descriptor y ct
380386
```
381387

382388
```
383389
br_on_cast_desc_fail labelidx reftype reftype
384390
385-
C |- br_on_cast_desc_fail l rt_1 rt_2 : t* rt_1 (ref null exact_1 y) -> t* rt_2
391+
C |- br_on_cast_desc_fail l rt_1 rt_2 : t* rt_1 (ref null (exact_1 y)) -> t* rt_2
386392
-- C.labels[l] = t* rt
387393
-- C |- rt_1 \ rt_2 <: rt
388394
-- C |- rt_2 <: rt_1
389-
-- rt_2 = (ref null? exact_1 x)
395+
-- rt_2 = (ref null? (exact_1 x))
390396
-- C.types[x] ~ descriptor y ct
391397
```
392398

@@ -476,7 +482,7 @@ WebAssembly.
476482
477483
(elem declare func $counter.get $counter.inc)
478484
479-
(global $counter.vtable (ref exact $counter.vtable)
485+
(global $counter.vtable (ref (exact $counter.vtable))
480486
(struct.new $counter.vtable
481487
(global.get $counter.proto)
482488
(ref.func $counter.get)
@@ -570,111 +576,33 @@ subtype ::=
570576
| ct:sharecomptype => sub final eps ct
571577
```
572578

573-
### Exact Reference Types
579+
### Exact Types
574580

575-
Rather than use two new opcodes in the type opcode space
576-
to represent nullable and non-nullable exact reference types,
577-
we introduce just a single new prefix opcode that encode both:
581+
Exact heap types are introduced with a prefix byte:
578582

579583
```
580-
reftype :: ...
581-
| 0x62 0x64 ht:heaptype => ref exact ht
582-
| 0x62 0x63 ht:heaptype => ref null exact ht
584+
heaptype :: ... | 0x62 x:u32 => exact x
583585
```
584586

585-
To make the most of the existing shorthands for nullable abstract heap types,
586-
we also allow using the exact prefix with those shorthands:
587-
588-
```
589-
reftype :: ...
590-
| 0x62 ht:absheaptype => ref null exact ht
591-
```
592-
593-
Similarly, we allow combining `exact` with the established shorthands in the text format.
594-
For example `(exact anyref)` is a shorthand for `(ref null exact any)`.
587+
Note that the type index being encoded as a `u32` instead of a `u33`
588+
intentionally makes it impossible to encode an exact abstract heap type.
595589

596590
### Instructions
597591

598-
The existing `ref.test`, `ref.cast`, `br_on_cast` and `br_on_cast_fail` instructions
599-
need to be able to work with exact reference types.
600-
`ref.test` and `ref.cast` currently have two opcodes each:
601-
one for nullable target types and one for non-nullable target types.
602-
Rather than introducing two new opcodes for each of these instructions
603-
to allow for nullable exact and non-nullable exact target types,
604-
we introduce one new opcode for each that takes a full reference type
605-
rather than a heap type as its immediate.
592+
All existing instructions that take heap type immediates work without
593+
modification with the encoding of exact heap types.
606594

607-
```
608-
instr ::= ...
609-
| 0xFB 32:u32 rt:reftype => ref.test rt
610-
| 0xFB 33:u32 rt:reftype => ref.cast rt
611-
```
612-
613-
(`0xFB 31` is already used by `ref.i31_shared` in the shared-everything-threads proposal.)
614-
615-
Note that these new encodings can be used instead of the existing encodings
616-
to represent casts to inexact reference types.
617-
618-
> Note: We could alternatively restrict the new encoding to be usable only with exact types,
619-
> but this artificial restriction does not seem useful.
620-
621-
> Note: We could alternatively continue the existing encoding scheme,
622-
> at the cost of using 4 new opcodes instead of 2.
623-
624-
`br_on_cast` and `br_on_cast_fail` already encode the nullability of their
625-
input and output types in a "castflags" u8 immediate. Castflags is extended to encode
626-
exactness as well:
627-
628-
```
629-
castflags ::= ...
630-
| 4:u8 => (exact, eps)
631-
| 5:u8 => (null exact, eps)
632-
| 6:u8 => (exact, null)
633-
| 7:u8 => (null exact, null)
634-
| 8:u8 => (eps, exact)
635-
| 9:u8 => (null, exact)
636-
| 10:u8 => (eps, null exact)
637-
| 11:u8 => (null, null exact)
638-
| 12:u8 => (exact, exact)
639-
| 13:u8 => (null exact, exact)
640-
| 14:u8 => (exact, null exact)
641-
| 15:u8 => (null exact, null exact)
642-
```
643-
644-
Note that the bits now have the following meanings:
645-
646-
```
647-
bit 0: source nullability
648-
bit 1: target nullability
649-
bit 2: source exactness
650-
bit 3: target exactness
651-
```
652-
653-
The other new instructions are encoded as follows:
595+
The new instructions are encoded as follows:
654596

655597
```
656598
instr ::= ...
657599
| 0xFB 34:u32 x:typeidx => ref.get_desc x
658-
| 0xFB 35:u32 rt:reftype => ref.cast_desc reftype
659-
| 0xFB 36:u32 (null_1? exact_1?, null_2? exact_2?):castflags
600+
| 0xFB 35:u32 ht:heaptype => ref.cast_desc (ref ht)
601+
| 0xFB 36:u32 ht:heaptype => ref.cast_desc (ref null ht)
602+
| 0xFB 37:u32 (null_1?, null_2?):castflags
660603
l:labelidx ht_1:heaptype ht_2:heaptype =>
661-
br_on_cast_desc l (ref null_1? exact_1? ht_1) (ref null_2? exact_2? ht_2)
662-
| 0xFB 37:u32 (null_1? exact_1?, null_2? exact_2?):castflags
604+
br_on_cast_desc l (ref null_1? ht_1) (ref null_2? ht_2)
605+
| 0xFB 38:u32 (null_1?, null_2?):castflags
663606
l:labelidx ht_1:heaptype ht_2:heaptype =>
664-
br_on_cast_desc_fail l (ref null_1? exact_1? ht_1) (ref null_2? exact_2? ht_2)
607+
br_on_cast_desc_fail l (ref null_1? ht_1) (ref null_2? ht_2)
665608
```
666-
667-
## Minimal Initial Prototyping for JS Interop
668-
669-
A truly minimal prototype for experimenting with JS interop can skip
670-
implementing most of the new features in this proposal:
671-
672-
- Arbitrary fields on custom descriptors.
673-
A minimal prototype can allow `descriptor` clauses only on empty structs.
674-
- Exact reference types.
675-
A minimal prototype can instead bake an RTT exactness check into the
676-
semantics of `struct.new` and `struct.new_default` to ensure soundness.
677-
- New instructions.
678-
A minimal prototype only needs to update `struct.new` and `struct.new_default`
679-
to take references to custom descriptors as necessary.
680-
It does not need to implement `ref.get_desc` or any of the new casts.

0 commit comments

Comments
 (0)