1use thiserror::Error;
7
8use crate::detector::{BlockIR, Op};
9use crate::ir::{ElemType, GridHint, TensorWasmKernelBlueprint, TensorWasmOp};
10
11#[derive(Debug, Error, PartialEq)]
13pub enum LowerError {
14 #[error("unsupported op in block `{block}`: {op:?}")]
16 UnsupportedOp {
17 block: String,
19 op: Op,
21 },
22 #[error("memory-reference out of bounds in block `{block}`")]
25 OutOfBoundsMemory {
26 block: String,
28 },
29 #[error("mixed SIMD element types in block `{block}` ({first} vs {second})")]
36 MixedElementTypes {
37 block: String,
39 first: ElemType,
41 second: ElemType,
43 },
44}
45
46const DEFAULT_LANES: u32 = 4;
49
50#[derive(Debug, Clone, Copy, PartialEq, Eq)]
55pub struct MemRef {
56 pub offset: u64,
58 pub len: u64,
60}
61
62fn infer_block_shape(block: &BlockIR) -> Result<(ElemType, u32), LowerError> {
71 let mut shape: Option<(ElemType, u32)> = None;
72 for op in &block.ops {
73 let cur = match op {
74 Op::V128Add { lane_ty, lanes }
75 | Op::V128Mul { lane_ty, lanes }
76 | Op::V128Fma { lane_ty, lanes } => (*lane_ty, *lanes),
77 _ => continue,
78 };
79 match shape {
80 None => shape = Some(cur),
81 Some((seen_ty, _)) if seen_ty != cur.0 => {
82 return Err(LowerError::MixedElementTypes {
83 block: block.name.clone(),
84 first: seen_ty,
85 second: cur.0,
86 });
87 }
88 Some(_) => {}
89 }
90 }
91 Ok(shape.unwrap_or((ElemType::F32, DEFAULT_LANES)))
92}
93
94pub fn lower_block(block: &BlockIR) -> Result<TensorWasmKernelBlueprint, LowerError> {
109 let (mem_elem, mem_lanes) = infer_block_shape(block)?;
110 let mut bp = TensorWasmKernelBlueprint::new(&block.name);
111 for op in &block.ops {
112 let lo = match op {
113 Op::V128Add { lane_ty, lanes } => TensorWasmOp::VecAdd {
114 elem: *lane_ty,
115 lanes: *lanes,
116 },
117 Op::V128Mul { lane_ty, lanes } => TensorWasmOp::VecMul {
118 elem: *lane_ty,
119 lanes: *lanes,
120 },
121 Op::V128Fma { lane_ty, lanes } => TensorWasmOp::VecFma {
122 elem: *lane_ty,
123 lanes: *lanes,
124 },
125 Op::Load => TensorWasmOp::LoadUnified {
126 elem: mem_elem,
127 lanes: mem_lanes,
128 },
129 Op::Store => TensorWasmOp::StoreUnified {
130 elem: mem_elem,
131 lanes: mem_lanes,
132 },
133 unsupported => {
134 return Err(LowerError::UnsupportedOp {
135 block: block.name.clone(),
136 op: *unsupported,
137 });
138 }
139 };
140 bp.ops.push(lo);
141 }
142
143 let total_threads = u32::try_from(block.loop_trip_count.unwrap_or(256)).unwrap_or(u32::MAX);
146 bp.grid_hint = GridHint {
147 total_threads,
148 preferred_block_size: 128,
149 };
150 Ok(bp)
151}
152
153pub fn lower_block_checked(
161 block: &BlockIR,
162 mem_refs: &[MemRef],
163 memory_bytes: u64,
164) -> Result<TensorWasmKernelBlueprint, LowerError> {
165 for r in mem_refs {
166 let end = r.offset.checked_add(r.len);
167 let out_of_bounds = match end {
168 Some(end) => end > memory_bytes,
169 None => true, };
171 if out_of_bounds {
172 return Err(LowerError::OutOfBoundsMemory {
173 block: block.name.clone(),
174 });
175 }
176 }
177 lower_block(block)
178}
179
180#[cfg(test)]
181mod tests {
182 use super::*;
183
184 fn block(name: &str, ops: Vec<Op>, loop_n: Option<u64>) -> BlockIR {
185 BlockIR::new(name, ops, loop_n)
186 }
187
188 fn add(lane_ty: ElemType, lanes: u32) -> Op {
189 Op::V128Add { lane_ty, lanes }
190 }
191 fn mul(lane_ty: ElemType, lanes: u32) -> Op {
192 Op::V128Mul { lane_ty, lanes }
193 }
194 fn fma(lane_ty: ElemType, lanes: u32) -> Op {
195 Op::V128Fma { lane_ty, lanes }
196 }
197
198 #[test]
199 fn lower_simple_vector_add() {
200 let b = block(
201 "vec_add",
202 vec![Op::Load, Op::Load, add(ElemType::F32, 4), Op::Store],
203 Some(128),
204 );
205 let bp = lower_block(&b).unwrap();
206 assert_eq!(bp.entry, "vec_add");
207 assert_eq!(bp.ops.len(), 4);
208 assert!(matches!(
209 bp.ops[2],
210 TensorWasmOp::VecAdd {
211 elem: ElemType::F32,
212 lanes: 4
213 }
214 ));
215 assert!(matches!(
217 bp.ops[0],
218 TensorWasmOp::LoadUnified {
219 elem: ElemType::F32,
220 lanes: 4
221 }
222 ));
223 assert_eq!(bp.grid_hint.total_threads, 128);
224 }
225
226 #[test]
230 fn lower_i32x4_add_carries_integer_element_type() {
231 let b = block(
232 "i32_add",
233 vec![Op::Load, Op::Load, add(ElemType::I32, 4), Op::Store],
234 Some(128),
235 );
236 let bp = lower_block(&b).unwrap();
237 assert!(matches!(
238 bp.ops[2],
239 TensorWasmOp::VecAdd {
240 elem: ElemType::I32,
241 lanes: 4
242 }
243 ));
244 assert!(matches!(
245 bp.ops[0],
246 TensorWasmOp::LoadUnified {
247 elem: ElemType::I32,
248 ..
249 }
250 ));
251 assert!(!bp.ops.iter().any(|o| matches!(
253 o,
254 TensorWasmOp::VecAdd {
255 elem: ElemType::F32,
256 ..
257 }
258 )));
259 }
260
261 #[test]
264 fn lower_mixed_element_types_rejected() {
265 let b = block(
266 "mixed",
267 vec![add(ElemType::F32, 4), add(ElemType::I32, 4)],
268 Some(128),
269 );
270 let err = lower_block(&b).unwrap_err();
271 assert!(matches!(err, LowerError::MixedElementTypes { .. }));
272 }
273
274 #[test]
276 fn lower_f64x2_lane_count_survives() {
277 let b = block("f64_add", vec![add(ElemType::F64, 2)], Some(128));
278 let bp = lower_block(&b).unwrap();
279 assert!(matches!(
280 bp.ops[0],
281 TensorWasmOp::VecAdd {
282 elem: ElemType::F64,
283 lanes: 2
284 }
285 ));
286 }
287
288 #[test]
289 fn unsupported_op_rejected() {
290 let b = block("with_call", vec![add(ElemType::F32, 4), Op::Call], Some(64));
291 let err = lower_block(&b).unwrap_err();
292 assert!(matches!(err, LowerError::UnsupportedOp { .. }));
293 }
294
295 #[test]
296 fn missing_trip_count_uses_default() {
297 let b = block("dyn", vec![add(ElemType::F32, 4)], None);
298 let bp = lower_block(&b).unwrap();
299 assert_eq!(bp.grid_hint.total_threads, 256);
300 }
301
302 #[test]
303 fn all_v128_lower() {
304 let b = block(
305 "fma_loop",
306 vec![
307 fma(ElemType::F32, 4),
308 fma(ElemType::F32, 4),
309 mul(ElemType::F32, 4),
310 ],
311 Some(64),
312 );
313 let bp = lower_block(&b).unwrap();
314 for op in &bp.ops {
315 assert!(matches!(
316 op,
317 TensorWasmOp::VecFma { .. }
318 | TensorWasmOp::VecMul { .. }
319 | TensorWasmOp::VecAdd { .. }
320 ));
321 }
322 }
323
324 #[test]
325 fn lower_block_checked_passes_with_in_bounds_refs() {
326 let b = block(
327 "vec_add",
328 vec![Op::Load, add(ElemType::F32, 4), Op::Store],
329 Some(128),
330 );
331 let refs = &[
332 MemRef { offset: 0, len: 16 },
333 MemRef {
334 offset: 1024,
335 len: 16,
336 },
337 ];
338 let bp = lower_block_checked(&b, refs, 64 * 1024).expect("in-bounds ok");
339 assert_eq!(bp.ops.len(), 3);
340 }
341
342 #[test]
343 fn lower_block_checked_rejects_out_of_bounds_refs() {
344 let b = block("oob", vec![Op::Load], Some(64));
345 let refs = &[MemRef {
347 offset: 1020,
348 len: 16,
349 }];
350 let err = lower_block_checked(&b, refs, 1024).unwrap_err();
351 assert!(matches!(err, LowerError::OutOfBoundsMemory { .. }));
352 }
353
354 #[test]
355 fn lower_block_checked_rejects_overflow_refs() {
356 let b = block("overflow", vec![Op::Load], Some(64));
357 let refs = &[MemRef {
358 offset: u64::MAX - 4,
359 len: 16,
360 }];
361 let err = lower_block_checked(&b, refs, 1024).unwrap_err();
362 assert!(matches!(err, LowerError::OutOfBoundsMemory { .. }));
363 }
364}