use thiserror::Error;
use crate::detector::{BlockIR, Op};
use crate::ir::{ElemType, GridHint, TensorWasmKernelBlueprint, TensorWasmOp};
#[derive(Debug, Error, PartialEq)]
pub enum LowerError {
#[error("unsupported op in block `{block}`: {op:?}")]
UnsupportedOp {
block: String,
op: Op,
},
#[error("memory-reference out of bounds in block `{block}`")]
OutOfBoundsMemory {
block: String,
},
#[error("mixed SIMD element types in block `{block}` ({first} vs {second})")]
MixedElementTypes {
block: String,
first: ElemType,
second: ElemType,
},
}
const DEFAULT_LANES: u32 = 4;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct MemRef {
pub offset: u64,
pub len: u64,
}
fn infer_block_shape(block: &BlockIR) -> Result<(ElemType, u32), LowerError> {
let mut shape: Option<(ElemType, u32)> = None;
for op in &block.ops {
let cur = match op {
Op::V128Add { lane_ty, lanes }
| Op::V128Mul { lane_ty, lanes }
| Op::V128Fma { lane_ty, lanes } => (*lane_ty, *lanes),
_ => continue,
};
match shape {
None => shape = Some(cur),
Some((seen_ty, _)) if seen_ty != cur.0 => {
return Err(LowerError::MixedElementTypes {
block: block.name.clone(),
first: seen_ty,
second: cur.0,
});
}
Some(_) => {}
}
}
Ok(shape.unwrap_or((ElemType::F32, DEFAULT_LANES)))
}
pub fn lower_block(block: &BlockIR) -> Result<TensorWasmKernelBlueprint, LowerError> {
let (mem_elem, mem_lanes) = infer_block_shape(block)?;
let mut bp = TensorWasmKernelBlueprint::new(&block.name);
for op in &block.ops {
let lo = match op {
Op::V128Add { lane_ty, lanes } => TensorWasmOp::VecAdd {
elem: *lane_ty,
lanes: *lanes,
},
Op::V128Mul { lane_ty, lanes } => TensorWasmOp::VecMul {
elem: *lane_ty,
lanes: *lanes,
},
Op::V128Fma { lane_ty, lanes } => TensorWasmOp::VecFma {
elem: *lane_ty,
lanes: *lanes,
},
Op::Load => TensorWasmOp::LoadUnified {
elem: mem_elem,
lanes: mem_lanes,
},
Op::Store => TensorWasmOp::StoreUnified {
elem: mem_elem,
lanes: mem_lanes,
},
unsupported => {
return Err(LowerError::UnsupportedOp {
block: block.name.clone(),
op: *unsupported,
});
}
};
bp.ops.push(lo);
}
let total_threads = u32::try_from(block.loop_trip_count.unwrap_or(256)).unwrap_or(u32::MAX);
bp.grid_hint = GridHint {
total_threads,
preferred_block_size: 128,
};
Ok(bp)
}
pub fn lower_block_checked(
block: &BlockIR,
mem_refs: &[MemRef],
memory_bytes: u64,
) -> Result<TensorWasmKernelBlueprint, LowerError> {
for r in mem_refs {
let end = r.offset.checked_add(r.len);
let out_of_bounds = match end {
Some(end) => end > memory_bytes,
None => true, };
if out_of_bounds {
return Err(LowerError::OutOfBoundsMemory {
block: block.name.clone(),
});
}
}
lower_block(block)
}
#[cfg(test)]
mod tests {
use super::*;
fn block(name: &str, ops: Vec<Op>, loop_n: Option<u64>) -> BlockIR {
BlockIR::new(name, ops, loop_n)
}
fn add(lane_ty: ElemType, lanes: u32) -> Op {
Op::V128Add { lane_ty, lanes }
}
fn mul(lane_ty: ElemType, lanes: u32) -> Op {
Op::V128Mul { lane_ty, lanes }
}
fn fma(lane_ty: ElemType, lanes: u32) -> Op {
Op::V128Fma { lane_ty, lanes }
}
#[test]
fn lower_simple_vector_add() {
let b = block(
"vec_add",
vec![Op::Load, Op::Load, add(ElemType::F32, 4), Op::Store],
Some(128),
);
let bp = lower_block(&b).unwrap();
assert_eq!(bp.entry, "vec_add");
assert_eq!(bp.ops.len(), 4);
assert!(matches!(
bp.ops[2],
TensorWasmOp::VecAdd {
elem: ElemType::F32,
lanes: 4
}
));
assert!(matches!(
bp.ops[0],
TensorWasmOp::LoadUnified {
elem: ElemType::F32,
lanes: 4
}
));
assert_eq!(bp.grid_hint.total_threads, 128);
}
#[test]
fn lower_i32x4_add_carries_integer_element_type() {
let b = block(
"i32_add",
vec![Op::Load, Op::Load, add(ElemType::I32, 4), Op::Store],
Some(128),
);
let bp = lower_block(&b).unwrap();
assert!(matches!(
bp.ops[2],
TensorWasmOp::VecAdd {
elem: ElemType::I32,
lanes: 4
}
));
assert!(matches!(
bp.ops[0],
TensorWasmOp::LoadUnified {
elem: ElemType::I32,
..
}
));
assert!(!bp.ops.iter().any(|o| matches!(
o,
TensorWasmOp::VecAdd {
elem: ElemType::F32,
..
}
)));
}
#[test]
fn lower_mixed_element_types_rejected() {
let b = block(
"mixed",
vec![add(ElemType::F32, 4), add(ElemType::I32, 4)],
Some(128),
);
let err = lower_block(&b).unwrap_err();
assert!(matches!(err, LowerError::MixedElementTypes { .. }));
}
#[test]
fn lower_f64x2_lane_count_survives() {
let b = block("f64_add", vec![add(ElemType::F64, 2)], Some(128));
let bp = lower_block(&b).unwrap();
assert!(matches!(
bp.ops[0],
TensorWasmOp::VecAdd {
elem: ElemType::F64,
lanes: 2
}
));
}
#[test]
fn unsupported_op_rejected() {
let b = block("with_call", vec![add(ElemType::F32, 4), Op::Call], Some(64));
let err = lower_block(&b).unwrap_err();
assert!(matches!(err, LowerError::UnsupportedOp { .. }));
}
#[test]
fn missing_trip_count_uses_default() {
let b = block("dyn", vec![add(ElemType::F32, 4)], None);
let bp = lower_block(&b).unwrap();
assert_eq!(bp.grid_hint.total_threads, 256);
}
#[test]
fn all_v128_lower() {
let b = block(
"fma_loop",
vec![
fma(ElemType::F32, 4),
fma(ElemType::F32, 4),
mul(ElemType::F32, 4),
],
Some(64),
);
let bp = lower_block(&b).unwrap();
for op in &bp.ops {
assert!(matches!(
op,
TensorWasmOp::VecFma { .. }
| TensorWasmOp::VecMul { .. }
| TensorWasmOp::VecAdd { .. }
));
}
}
#[test]
fn lower_block_checked_passes_with_in_bounds_refs() {
let b = block(
"vec_add",
vec![Op::Load, add(ElemType::F32, 4), Op::Store],
Some(128),
);
let refs = &[
MemRef { offset: 0, len: 16 },
MemRef {
offset: 1024,
len: 16,
},
];
let bp = lower_block_checked(&b, refs, 64 * 1024).expect("in-bounds ok");
assert_eq!(bp.ops.len(), 3);
}
#[test]
fn lower_block_checked_rejects_out_of_bounds_refs() {
let b = block("oob", vec![Op::Load], Some(64));
let refs = &[MemRef {
offset: 1020,
len: 16,
}];
let err = lower_block_checked(&b, refs, 1024).unwrap_err();
assert!(matches!(err, LowerError::OutOfBoundsMemory { .. }));
}
#[test]
fn lower_block_checked_rejects_overflow_refs() {
let b = block("overflow", vec![Op::Load], Some(64));
let refs = &[MemRef {
offset: u64::MAX - 4,
len: 16,
}];
let err = lower_block_checked(&b, refs, 1024).unwrap_err();
assert!(matches!(err, LowerError::OutOfBoundsMemory { .. }));
}
}