Skip to main content

tensor_wasm_jit/
blueprint_adapter.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 Craton Software Company
3
4//! `LoweredFunction → TensorWasmKernelBlueprint` adapter.
5//!
6//! Wave 2.9 of the Phase 1 Pliron pipeline. The wave-1 lowering passes
7//! produce a fine-grained [`crate::lowered_ir::LoweredFunction`] from
8//! Cranelift IR. The existing PTX emitter (S12), however, consumes the
9//! coarse-grained [`crate::ir::TensorWasmKernelBlueprint`]. Until wave 3
10//! lands a real `LoweredFunction → pliron::Operation → PTX` path, this
11//! adapter bridges the two by pattern-matching a small, well-known set
12//! of straight-line op sequences and emitting the corresponding
13//! blueprint.
14//!
15//! # Recognised patterns
16//!
17//! Single-block (straight-line) functions whose body is exactly
18//! a Load + arith + Store triple (terminated by a `Return`):
19//!
20//! | LoweredOp sequence                          | Blueprint op            |
21//! |---------------------------------------------|-------------------------|
22//! | `Load` + (`AddF` \| `AddI`) + `Store`       | `VecAdd { elem: ElemType::F32, lanes: 4 }`   |
23//! | `Load` + (`MulF` \| `MulI`) + `Store`       | `VecMul { elem: ElemType::F32, lanes: 4 }`   |
24//! | `Load` + `Fma` + `Store`                    | `VecFma { elem: ElemType::F32, lanes: 4 }`   |
25//!
26//! Lane count is fixed at `4` (f32x4 / i32x4) for wave 1. Wave 3 will
27//! derive it from the actual vector type carried by the `LoweredOp`s.
28//!
29//! `MatMul` detection is **deferred to wave 3** — it requires
30//! recognising nested loops or wmma-style tile builders, which the
31//! pattern-match approach in this module is too simple to handle.
32//!
33//! Anything else (multi-block, unsupported op mix, extra intermediates)
34//! returns a structured [`AdapterError`].
35
36#![cfg(feature = "cuda-oxide-backend")]
37
38use crate::ir::{ElemType, GridHint, TensorWasmKernelBlueprint, TensorWasmOp};
39use crate::lowered_ir::{LoweredFunction, LoweredOp, LoweredType};
40
41/// Default lane count for the wave-1 adapter. f32x4 / i32x4. Wave 3
42/// will derive this from the actual vector type the LoweredOps carry.
43const DEFAULT_LANES: u32 = 4;
44
45/// Map a scalar [`LoweredType`] to the blueprint [`ElemType`].
46///
47/// jit CRITICAL fix: the adapter previously discarded the arith op's type
48/// and always built an f32 blueprint, so an integer add lowered to a float
49/// kernel. The element type now flows from the `LoweredOp::Add*`/`Mul*`/`Fma`
50/// type field into the blueprint. Non-scalar / non-emittable types are
51/// failed closed by the caller.
52fn lowered_type_to_elem(ty: &LoweredType) -> Option<ElemType> {
53    match ty {
54        LoweredType::I8 => Some(ElemType::I8),
55        LoweredType::I16 => Some(ElemType::I16),
56        LoweredType::I32 => Some(ElemType::I32),
57        LoweredType::I64 => Some(ElemType::I64),
58        LoweredType::F32 => Some(ElemType::F32),
59        LoweredType::F64 => Some(ElemType::F64),
60        // Ptr / Bool / V128 have no blueprint element-type representation.
61        _ => None,
62    }
63}
64
65/// Errors produced by the LoweredFunction → blueprint adapter.
66#[derive(Debug, thiserror::Error, Clone, PartialEq, Eq)]
67pub enum AdapterError {
68    /// The LoweredFunction's op sequence didn't match any blueprint pattern
69    /// the wave-1 adapter recognizes. Wave 3 will replace the adapter with
70    /// real Pliron-based PTX emission, so this is not a permanent gap.
71    #[error("blueprint_adapter: function does not match a wave-1 blueprint pattern ({reason})")]
72    UnmatchedPattern {
73        /// Short description of what was unexpected (e.g. "no Load before
74        /// arith op", "MatMul not supported in wave 1").
75        reason: String,
76    },
77
78    /// The LoweredFunction had multiple blocks; wave-1 adapter only handles
79    /// single-block (straight-line) kernels.
80    #[error("blueprint_adapter: multi-block LoweredFunction not supported in wave 1 (got {block_count} blocks)")]
81    MultiBlockNotSupported {
82        /// Number of blocks in the input function.
83        block_count: usize,
84    },
85
86    /// The LoweredFunction was malformed (e.g. no terminator).
87    #[error("blueprint_adapter: malformed LoweredFunction")]
88    Malformed,
89}
90
91/// Convert a wave-1 [`LoweredFunction`] into a coarse-grained
92/// [`TensorWasmKernelBlueprint`] that the existing PTX emitter can render.
93///
94/// Recognises three blueprint patterns:
95/// - `Load` + `AddF`/`AddI` + `Store`  → `VecAdd`
96/// - `Load` + `MulF`/`MulI` + `Store`  → `VecMul`
97/// - `Load` + `Fma`         + `Store`  → `VecFma`
98///
99/// All other `LoweredFunction` shapes return
100/// [`AdapterError::UnmatchedPattern`].
101///
102/// # Errors
103///
104/// - [`AdapterError::Malformed`] if the function has no blocks or its
105///   single block has no terminator.
106/// - [`AdapterError::MultiBlockNotSupported`] if the function has more
107///   than one block.
108/// - [`AdapterError::UnmatchedPattern`] if the single block's op
109///   sequence doesn't match one of the three recognised patterns.
110pub fn lowered_function_to_blueprint(
111    func: &LoweredFunction,
112) -> Result<TensorWasmKernelBlueprint, AdapterError> {
113    // Wave-1 only handles single-block straight-line kernels.
114    if func.blocks.is_empty() {
115        return Err(AdapterError::Malformed);
116    }
117    if func.blocks.len() > 1 {
118        return Err(AdapterError::MultiBlockNotSupported {
119            block_count: func.blocks.len(),
120        });
121    }
122
123    let block = &func.blocks[0];
124
125    // Block must end in a terminator. We also require that terminator to
126    // be `Return` for the patterns we recognise.
127    let last = block.ops.last().ok_or(AdapterError::Malformed)?;
128    if !last.is_terminator() {
129        return Err(AdapterError::Malformed);
130    }
131    if !matches!(last, LoweredOp::Return { .. }) {
132        return Err(AdapterError::UnmatchedPattern {
133            reason: format!(
134                "terminator is not Return ({} ops in block)",
135                block.ops.len()
136            ),
137        });
138    }
139
140    // The recognised patterns are exactly four ops:
141    //   [Load, arith, Store, Return]
142    // Anything else (zero arith ops, multiple arith ops, extra moves
143    // between Load and arith, etc.) is rejected.
144    if block.ops.len() != 4 {
145        return Err(AdapterError::UnmatchedPattern {
146            reason: format!(
147                "expected exactly 4 ops (Load, arith, Store, Return), got {}",
148                block.ops.len()
149            ),
150        });
151    }
152
153    // op[0] must be a Load.
154    let is_load = matches!(&block.ops[0], LoweredOp::Load { .. });
155    if !is_load {
156        return Err(AdapterError::UnmatchedPattern {
157            reason: "no Load before arith op".to_string(),
158        });
159    }
160
161    // op[2] must be a Store.
162    let is_store = matches!(&block.ops[2], LoweredOp::Store { .. });
163    if !is_store {
164        return Err(AdapterError::UnmatchedPattern {
165            reason: "no Store after arith op".to_string(),
166        });
167    }
168
169    // op[1] must be one of the recognised arith ops. The element type is
170    // derived from the op's `ty` field (jit CRITICAL fix) so an integer add
171    // produces an integer blueprint, not a silently-miscompiled f32 kernel.
172    let unsupported_elem = |ty: &LoweredType| AdapterError::UnmatchedPattern {
173        reason: format!("arith op element type `{ty}` has no blueprint representation"),
174    };
175    let (kernel_op, elem) = match &block.ops[1] {
176        LoweredOp::AddF { ty, .. } | LoweredOp::AddI { ty, .. } => {
177            let elem = lowered_type_to_elem(ty).ok_or_else(|| unsupported_elem(ty))?;
178            (
179                TensorWasmOp::VecAdd {
180                    elem,
181                    lanes: DEFAULT_LANES,
182                },
183                elem,
184            )
185        }
186        LoweredOp::MulF { ty, .. } | LoweredOp::MulI { ty, .. } => {
187            let elem = lowered_type_to_elem(ty).ok_or_else(|| unsupported_elem(ty))?;
188            (
189                TensorWasmOp::VecMul {
190                    elem,
191                    lanes: DEFAULT_LANES,
192                },
193                elem,
194            )
195        }
196        LoweredOp::Fma { ty, .. } => {
197            let elem = lowered_type_to_elem(ty).ok_or_else(|| unsupported_elem(ty))?;
198            (
199                TensorWasmOp::VecFma {
200                    elem,
201                    lanes: DEFAULT_LANES,
202                },
203                elem,
204            )
205        }
206        // MatMul detection is deferred to wave 3 — it would need
207        // recognising nested loops or wmma tile builders, which the
208        // pattern-match approach above is too simple to handle.
209        other => {
210            return Err(AdapterError::UnmatchedPattern {
211                reason: format!(
212                    "unsupported middle op: {} (wave 1 recognises Add/Mul/Fma only; \
213                     MatMul deferred to wave 3)",
214                    debug_op_name(other)
215                ),
216            });
217        }
218    };
219
220    // Loads/stores adopt the arith op's element type so the whole kernel is
221    // element-type-coherent.
222    let blueprint = TensorWasmKernelBlueprint {
223        entry: func.name.clone(),
224        ops: vec![
225            TensorWasmOp::LoadUnified {
226                elem,
227                lanes: DEFAULT_LANES,
228            },
229            kernel_op,
230            TensorWasmOp::StoreUnified {
231                elem,
232                lanes: DEFAULT_LANES,
233            },
234        ],
235        grid_hint: GridHint::default(),
236        shared_mem_bytes: 0,
237    };
238
239    Ok(blueprint)
240}
241
242/// Short variant name for an unsupported `LoweredOp`. Used only for
243/// constructing descriptive error messages — not part of any API
244/// contract.
245fn debug_op_name(op: &LoweredOp) -> &'static str {
246    match op {
247        LoweredOp::AddI { .. } => "AddI",
248        LoweredOp::SubI { .. } => "SubI",
249        LoweredOp::MulI { .. } => "MulI",
250        LoweredOp::DivS { .. } => "DivS",
251        LoweredOp::DivU { .. } => "DivU",
252        LoweredOp::RemS { .. } => "RemS",
253        LoweredOp::RemU { .. } => "RemU",
254        LoweredOp::AddF { .. } => "AddF",
255        LoweredOp::SubF { .. } => "SubF",
256        LoweredOp::MulF { .. } => "MulF",
257        LoweredOp::DivF { .. } => "DivF",
258        LoweredOp::Fma { .. } => "Fma",
259        LoweredOp::FNeg { .. } => "FNeg",
260        LoweredOp::FAbs { .. } => "FAbs",
261        LoweredOp::Load { .. } => "Load",
262        LoweredOp::Store { .. } => "Store",
263        LoweredOp::StackAlloc { .. } => "StackAlloc",
264        LoweredOp::Br { .. } => "Br",
265        LoweredOp::CondBr { .. } => "CondBr",
266        LoweredOp::Switch { .. } => "Switch",
267        LoweredOp::Return { .. } => "Return",
268        LoweredOp::VMin { .. } => "VMin",
269        LoweredOp::VMax { .. } => "VMax",
270        LoweredOp::VSplat { .. } => "VSplat",
271        LoweredOp::VSelect { .. } => "VSelect",
272        LoweredOp::VAllTrue { .. } => "VAllTrue",
273        LoweredOp::VAnyTrue { .. } => "VAnyTrue",
274        LoweredOp::Select { .. } => "Select",
275        LoweredOp::Bitcast { .. } => "Bitcast",
276        LoweredOp::TruncI { .. } => "TruncI",
277        LoweredOp::ExtendU { .. } => "ExtendU",
278        LoweredOp::ExtendS { .. } => "ExtendS",
279    }
280}
281
282#[cfg(test)]
283mod tests {
284    use super::*;
285    use crate::lowered_ir::{
286        LoweredBlock, LoweredFunction, LoweredOp, LoweredSignature, LoweredType,
287    };
288
289    /// Helper: build a single-block function `[load, mid, store, return]`.
290    fn straight_line_fn(name: &str, mid: LoweredOp) -> LoweredFunction {
291        let mut func = LoweredFunction::new(
292            name,
293            LoweredSignature {
294                params: vec![LoweredType::Ptr, LoweredType::Ptr],
295                returns: vec![],
296            },
297        );
298        let mut block = LoweredBlock::new(0);
299        block.params = vec![(1, LoweredType::Ptr), (2, LoweredType::Ptr)];
300        block.ops.push(LoweredOp::Load {
301            ty: LoweredType::F32,
302            base: 1,
303            offset: 0,
304            result: 10,
305        });
306        block.ops.push(mid);
307        block.ops.push(LoweredOp::Store {
308            ty: LoweredType::F32,
309            value: 11,
310            base: 2,
311            offset: 0,
312        });
313        block.ops.push(LoweredOp::Return { values: vec![] });
314        func.blocks.push(block);
315        func
316    }
317
318    #[test]
319    fn load_addf_store_becomes_vec_add() {
320        let func = straight_line_fn(
321            "vector_add",
322            LoweredOp::AddF {
323                ty: LoweredType::F32,
324                lhs: 10,
325                rhs: 10,
326                result: 11,
327            },
328        );
329        let bp = lowered_function_to_blueprint(&func).expect("should produce blueprint");
330        assert_eq!(bp.entry, "vector_add");
331        assert_eq!(
332            bp.ops,
333            vec![
334                TensorWasmOp::LoadUnified {
335                    elem: ElemType::F32,
336                    lanes: 4
337                },
338                TensorWasmOp::VecAdd {
339                    elem: ElemType::F32,
340                    lanes: 4
341                },
342                TensorWasmOp::StoreUnified {
343                    elem: ElemType::F32,
344                    lanes: 4
345                },
346            ]
347        );
348        assert_eq!(bp.shared_mem_bytes, 0);
349        assert_eq!(bp.grid_hint, GridHint::default());
350    }
351
352    #[test]
353    fn load_addi_store_also_becomes_vec_add() {
354        let func = straight_line_fn(
355            "int_add",
356            LoweredOp::AddI {
357                ty: LoweredType::I32,
358                lhs: 10,
359                rhs: 10,
360                result: 11,
361            },
362        );
363        let bp = lowered_function_to_blueprint(&func).expect("should produce blueprint");
364        assert!(matches!(
365            bp.ops[1],
366            TensorWasmOp::VecAdd {
367                elem: ElemType::F32,
368                lanes: 4
369            }
370        ));
371    }
372
373    #[test]
374    fn load_mulf_store_becomes_vec_mul() {
375        let func = straight_line_fn(
376            "vector_mul",
377            LoweredOp::MulF {
378                ty: LoweredType::F32,
379                lhs: 10,
380                rhs: 10,
381                result: 11,
382            },
383        );
384        let bp = lowered_function_to_blueprint(&func).expect("should produce blueprint");
385        assert_eq!(bp.entry, "vector_mul");
386        assert!(matches!(
387            bp.ops[1],
388            TensorWasmOp::VecMul {
389                elem: ElemType::F32,
390                lanes: 4
391            }
392        ));
393    }
394
395    #[test]
396    fn load_muli_store_also_becomes_vec_mul() {
397        let func = straight_line_fn(
398            "int_mul",
399            LoweredOp::MulI {
400                ty: LoweredType::I32,
401                lhs: 10,
402                rhs: 10,
403                result: 11,
404            },
405        );
406        let bp = lowered_function_to_blueprint(&func).expect("should produce blueprint");
407        assert!(matches!(
408            bp.ops[1],
409            TensorWasmOp::VecMul {
410                elem: ElemType::F32,
411                lanes: 4
412            }
413        ));
414    }
415
416    #[test]
417    fn load_fma_store_becomes_vec_fma() {
418        let func = straight_line_fn(
419            "vector_fma",
420            LoweredOp::Fma {
421                ty: LoweredType::F32,
422                a: 10,
423                b: 10,
424                c: 10,
425                result: 11,
426            },
427        );
428        let bp = lowered_function_to_blueprint(&func).expect("should produce blueprint");
429        assert_eq!(bp.entry, "vector_fma");
430        assert!(matches!(
431            bp.ops[1],
432            TensorWasmOp::VecFma {
433                elem: ElemType::F32,
434                lanes: 4
435            }
436        ));
437    }
438
439    #[test]
440    fn multi_block_rejected() {
441        let mut func = LoweredFunction::new(
442            "multi",
443            LoweredSignature {
444                params: vec![],
445                returns: vec![],
446            },
447        );
448        // Two blocks, both terminated.
449        let mut b0 = LoweredBlock::new(0);
450        b0.ops.push(LoweredOp::Br {
451            target: 1,
452            args: vec![],
453        });
454        let mut b1 = LoweredBlock::new(1);
455        b1.ops.push(LoweredOp::Return { values: vec![] });
456        func.blocks.push(b0);
457        func.blocks.push(b1);
458
459        let err = lowered_function_to_blueprint(&func).unwrap_err();
460        assert_eq!(err, AdapterError::MultiBlockNotSupported { block_count: 2 });
461    }
462
463    #[test]
464    fn unsupported_pattern_rejected_with_descriptive_reason() {
465        // Load + Bitcast + Store + Return — Bitcast is not a recognised
466        // arith op, so the adapter should reject with a UnmatchedPattern
467        // error mentioning the offending op.
468        let func = straight_line_fn(
469            "weird",
470            LoweredOp::Bitcast {
471                from_ty: LoweredType::F32,
472                to_ty: LoweredType::I32,
473                src: 10,
474                result: 11,
475            },
476        );
477        let err = lowered_function_to_blueprint(&func).unwrap_err();
478        match err {
479            AdapterError::UnmatchedPattern { reason } => {
480                assert!(
481                    reason.contains("Bitcast"),
482                    "reason should name the offending op, got: {reason}"
483                );
484            }
485            other => panic!("expected UnmatchedPattern, got {other:?}"),
486        }
487    }
488
489    #[test]
490    fn empty_function_is_malformed() {
491        let func = LoweredFunction::new("empty", LoweredSignature::default());
492        let err = lowered_function_to_blueprint(&func).unwrap_err();
493        assert_eq!(err, AdapterError::Malformed);
494    }
495
496    #[test]
497    fn block_without_terminator_is_malformed() {
498        // Single block with no ops at all → no last op → Malformed.
499        let mut func = LoweredFunction::new("bad", LoweredSignature::default());
500        func.blocks.push(LoweredBlock::new(0));
501        let err = lowered_function_to_blueprint(&func).unwrap_err();
502        assert_eq!(err, AdapterError::Malformed);
503    }
504
505    #[test]
506    fn extra_intermediate_op_rejected() {
507        // Load + AddF + Bitcast + Store + Return — five ops, with an
508        // extra Bitcast between the arith and the Store. Must NOT match
509        // (the adapter requires exactly Load+arith+Store+Return).
510        let mut func = LoweredFunction::new(
511            "extra",
512            LoweredSignature {
513                params: vec![LoweredType::Ptr],
514                returns: vec![],
515            },
516        );
517        let mut block = LoweredBlock::new(0);
518        block.ops.push(LoweredOp::Load {
519            ty: LoweredType::F32,
520            base: 1,
521            offset: 0,
522            result: 10,
523        });
524        block.ops.push(LoweredOp::AddF {
525            ty: LoweredType::F32,
526            lhs: 10,
527            rhs: 10,
528            result: 11,
529        });
530        block.ops.push(LoweredOp::Bitcast {
531            from_ty: LoweredType::F32,
532            to_ty: LoweredType::I32,
533            src: 11,
534            result: 12,
535        });
536        block.ops.push(LoweredOp::Store {
537            ty: LoweredType::I32,
538            value: 12,
539            base: 1,
540            offset: 0,
541        });
542        block.ops.push(LoweredOp::Return { values: vec![] });
543        func.blocks.push(block);
544
545        let err = lowered_function_to_blueprint(&func).unwrap_err();
546        assert!(matches!(err, AdapterError::UnmatchedPattern { .. }));
547    }
548
549    #[test]
550    fn adapter_error_displays_useful_messages() {
551        // Ensure the thiserror Display strings actually carry the structured
552        // data — surface tests so a future refactor of the error formats
553        // doesn't silently drop information callers rely on.
554        let e = AdapterError::MultiBlockNotSupported { block_count: 3 };
555        let s = e.to_string();
556        assert!(s.contains("multi-block"));
557        assert!(s.contains('3'));
558
559        let e = AdapterError::UnmatchedPattern {
560            reason: "no Load before arith op".to_string(),
561        };
562        let s = e.to_string();
563        assert!(s.contains("no Load before arith op"));
564    }
565}