WEP: SIMD v128 Types
Context
WebAssembly SIMD provides 128-bit vector operations for parallel data processing. Wado exposes these capabilities through the core:simd module with type-safe newtype wrappers over the primitive v128 type.
Decision
Base Types
A single primitive base type that provides no operations directly:
#[primitive]
type v128;
Interpretation Types (Newtypes)
Concrete interpretations as newtypes with operator overloading:
// Signed integer interpretations
pub type i8x16 = v128;
pub type i16x8 = v128;
pub type i32x4 = v128;
pub type i64x2 = v128;
// Unsigned integer interpretations
pub type u8x16 = v128;
pub type u16x8 = v128;
pub type u32x4 = v128;
pub type u64x2 = v128;
// Floating-point interpretations
pub type f32x4 = v128;
pub type f64x2 = v128;
Construction
Splat (broadcast single value to all lanes):
let zeros = i32x4::splat(0); // [0, 0, 0, 0]
let ones = f64x2::splat(1.0); // [1.0, 1.0]
From tuple literals via coercion (using SequenceLiteral trait):
let v: i32x4 = [1, 2, 3, 4];
let f: f32x4 = [1.0, 2.0, 3.0, 4.0];
let bytes: u8x16 = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15];
Lane Access
Read lanes using extract_lane:
let v = i32x4::splat(0);
let first = v.extract_lane(0); // 0: i32
let second = v.extract_lane(1); // 0: i32
For sub-word types, signed and unsigned extraction:
let v = i8x16::splat(0);
let signed = v.extract_lane_s(0); // i32 (sign-extended)
let unsigned = v.extract_lane_u(0); // i32 (zero-extended)
Operators
Arithmetic operators via trait implementations:
let a: i32x4 = [1, 2, 3, 4];
let b: i32x4 = [10, 20, 30, 40];
// Arithmetic
let sum = a + b; // [11, 22, 33, 44]
let diff = a - b; // [-9, -18, -27, -36]
let prod = a * b; // [10, 40, 90, 160]
// Bitwise
let and = a & b;
let or = a | b;
let xor = a ^ b;
let not = ~a;
// Shifts (by scalar)
let shl = a << 2; // Each lane shifted left by 2
let shr = a >> 1; // Each lane shifted right by 1
// Negation
let neg = -a; // [-1, -2, -3, -4]
Floating-point operations:
let a: f32x4 = [1.0, 2.0, 3.0, 4.0];
let b: f32x4 = [2.0, 2.0, 2.0, 2.0];
let sum = a + b; // [3.0, 4.0, 5.0, 6.0]
let div = a / b; // [0.5, 1.0, 1.5, 2.0]
let neg = -a; // [-1.0, -2.0, -3.0, -4.0]
Type Conversion
Bitwise reinterpretation via as cast (zero-cost, all newtypes share v128 base):
let ints: i32x4 = [1, 2, 3, 4];
let floats: f32x4 = ints as f32x4; // Reinterpret bits
let unsigned: u32x4 = ints as u32x4; // Same bits, different ops
Value conversions between integer and float:
let ints: i32x4 = [1, 2, 3, 4];
let floats = f32x4::convert_i32x4_s(ints); // [1.0, 2.0, 3.0, 4.0]
let back = i32x4::trunc_sat_f32x4_s(floats); // [1, 2, 3, 4]
// Widening/narrowing conversions
let wide = f64x2::convert_low_i32x4_s(ints); // [1.0, 2.0] (low 2 lanes)
let narrow = f32x4::demote_f64x2_zero(wide); // [1.0, 2.0, 0.0, 0.0]
Comparison Operations
Comparison returns a mask vector (all 1s or all 0s per lane), not a bool. Use explicit methods, not ==/> operators:
let a: i32x4 = [1, 5, 3, 8];
let b: i32x4 = [2, 3, 3, 7];
let eq = a.eq(&b); // i32x4: [0, 0, -1, 0] (-1 = all bits set)
let ne = a.ne(&b); // i32x4: [-1, -1, 0, -1]
let lt = a.lt(&b); // i32x4: [-1, 0, 0, 0]
let gt = a.gt(&b); // i32x4: [0, -1, 0, -1]
let le = a.le(&b); // i32x4: [-1, 0, -1, 0]
let ge = a.ge(&b); // i32x4: [0, -1, -1, -1]
// Use mask for selection
let max = a.bitselect(>, &b); // Select a where gt is true, else b
Integer-Specific Operations
Saturating Arithmetic
Available on i8x16, i16x8 (signed/unsigned variants) and u8x16, u16x8:
let a: i8x16 = [120, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
let b: i8x16 = [120, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
let sat = a.add_sat_s(&b); // Clamps to 127 instead of overflowing
Extension and Narrowing
Widen lanes from a narrower type, or narrow lanes to a smaller type:
// Extend i8x16 → i16x8 (low/high half, signed/unsigned)
let bytes: i8x16 = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16];
let low = i16x8::extend_low_i8x16_s(bytes); // [1, 2, 3, 4, 5, 6, 7, 8]
let high = i16x8::extend_high_i8x16_s(bytes); // [9, 10, 11, 12, 13, 14, 15, 16]
// Narrow i16x8 → i8x16 (saturating)
let wide = (i16x8::splat(200) as i8x16);
let narrow = wide.narrow_i16x8_s(&wide); // Clamped to [-128, 127]
Extended Multiplication and Pairwise Addition
// Extended multiply: widen inputs then multiply
let a = i16x8::splat(100) as i16x8;
let b = i16x8::splat(200) as i16x8;
let product = a.extmul_low_i8x16_s(&b); // 100 * 200 per widened lane
// Pairwise addition: add adjacent pairs and widen
let v: i8x16 = [1, 2, 3, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
let pairwise = i16x8::extadd_pairwise_i8x16_s(v); // [3, 7, 0, 0, 0, 0, 0, 0]
Dot Product
// i16x8 dot product → i32x4 (multiply pairs and add adjacent results)
let a = (i16x8::splat(0) as i32x4); // data interpreted as i16x8
let b = (i16x8::splat(0) as i32x4);
let dot = a.dot_i16x8_s(&b);
Floating-Point Specific Operations
let v: f64x2 = [4.0, 9.0];
let sq = v.sqrt(); // [2.0, 3.0]
let ab = v.abs(); // absolute value
let mn = v.min(&v); // IEEE 754 minimum
let mx = v.max(&v); // IEEE 754 maximum
let pm = v.pmin(&v); // pseudo-minimum (faster, NaN propagation differs)
// Rounding
let c = v.ceil();
let f = v.floor();
let t = v.trunc();
let n = v.nearest(); // round to nearest even
Swizzle
Single-operand swizzle (runtime indices via i8x16):
let v: i8x16 = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15];
let indices: i8x16 = [15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0];
let reversed = v.swizzle(&indices); // Runtime lane selection
Relaxed SIMD
Relaxed SIMD operations are provided as methods on the standard newtypes, not via separate types. Relaxed SIMD's non-determinism is handled via explicit method names (e.g., relaxed_madd), not effects.
Rationale:
- Deterministic within environment: Results vary across hardware but are consistent within a single runtime
- Not handleable: Unlike I/O effects, hardware behavior cannot be intercepted or mocked
- Consistency with floats: Standard f32/f64 also have NaN non-determinism but are not effects
- Performance focus: Relaxed SIMD exists for speed; effect propagation would add friction
Fused Multiply-Add
let a: f32x4 = [1.0, 2.0, 3.0, 4.0];
let b = f32x4::splat(2.0);
let c = f32x4::splat(10.0);
let fma = a.relaxed_madd(&b, &c); // a*b+c = [12, 14, 16, 18]
let nfma = a.relaxed_nmadd(&b, &c); // -(a*b)+c = [8, 6, 4, 2]
Also available on f64x2.
Relaxed Min/Max
let rmin = a.relaxed_min(&b); // Faster than strict min, NaN handling varies
let rmax = a.relaxed_max(&b);
Available on f32x4 and f64x2.
Relaxed Truncation
let floats: f32x4 = [1.5, 2.9, -1.5, 100.0];
let trunc_s = i32x4::relaxed_trunc_f32x4_s(floats); // Signed truncation
let trunc_u = i32x4::relaxed_trunc_f32x4_u(floats); // Unsigned truncation
let doubles: f64x2 = [1.5, 2.9];
let trunc_d = i32x4::relaxed_trunc_f64x2_s_zero(doubles); // Low 2 lanes, high zeroed
Out-of-range values produce implementation-defined results (unlike trunc_sat_* which saturates).
Relaxed Lane Select
let v1: i32x4 = [100, 200, 300, 400];
let v2: i32x4 = [1, 2, 3, 4];
let mask: i32x4 = [-1, 0, -1, 0];
let sel = v1.relaxed_laneselect(&v2, &mask); // [100, 2, 300, 4]
Available on i8x16, i16x8, i32x4, i64x2.
Relaxed Swizzle
let v: i8x16 = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15];
let idx: i8x16 = [3, 2, 1, 0, 7, 6, 5, 4, 11, 10, 9, 8, 15, 14, 13, 12];
let swizzled = v.relaxed_swizzle(&idx);
Unlike standard swizzle (which returns 0 for out-of-range indices), relaxed_swizzle produces implementation-defined values for out-of-range indices.
Relaxed Q15 Multiply and Dot Product
// Q15 fixed-point saturating multiply (rounding behavior varies)
let a: i16x8 = [16384, 0, 0, 0, 0, 0, 0, 0];
let b: i16x8 = [16384, 0, 0, 0, 0, 0, 0, 0];
let q15 = a.relaxed_q15mulr_s(&b);
// i8 × i7 dot product → i16
let bytes_a = i16x8::splat(0);
let bytes_b = i16x8::splat(0);
let dot = bytes_a.relaxed_dot_i8x16_i7x16_s(&bytes_b);
// i8 × i7 dot product with i32 accumulate
let acc: i32x4 = [0, 0, 0, 0];
let src_a: i8x16 = [1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
let src_b: i8x16 = [1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
let dot_add = i32x4::relaxed_dot_i8x16_i7x16_add_s(src_a, src_b, &acc);
Global Variables
v128 types can be used in global variables:
global ONES: f32x4 = f32x4::splat(1.0); // splat for uniform values
global mut counter: i32x4 = i32x4::splat(0); // splat for zero-init
fn increment() {
counter = counter + i32x4::splat(1);
}
Implementation Status
Implemented
- v128 as primitive type in TIR
core:simdmodule with 10 newtype wrappers (i8x16, i16x8, i32x4, i64x2, u8x16, u16x8, u32x4, u64x2, f32x4, f64x2)- 230+ methods across all types
- Operator overloading (+, -, *, /, &, |, ^, ~, <<, >>) for all SIMD newtypes
- v128 in struct fields (Wasm GC integrates v128 as a storable type)
- Tuple literal coercion via
SequenceLiteraltrait (let v: i32x4 = [1, 2, 3, 4]) - Codegen support (emit.rs, WIR, lowering, inline)
- Relaxed SIMD: 20 instructions as
relaxed_*methods on existing newtypes
Not Yet Implemented
- Const generics (deferred): shuffle with compile-time lane indices, replace_lane with const index
- Tuple-style lane access (
.0,.1, etc.) — depends on const generics
Consequences
Advantages
- Type safety: Newtypes prevent accidental mixing of interpretations
- Ergonomic syntax: Operator overloading, splat construction
- Wasm alignment: Maps directly to Wasm SIMD instructions
- Const generics: (Future) Compile-time validation of lane indices and shuffle patterns
Trade-offs
- Const generics required: Shuffle and tuple-style lane access need const generic support in the compiler
- Many newtypes: 10 types, but clear semantics
Future Extensions
- SIMD-optimized math functions (
sin,cos,expfor f32x4/f64x2) - Horizontal operations (
sum_lanes,min_lane,max_lane) - Load/store from linear memory (if Wado adds linear memory support)
Future Considerations: Flexible Vectors
The WebAssembly Flexible Vectors proposal introduces variable-width vector types (vec.i32, vec.f32, etc.) where lane count is determined at runtime rather than compile time. This would enable exploitation of wider SIMD hardware (AVX-512, ARM SVE).
Current Status (as of January 2026)
- Proposal remains at Phase 1 since 2019
- Cranelift infrastructure work started but stalled on register allocator integration
- No wasmtime runtime support available
Design Implications
| Aspect | v128 (This WEP) | Flexible Vectors |
|---|---|---|
| Lane count | Compile-time (4 for i32x4) | Runtime query |
| Lane access | .extract_lane(i) |
Loop-based iteration |
| Type safety | Static validation | Dynamic bounds checking |
Wado's Position
v128 types are the stable baseline for portable SIMD in Wado:
- Standardized: v128 is part of Wasm 3.0, supported by all major runtimes
- Predictable: Fixed lane count enables compile-time optimization and validation
- Sufficient: 128-bit SIMD covers most practical use cases
If Flexible Vectors advances to Phase 3+, Wado may add optional support:
// Hypothetical future syntax
use {VecI32} from "core:simd/flexible";
fn sum_vector(v: VecI32) -> i32 {
let len = VecI32::lanes(); // Runtime query
let mut total = 0;
for let mut i = 0; i < len; i += 1 {
total += v.lane(i); // Dynamic access
}
return total;
}
Key principles for future flexible vectors support:
- Coexistence with v128 (different use cases, not replacement)
- Explicit opt-in via separate types
- Loop-based idioms for lane iteration
