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

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