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
== and != are the exception. They ask whether all 128 bits are equal and
answer with one bool. There is no lane type to defer to, since every
interpretation type is the same v128, so a float lane's NaN is a bit pattern
here and equals itself. That serves a test asserting a whole register. Use the
lane-wise eq where the IEEE reading is the one you need.
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
- A typed
shufflemethod. The instruction is reachable asbuiltin::i8x16_shuffle(l0, …, l15, a, b), whose sixteen lane indices must be written as literals at the call. A parameter does not keep its argument a literal, so no method can carry them until const generic parameters land. Each lane index is declared#[immediate(lN)], as are thelaneparameters ofextract_lane/replace_laneand the bit pattern ofv128_const. That is what stops an optimizer replacing the literal with a global read or a local; codegen reads the value out of the call itself. See the spec on#[immediate(...)]. replace_lanewith a const index, for the same reason.- 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: a typed shuffle and tuple-style lane access need const generic support in the compiler. Until then shuffle is reached through its builtin.
- 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
