Wado

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(&gt, &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:

  1. Deterministic within environment: Results vary across hardware but are consistent within a single runtime
  2. Not handleable: Unlike I/O effects, hardware behavior cannot be intercepted or mocked
  3. Consistency with floats: Standard f32/f64 also have NaN non-determinism but are not effects
  4. 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

Not Yet Implemented

Consequences

Advantages

Trade-offs

Future Extensions

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)

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:

  1. Standardized: v128 is part of Wasm 3.0, supported by all major runtimes
  2. Predictable: Fixed lane count enables compile-time optimization and validation
  3. 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:

References