From f25d544574f19ed8c924eeb7cc47b50139d3f76d Mon Sep 17 00:00:00 2001 From: Brendan Dahl Date: Wed, 17 Jun 2026 01:04:48 +0000 Subject: [PATCH 1/2] Add more details on the proposal. --- README.md | 2 +- proposals/multibyte-array-access/Overview.md | 150 ++++++++++++++++--- 2 files changed, 134 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index 97e5144d..fcfc5b46 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ This repository is a clone of [`WebAssembly/spec`](https://github.com/WebAssembly/spec/). It is meant for discussion, prototype specification, and implementation of a proposal to add -support for reading and writing multiple bytes at a time from `(array i8)`. +support for reading and writing multiple bytes at a time from WebAssembly GC numeric arrays. See the [overview](proposals/multibyte-array-access/Overview.md) for a high-level summary of the proposal. diff --git a/proposals/multibyte-array-access/Overview.md b/proposals/multibyte-array-access/Overview.md index b9f007f6..f9830c05 100644 --- a/proposals/multibyte-array-access/Overview.md +++ b/proposals/multibyte-array-access/Overview.md @@ -2,40 +2,156 @@ ## Summary -Reuse the exiting linear memory load/store instructions to allow multibyte access to `(array i8)`. +Reuse the existing linear memory load/store instructions to allow multibyte access to Wasm GC numeric (and packed) array types (e.g., `i8`, `i16`, `i32`, `i64`, `f32`, `f64`). ## Motivation -A number of languages currently use `(array i8)` as a backing store for custom data types and/or +A number of languages currently use GC arrays (such as `(array i8)` or arrays of other numeric types) as a backing store for custom data types and/or byte buffers style objects (e.g., custom structs, Dart typed arrays, JVM byte arrays). Reading and -writing to these custom data types requires performing sequences of single-byte operations that are +writing to these custom data types requires performing sequences of single-element operations that are highly inefficient, and hinder performance. ## Proposal ### Semantics -The array versions of the load/store instructions will largely follow the same semantics as the -linear memory instructions. The main differences are as follows: - - a type index immediate and array reference argument are required before the address argument - - a memory index or align immediate are not allowed - -Valid array types for the instructions: - - load:`expand($t) = array i8` - - store:`expand($t) = array mut i8` +The array versions of the load/store instructions follow the same data transformation semantics as the linear memory instructions, but they operate on GC arrays instead of linear memories. + +#### Valid Array Types +- **Load instructions**: The array type `$t` must expand to `array (mut? t_elem)` where `t_elem` is a numeric type (`i8`, `i16`, `i32`, `i64`, `f32`, `f64`). +- **Store instructions**: The array type `$t` must expand to `array (mut t_elem)` where `t_elem` is a numeric type (`i8`, `i16`, `i32`, `i64`, `f32`, `f64`). + +#### Validation + +For type index `$t` and memory instruction `t_value.op`: +1. The type `$t` must be a valid type index in the module, and must expand to an array type. +2. The element type of the array must be a numeric type or packed numeric type. +3. If it is a store instruction, the array type must be mutable. +4. The instruction has the following input and output types on the stack: + - **Load** (`t_value.load`): + - Inputs: `[ref: (ref null $t), index: i32]` + - Outputs: `[val: t_value]` + - **Store** (`t_value.store`): + - Inputs: `[ref: (ref null $t), index: i32, val: t_value]` + - Outputs: `[]` + - **Lane Load** (`v128.loadN_lane`): + - Inputs: `[ref: (ref null $t), index: i32, vec: v128]` + - Outputs: `[vec: v128]` + - **Lane Store** (`v128.storeN_lane`): + - Inputs: `[ref: (ref null $t), index: i32, vec: v128]` + - Outputs: `[]` + +#### Execution +1. **Null Validation**: + - If the array reference operand is null, execution traps with a null reference error. +2. **Effective Address Calculation**: + - The effective address $ea$ is calculated as $index + offset$, where $index$ is the `i32` stack operand (interpreted as an unsigned 32-bit integer) and $offset$ is the static byte offset immediate from `memarg`. +3. **Bounds Check**: + - Let $S$ be the size (in bytes) of the memory access (e.g., 4 for `i32.load`, 16 for `v128.load`). + - Let $L$ be the length of the array (obtained via `array.len`). + - Let $E$ be the element size in bytes of the array type `$t` (e.g., 1 for `i8`, 2 for `i16`, etc.). + - The access is in bounds if $ea \ge 0$ and $ea + S \le L \times E$. + - If the access is out of bounds, execution traps with an out-of-bounds array access error. +4. **Operation**: + - **Load**: Reads $S$ bytes from the array payload starting at byte offset $ea$, decodes them using little-endian byte order, and pushes the resulting value onto the stack. + - **Store**: Encodes the value operand into $S$ bytes using little-endian byte order, and writes them to the array payload starting at byte offset $ea$. + - **Lane Operations**: Loads/stores a single lane of the vector operand from/to the array payload at byte offset $ea$. +5. **Alignment**: + - The alignment value (expressed as power of 2 exponent in `memarg`) does not affect execution semantics, serving only as a hint for access alignment. + +### Supported Instructions + +The proposal supports the following memory load and store instructions: + +| Instruction | Category | Operation | +| :--- | :--- | :--- | +| `i32.load` | Regular | Load | +| `i64.load` | Regular | Load | +| `f32.load` | Regular | Load | +| `f64.load` | Regular | Load | +| `i32.load8_s` | Regular | Load | +| `i32.load8_u` | Regular | Load | +| `i32.load16_s` | Regular | Load | +| `i32.load16_u` | Regular | Load | +| `i64.load8_s` | Regular | Load | +| `i64.load8_u` | Regular | Load | +| `i64.load16_s` | Regular | Load | +| `i64.load16_u` | Regular | Load | +| `i64.load32_s` | Regular | Load | +| `i64.load32_u` | Regular | Load | +| `i32.store` | Regular | Store | +| `i64.store` | Regular | Store | +| `f32.store` | Regular | Store | +| `f64.store` | Regular | Store | +| `i32.store8` | Regular | Store | +| `i32.store16` | Regular | Store | +| `i64.store8` | Regular | Store | +| `i64.store16` | Regular | Store | +| `i64.store32` | Regular | Store | +| `v128.load` | SIMD | Load | +| `v128.store` | SIMD | Store | +| `v128.load8x8_s` | SIMD | Load | +| `v128.load8x8_u` | SIMD | Load | +| `v128.load16x4_s` | SIMD | Load | +| `v128.load16x4_u` | SIMD | Load | +| `v128.load32x2_s` | SIMD | Load | +| `v128.load32x2_u` | SIMD | Load | +| `v128.load8_splat` | SIMD | Load (Splat) | +| `v128.load16_splat` | SIMD | Load (Splat) | +| `v128.load32_splat` | SIMD | Load (Splat) | +| `v128.load64_splat` | SIMD | Load (Splat) | +| `v128.load32_zero` | SIMD | Load (Zero) | +| `v128.load64_zero` | SIMD | Load (Zero) | +| `v128.load8_lane` | SIMD | Load (Lane) | +| `v128.load16_lane` | SIMD | Load (Lane) | +| `v128.load32_lane` | SIMD | Load (Lane) | +| `v128.load64_lane` | SIMD | Load (Lane) | +| `v128.store8_lane` | SIMD | Store (Lane) | +| `v128.store16_lane` | SIMD | Store (Lane) | +| `v128.store32_lane` | SIMD | Store (Lane) | +| `v128.store64_lane` | SIMD | Store (Lane) | ### Encoding -Use a reserved bit (4) in the `memarg` field to signal that the load/store instructions will be -operating on an array. As mentioned above there will be a type index immediate and array reference -argument. +The array versions of the instructions reuse the existing opcodes for the standard memory instructions. + +An instruction is determined to be an array access instruction if **bit 4** (value `0x10`) of the `flags` field in its `memarg` immediate is set. + +When bit 4 of `flags` is set: +- The instruction operates on a GC array instead of linear memory. +- The `memarg` is parsed normally for `flags` and `offset` (both `u32` in LEB128). Bits 0-3 of `flags` represent the alignment exponent (expressed as `log_2(align)`). +- **No memory index (`mem_idx`) is read/parsed**, even if bit 6 of `flags` is set. In a valid module, bit 6 of `flags` MUST be 0 when bit 4 is set. This avoids conflicts where the type index would be misparsed as a memory index. +- Immediately following the `memarg` fields (`flags` and `offset`), a `typeidx` (representing the type index of the array type `$t`) is encoded as a `u32` (LEB128). +- For lane instructions (e.g., `v128.load8_lane`, `v128.store8_lane`), the 1-byte `laneidx` is encoded immediately *after* the `typeidx`. + +Thus, the binary format of a multibyte array instruction is: +`instr ::= op memarg_array typeidx` (for regular and SIMD load/store) +`instr ::= op memarg_array typeidx laneidx` (for SIMD lane load/store) + +Where: +- `memarg_array ::= flags:u32 offset:u32` (where `flags & 0x10 != 0`) ### Text Format Syntax -``` -i32.load (type ) () (
) +In the text format, the instructions reuse the keyword names of the standard memory instructions. A type index immediate `(type $t)` is required. + +Since these instructions operate on GC arrays and not linear memory, **a memory index is not allowed**. An offset immediate (`offset=N`) and an alignment immediate (`align=N`) are allowed (with `offset` defaulting to `0` and `align` defaulting to the instruction's natural alignment if omitted). + +For lane instructions, a lane index immediate is required at the end. + +#### Folded (S-Expression) Form +```wat +;; Load +(i32.load (type $t) [offset=N] [align=N] (local.get $array) (local.get $index)) + +;; Store +(i32.store (type $t) [offset=N] [align=N] (local.get $array) (local.get $index) (local.get $val)) + +;; Lane Load +(v128.load8_lane (type $t) [offset=N] [align=N] $lane (local.get $array) (local.get $index) (local.get $vec)) -i32.store (type ) () (
) () +;; Lane Store +(v128.store8_lane (type $t) [offset=N] [align=N] $lane (local.get $array) (local.get $index) (local.get $vec)) ``` ## Alternatives From ac553f3434a100b033abf3be2ac91ced137fa0f4 Mon Sep 17 00:00:00 2001 From: Brendan Dahl Date: Wed, 17 Jun 2026 20:12:57 +0000 Subject: [PATCH 2/2] Review comments. --- proposals/multibyte-array-access/Overview.md | 61 ++++++++++---------- 1 file changed, 30 insertions(+), 31 deletions(-) diff --git a/proposals/multibyte-array-access/Overview.md b/proposals/multibyte-array-access/Overview.md index f9830c05..a7c4e053 100644 --- a/proposals/multibyte-array-access/Overview.md +++ b/proposals/multibyte-array-access/Overview.md @@ -2,7 +2,7 @@ ## Summary -Reuse the existing linear memory load/store instructions to allow multibyte access to Wasm GC numeric (and packed) array types (e.g., `i8`, `i16`, `i32`, `i64`, `f32`, `f64`). +Reuse the existing linear memory load/store instructions to allow multibyte access to Wasm GC numeric, vector, and packed array types (e.g., `i8`, `i16`, `i32`, `i64`, `f32`, `f64`, `v128`). ## Motivation @@ -18,44 +18,44 @@ highly inefficient, and hinder performance. The array versions of the load/store instructions follow the same data transformation semantics as the linear memory instructions, but they operate on GC arrays instead of linear memories. #### Valid Array Types -- **Load instructions**: The array type `$t` must expand to `array (mut? t_elem)` where `t_elem` is a numeric type (`i8`, `i16`, `i32`, `i64`, `f32`, `f64`). -- **Store instructions**: The array type `$t` must expand to `array (mut t_elem)` where `t_elem` is a numeric type (`i8`, `i16`, `i32`, `i64`, `f32`, `f64`). +- **Load instructions**: The array type `$t` must expand to `array (mut? t_elem)` where `t_elem` is a numeric type (`i8`, `i16`, `i32`, `i64`, `f32`, `f64`, `v128`). +- **Store instructions**: The array type `$t` must expand to `array (mut t_elem)` where `t_elem` is a numeric type (`i8`, `i16`, `i32`, `i64`, `f32`, `f64`, `v128`). #### Validation For type index `$t` and memory instruction `t_value.op`: 1. The type `$t` must be a valid type index in the module, and must expand to an array type. -2. The element type of the array must be a numeric type or packed numeric type. +2. The element type of the array must be a numeric type, vector type, or packed numeric type. 3. If it is a store instruction, the array type must be mutable. 4. The instruction has the following input and output types on the stack: - - **Load** (`t_value.load`): - - Inputs: `[ref: (ref null $t), index: i32]` + - **Load** (`t_value.loadN_sx`): + - Inputs: `[ref: (ref null $t), address: i32]` - Outputs: `[val: t_value]` - - **Store** (`t_value.store`): - - Inputs: `[ref: (ref null $t), index: i32, val: t_value]` + - **Store** (`t_value.storeN`): + - Inputs: `[ref: (ref null $t), address: i32, val: t_value]` - Outputs: `[]` - - **Lane Load** (`v128.loadN_lane`): - - Inputs: `[ref: (ref null $t), index: i32, vec: v128]` + - **Lane Load** (`v128.loadN_lane/spat/zero/xM_sx`): + - Inputs: `[ref: (ref null $t), address: i32, vec: v128]` - Outputs: `[vec: v128]` - **Lane Store** (`v128.storeN_lane`): - - Inputs: `[ref: (ref null $t), index: i32, vec: v128]` + - Inputs: `[ref: (ref null $t), address: i32, vec: v128]` - Outputs: `[]` #### Execution -1. **Null Validation**: - - If the array reference operand is null, execution traps with a null reference error. +1. **Null Check**: + - If the array reference operand is null, execution traps. 2. **Effective Address Calculation**: - - The effective address $ea$ is calculated as $index + offset$, where $index$ is the `i32` stack operand (interpreted as an unsigned 32-bit integer) and $offset$ is the static byte offset immediate from `memarg`. + - The effective address $ea$ is calculated as $address + offset$, where $address$ is the `i32` stack operand (interpreted as an unsigned 32-bit integer) and $offset$ is the static byte offset immediate from `memarg`. 3. **Bounds Check**: - - Let $S$ be the size (in bytes) of the memory access (e.g., 4 for `i32.load`, 16 for `v128.load`). + - Let $S$ be the size (in bytes) of the memory access (e.g., the byte size of type `t` for `t.load`/`t.store`, or $N/8$ for instructions with a size suffix $N$, such as `t.loadN_sx`, `t.storeN`, `v128.loadN_lane`, etc.). - Let $L$ be the length of the array (obtained via `array.len`). - Let $E$ be the element size in bytes of the array type `$t` (e.g., 1 for `i8`, 2 for `i16`, etc.). - - The access is in bounds if $ea \ge 0$ and $ea + S \le L \times E$. - - If the access is out of bounds, execution traps with an out-of-bounds array access error. + - The access is in bounds if $ea + S \le L \times E$. + - If the access is out of bounds, execution traps. 4. **Operation**: - - **Load**: Reads $S$ bytes from the array payload starting at byte offset $ea$, decodes them using little-endian byte order, and pushes the resulting value onto the stack. - - **Store**: Encodes the value operand into $S$ bytes using little-endian byte order, and writes them to the array payload starting at byte offset $ea$. - - **Lane Operations**: Loads/stores a single lane of the vector operand from/to the array payload at byte offset $ea$. + - **Load**: Reads $S$ bytes from the array payload starting at byte offset $ea$, decodes them using little-endian byte order, possibly extends them to the result size, and pushes the resulting value onto the stack. + - **Store**: Possibly wraps the value operand according to the target size, encodes it into $S$ bytes using little-endian byte order, and writes them to the array payload starting at byte offset $ea$. + - **Lane Operations**: Loads/stores a single lane of the vector operand, possibly extended or wrapped, from/to the array payload at byte offset $ea$. 5. **Alignment**: - The alignment value (expressed as power of 2 exponent in `memarg`) does not affect execution semantics, serving only as a hint for access alignment. @@ -119,17 +119,16 @@ An instruction is determined to be an array access instruction if **bit 4** (val When bit 4 of `flags` is set: - The instruction operates on a GC array instead of linear memory. -- The `memarg` is parsed normally for `flags` and `offset` (both `u32` in LEB128). Bits 0-3 of `flags` represent the alignment exponent (expressed as `log_2(align)`). -- **No memory index (`mem_idx`) is read/parsed**, even if bit 6 of `flags` is set. In a valid module, bit 6 of `flags` MUST be 0 when bit 4 is set. This avoids conflicts where the type index would be misparsed as a memory index. +- The `memarg` is parsed normally for `flags` and `offset` (both `u32` in LEB128) as well as the `typeidx` (encoded as `u32` in LEB128). Bits 0-3 of `flags` represent the alignment exponent (expressed as `log_2(align)`). +- No memory index (`memidx`) is allowed. In a valid module, bit 6 of `flags` must be 0 when bit 4 is set. - Immediately following the `memarg` fields (`flags` and `offset`), a `typeidx` (representing the type index of the array type `$t`) is encoded as a `u32` (LEB128). -- For lane instructions (e.g., `v128.load8_lane`, `v128.store8_lane`), the 1-byte `laneidx` is encoded immediately *after* the `typeidx`. Thus, the binary format of a multibyte array instruction is: -`instr ::= op memarg_array typeidx` (for regular and SIMD load/store) -`instr ::= op memarg_array typeidx laneidx` (for SIMD lane load/store) +- `instr ::= op memarg` (for regular and SIMD load/store) +- `instr ::= op memarg laneidx` (for SIMD lane load/store) -Where: -- `memarg_array ::= flags:u32 offset:u32` (where `flags & 0x10 != 0`) +Where `memarg` is defined as: +- `memarg ::= flags:u32 offset:u32 typeidx:u32` (where `flags & 0x50 = 0x10`) ### Text Format Syntax @@ -142,16 +141,16 @@ For lane instructions, a lane index immediate is required at the end. #### Folded (S-Expression) Form ```wat ;; Load -(i32.load (type $t) [offset=N] [align=N] (local.get $array) (local.get $index)) +(i32.load (type $t) [offset=N] [align=N] (local.get $array) (local.get $address)) ;; Store -(i32.store (type $t) [offset=N] [align=N] (local.get $array) (local.get $index) (local.get $val)) +(i32.store (type $t) [offset=N] [align=N] (local.get $array) (local.get $address) (local.get $val)) ;; Lane Load -(v128.load8_lane (type $t) [offset=N] [align=N] $lane (local.get $array) (local.get $index) (local.get $vec)) +(v128.load8_lane (type $t) [offset=N] [align=N] $lane (local.get $array) (local.get $address) (local.get $vec)) ;; Lane Store -(v128.store8_lane (type $t) [offset=N] [align=N] $lane (local.get $array) (local.get $index) (local.get $vec)) +(v128.store8_lane (type $t) [offset=N] [align=N] $lane (local.get $array) (local.get $address) (local.get $vec)) ``` ## Alternatives