Skip to content

Commit 3ed572f

Browse files
committed
Add optional fixed length to lists
Resolves #181
1 parent 74bd278 commit 3ed572f

File tree

6 files changed

+166
-35
lines changed

6 files changed

+166
-35
lines changed

design/mvp/Binary.md

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ rules, but rather merge the minimal need-to-know elements of both, with just
1212
enough detail to create a prototype. A complete definition of the binary format
1313
and validation will be present in the [formal specification](../../spec/).
1414

15-
See the [explainer introduction](Explainer.md) for an explanation of 🪙.
15+
See [Gated Features](Explainer.md#gated-features) for an explanation of 🪙 and 🔧.
1616

1717

1818
## Component Definitions
@@ -186,6 +186,7 @@ defvaltype ::= pvt:<primvaltype> => pvt
186186
| 0x72 lt*:vec(<labelvaltype>) => (record (field lt)*) (if |lt*| > 0)
187187
| 0x71 case*:vec(<case>) => (variant case+) (if |case*| > 0)
188188
| 0x70 t:<valtype> => (list t)
189+
| 0x67 t:<valtype> len:<u32> => (list t len) 🔧
189190
| 0x6f t*:vec(<valtype>) => (tuple t+) (if |t*| > 0)
190191
| 0x6e l*:vec(<label'>) => (flags l+) (if 0 < |l*| <= 32)
191192
| 0x6d l*:vec(<label'>) => (enum l+) (if |l*| > 0)
@@ -449,6 +450,12 @@ appear once within a `name` section, for example component instances can only be
449450
named once.
450451

451452

453+
## Binary Format Warts to Fix in a 1.0 Release
454+
455+
* The two `list` type codes should be merged into one with an optional immediate.
456+
* The `0x00` prefix byte of `importname'` and `exportname'` will be removed or repurposed.
457+
458+
452459
[`core:byte`]: https://webassembly.github.io/spec/core/binary/values.html#binary-byte
453460
[`core:s16`]: https://webassembly.github.io/spec/core/binary/values.html#integers
454461
[`core:u16`]: https://webassembly.github.io/spec/core/binary/values.html#integers

design/mvp/CanonicalABI.md

Lines changed: 71 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -110,13 +110,23 @@ def alignment(t):
110110
case F32() : return 4
111111
case F64() : return 8
112112
case Char() : return 4
113-
case String() | List(_) : return 4
113+
case String() : return 4
114+
case List(t, l) : return alignment_list(t, l)
114115
case Record(fields) : return alignment_record(fields)
115116
case Variant(cases) : return alignment_variant(cases)
116117
case Flags(labels) : return alignment_flags(labels)
117118
case Own(_) | Borrow(_) : return 4
118119
```
119120

121+
List alignment is the same as tuple alignment when the length is fixed and
122+
otherwise uses the alignment of pointers.
123+
```python
124+
def alignment_list(elem_type, maybe_length):
125+
if maybe_length is not None:
126+
return alignment(elem_type)
127+
return 4
128+
```
129+
120130
Record alignment is tuple alignment, with the definitions split for reuse below:
121131
```python
122132
def alignment_record(fields):
@@ -188,12 +198,18 @@ def elem_size(t):
188198
case F32() : return 4
189199
case F64() : return 8
190200
case Char() : return 4
191-
case String() | List(_) : return 8
201+
case String() : return 8
202+
case List(t, l) : return elem_size_list(t, l)
192203
case Record(fields) : return elem_size_record(fields)
193204
case Variant(cases) : return elem_size_variant(cases)
194205
case Flags(labels) : return elem_size_flags(labels)
195206
case Own(_) | Borrow(_) : return 4
196207

208+
def elem_size_list(elem_type, maybe_length):
209+
if maybe_length is not None:
210+
return maybe_length * size(elem_type)
211+
return 8
212+
197213
def elem_size_record(fields):
198214
s = 0
199215
for f in fields:
@@ -872,7 +888,7 @@ def load(cx, ptr, t):
872888
case F64() : return decode_i64_as_float(load_int(cx, ptr, 8))
873889
case Char() : return convert_i32_to_char(cx, load_int(cx, ptr, 4))
874890
case String() : return load_string(cx, ptr)
875-
case List(t) : return load_list(cx, ptr, t)
891+
case List(t, l) : return load_list(cx, ptr, t, l)
876892
case Record(fields) : return load_record(cx, ptr, fields)
877893
case Variant(cases) : return load_variant(cx, ptr, cases)
878894
case Flags(labels) : return load_flags(cx, ptr, labels)
@@ -992,14 +1008,19 @@ def load_string_from_range(cx, ptr, tagged_code_units):
9921008

9931009
Lists and records are loaded by recursively loading their elements/fields:
9941010
```python
995-
def load_list(cx, ptr, elem_type):
1011+
def load_list(cx, ptr, elem_type, maybe_length):
1012+
if maybe_length is not None:
1013+
return load_list_from_valid_range(cx, ptr, maybe_length, elem_type)
9961014
begin = load_int(cx, ptr, 4)
9971015
length = load_int(cx, ptr + 4, 4)
9981016
return load_list_from_range(cx, begin, length, elem_type)
9991017

10001018
def load_list_from_range(cx, ptr, length, elem_type):
10011019
trap_if(ptr != align_to(ptr, alignment(elem_type)))
10021020
trap_if(ptr + length * elem_size(elem_type) > len(cx.opts.memory))
1021+
return load_list_from_valid_range(cx, ptr, length, elem_type)
1022+
1023+
def load_list_from_valid_range(cx, ptr, length, elem_type):
10031024
a = []
10041025
for i in range(length):
10051026
a.append(load(cx, ptr + i * elem_size(elem_type), elem_type))
@@ -1131,7 +1152,7 @@ def store(cx, v, t, ptr):
11311152
case F64() : store_int(cx, encode_float_as_i64(v), ptr, 8)
11321153
case Char() : store_int(cx, char_to_i32(v), ptr, 4)
11331154
case String() : store_string(cx, v, ptr)
1134-
case List(t) : store_list(cx, v, ptr, t)
1155+
case List(t, l) : store_list(cx, v, ptr, t, l)
11351156
case Record(fields) : store_record(cx, v, ptr, fields)
11361157
case Variant(cases) : store_variant(cx, v, ptr, cases)
11371158
case Flags(labels) : store_flags(cx, v, ptr, labels)
@@ -1420,7 +1441,11 @@ are symmetric to the loading functions. Unlike strings, lists can
14201441
simply allocate based on the up-front knowledge of length and static
14211442
element size.
14221443
```python
1423-
def store_list(cx, v, ptr, elem_type):
1444+
def store_list(cx, v, ptr, elem_type, maybe_length):
1445+
if maybe_length is not None:
1446+
assert(maybe_length == len(v))
1447+
store_list_into_valid_range(cx, v, ptr, elem_type)
1448+
return
14241449
begin, length = store_list_into_range(cx, v, elem_type)
14251450
store_int(cx, begin, ptr, 4)
14261451
store_int(cx, length, ptr + 4, 4)
@@ -1431,9 +1456,12 @@ def store_list_into_range(cx, v, elem_type):
14311456
ptr = cx.opts.realloc(0, 0, alignment(elem_type), byte_length)
14321457
trap_if(ptr != align_to(ptr, alignment(elem_type)))
14331458
trap_if(ptr + byte_length > len(cx.opts.memory))
1459+
store_list_into_valid_range(cx, v, ptr, elem_type)
1460+
return (ptr, len(v))
1461+
1462+
def store_list_into_valid_range(cx, v, ptr, elem_type):
14341463
for i,e in enumerate(v):
14351464
store(cx, e, elem_type, ptr + i * elem_size(elem_type))
1436-
return (ptr, len(v))
14371465

14381466
def store_record(cx, v, ptr, fields):
14391467
for f in fields:
@@ -1587,13 +1615,23 @@ def flatten_type(t):
15871615
case F32() : return ['f32']
15881616
case F64() : return ['f64']
15891617
case Char() : return ['i32']
1590-
case String() | List(_) : return ['i32', 'i32']
1618+
case String() : return ['i32', 'i32']
1619+
case List(t, l) : return flatten_list(t, l)
15911620
case Record(fields) : return flatten_record(fields)
15921621
case Variant(cases) : return flatten_variant(cases)
15931622
case Flags(labels) : return ['i32']
15941623
case Own(_) | Borrow(_) : return ['i32']
15951624
```
15961625

1626+
List flattening of a fixed-length list uses the same flattening as a tuple
1627+
(via `flatten_record` below).
1628+
```python
1629+
def flatten_list(elem_type, maybe_length):
1630+
if maybe_length is not None:
1631+
return flatten_type(elem_type) * maybe_length
1632+
return ['i32', 'i32']
1633+
```
1634+
15971635
Record flattening simply flattens each field in sequence.
15981636
```python
15991637
def flatten_record(fields):
@@ -1671,7 +1709,7 @@ def lift_flat(cx, vi, t):
16711709
case F64() : return canonicalize_nan64(vi.next('f64'))
16721710
case Char() : return convert_i32_to_char(cx, vi.next('i32'))
16731711
case String() : return lift_flat_string(cx, vi)
1674-
case List(t) : return lift_flat_list(cx, vi, t)
1712+
case List(t, l) : return lift_flat_list(cx, vi, t, l)
16751713
case Record(fields) : return lift_flat_record(cx, vi, fields)
16761714
case Variant(cases) : return lift_flat_variant(cx, vi, cases)
16771715
case Flags(labels) : return lift_flat_flags(vi, labels)
@@ -1700,17 +1738,23 @@ def lift_flat_signed(vi, core_width, t_width):
17001738
return i
17011739
```
17021740

1703-
The contents of strings and lists are always stored in memory so lifting these
1704-
types is essentially the same as loading them from memory; the only difference
1705-
is that the pointer and length come from `i32` values instead of from linear
1706-
memory:
1741+
The contents of strings and variable-length lists are stored in memory so
1742+
lifting these types is essentially the same as loading them from memory; the
1743+
only difference is that the pointer and length come from `i32` values instead
1744+
of from linear memory. Fixed-length lists are lifted the same way as a
1745+
tuple (via `lift_flat_record` below).
17071746
```python
17081747
def lift_flat_string(cx, vi):
17091748
ptr = vi.next('i32')
17101749
packed_length = vi.next('i32')
17111750
return load_string_from_range(cx, ptr, packed_length)
17121751

1713-
def lift_flat_list(cx, vi, elem_type):
1752+
def lift_flat_list(cx, vi, elem_type, maybe_length):
1753+
if maybe_length is not None:
1754+
a = []
1755+
for i in range(maybe_length):
1756+
a.append(lift_flat(cx, vi, elem_type))
1757+
return a
17141758
ptr = vi.next('i32')
17151759
length = vi.next('i32')
17161760
return load_list_from_range(cx, ptr, length, elem_type)
@@ -1791,7 +1835,7 @@ def lower_flat(cx, v, t):
17911835
case F64() : return [maybe_scramble_nan64(v)]
17921836
case Char() : return [char_to_i32(v)]
17931837
case String() : return lower_flat_string(cx, v)
1794-
case List(t) : return lower_flat_list(cx, v, t)
1838+
case List(t, l) : return lower_flat_list(cx, v, t, l)
17951839
case Record(fields) : return lower_flat_record(cx, v, fields)
17961840
case Variant(cases) : return lower_flat_variant(cx, v, cases)
17971841
case Flags(labels) : return lower_flat_flags(v, labels)
@@ -1811,15 +1855,23 @@ def lower_flat_signed(i, core_bits):
18111855
return [i]
18121856
```
18131857

1814-
Since strings and lists are stored in linear memory, lifting can reuse the
1815-
previous definitions; only the resulting pointers are returned differently
1816-
(as `i32` values instead of as a pair in linear memory):
1858+
Since strings and variable-length lists are stored in linear memory, lifting
1859+
can reuse the previous definitions; only the resulting pointers are returned
1860+
differently (as `i32` values instead of as a pair in linear memory).
1861+
Fixed-length lists are lowered the same way as tuples (via `lower_flat_record`
1862+
below).
18171863
```python
18181864
def lower_flat_string(cx, v):
18191865
ptr, packed_length = store_string_into_range(cx, v)
18201866
return [ptr, packed_length]
18211867

1822-
def lower_flat_list(cx, v, elem_type):
1868+
def lower_flat_list(cx, v, elem_type, maybe_length):
1869+
if maybe_length is not None:
1870+
assert(maybe_length == len(v))
1871+
flat = []
1872+
for e in v:
1873+
flat += lower_flat(cx, e, elem_type)
1874+
return flat
18231875
(ptr, length) = store_list_into_range(cx, v, elem_type)
18241876
return [ptr, length]
18251877
```

design/mvp/Explainer.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ implemented, considered stable and included in a future milestone:
4747
* 🪺: nested namespaces and packages in import/export names
4848
* 🔀: async
4949
* 🧵: threading built-ins
50+
* 🔧: fixed-length lists
5051

5152
(Based on the previous [scoping and layering] proposal to the WebAssembly CG,
5253
this repo merges and supersedes the [module-linking] and [interface-types]
@@ -542,6 +543,7 @@ defvaltype ::= bool
542543
| (record (field "<label>" <valtype>)+)
543544
| (variant (case "<label>" <valtype>?)+)
544545
| (list <valtype>)
546+
| (list <valtype> <u32>) 🔧
545547
| (tuple <valtype>+)
546548
| (flags "<label>"+)
547549
| (enum "<label>"+)
@@ -596,7 +598,7 @@ sets of abstract values:
596598
| `char` | [Unicode Scalar Values] |
597599
| `record` | heterogeneous [tuples] of named values |
598600
| `variant` | heterogeneous [tagged unions] of named values |
599-
| `list` | homogeneous, variable-length [sequences] of values |
601+
| `list` | homogeneous, variable- or fixed-length [sequences] of values |
600602
| `own` | a unique, opaque address of a resource that will be destroyed when this value is dropped |
601603
| `borrow` | an opaque address of a resource that must be dropped before the current export call returns |
602604

@@ -628,6 +630,10 @@ component-level there is a `bool` type with `true` and `false` values.
628630
The `record`, `variant`, and `list` types allow for grouping, categorizing,
629631
and sequencing contained values.
630632

633+
🔧 When the optional `<u32>` immediate of the `list` type constructor is present,
634+
the list has a fixed length and the representation of the list in memory is
635+
specialized to this length.
636+
631637
##### Handle types
632638

633639
The `own` and `borrow` value types are both *handle types*. Handles logically

design/mvp/WIT.md

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,11 @@ document, a pseudo-formal [grammar specification][lexical-structure], and
2525
additionally a specification of the [package format][package-format] of a WIT
2626
package suitable for distribution.
2727

28+
See [Gated Features] for an explanation of 🔧.
29+
2830
[IDL]: https://en.wikipedia.org/wiki/Interface_description_language
2931
[components]: https://github.com/webassembly/component-model
32+
[Gated Features]: Explainer.md#gated-features
3033

3134
## Package Names
3235

@@ -1561,6 +1564,9 @@ tuple-list ::= ty
15611564
| ty ',' tuple-list?
15621565
15631566
list ::= 'list' '<' ty '>'
1567+
| 'list' '<' ty ',' uint '>' 🔧
1568+
1569+
uint ::= [1-9][0-9]*
15641570
15651571
option ::= 'option' '<' ty '>'
15661572
@@ -1574,8 +1580,18 @@ The `tuple` type is semantically equivalent to a `record` with numerical fields,
15741580
but it frequently can have language-specific meaning so it's provided as a
15751581
first-class type.
15761582

1577-
Similarly the `option` and `result` types are semantically equivalent to the
1578-
variants:
1583+
🔧 A `list` with a fixed length provides the low-level memory representation of a
1584+
homogeneous `tuple` of the same length, but with the dynamic indexing of a
1585+
list. E.g., the following two functions have the same low-level (Core
1586+
WebAssembly) representation, but will naturally produce different source-level
1587+
bindings:
1588+
1589+
```wit
1590+
get-ipv4-address1: func() -> list<u8, 4>;
1591+
get-ipv4-address2: func() -> tuple<u8, u8, u8, u8>;
1592+
```
1593+
1594+
The `option` and `result` types are semantically equivalent to the variants:
15791595

15801596
```wit
15811597
variant option {

0 commit comments

Comments
 (0)