Skip to main content

tensor_wasm_jit/
lowered_ir.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 Craton Software Company
3
4//! `LoweredOp` — the pure-Rust interim IR between Cranelift IR and Pliron
5//! `dialect-mir`.
6//!
7//! Wave 1 of the Phase 1 Pliron pipeline (RFC 0001 "Future possibilities")
8//! builds this interim IR. It mirrors the [Cranelift → dialect-mir mapping
9//! table](crate::pliron_dialect#mapping-table) exactly, but in pure Rust
10//! types so the lowering passes can land *before* the `pliron` git-pinned
11//! dep is added in wave 3.
12//!
13//! # Why an interim IR
14//!
15//! Two reasons:
16//!
17//! 1. **Pliron's crate is alpha and git-pinned.** Coding directly against
18//!    its `Operation` / `Module` types today exposes every lowering pass
19//!    to upstream API churn until cuda-oxide ≥ 0.2.0 ships. The interim
20//!    IR is a stable contract under our control.
21//! 2. **The lowering passes are independently testable.** Each
22//!    `lower_*` module (arith, float, memory, cf, vector, conv) returns a
23//!    `LoweredOp` and can be unit-tested without dragging in any GPU
24//!    toolchain. Wave 3 adds a single `LoweredOp -> pliron::Operation`
25//!    converter and the rest of the pipeline downstream stays the same.
26//!
27//! # SSA shape
28//!
29//! [`LoweredFunction`] is in pre-SSA form: every op names its operand
30//! [`LoweredValueId`]s and its result [`LoweredValueId`] explicitly. Block
31//! params carry types. This matches what Cranelift's [`ir::Function`]
32//! gives us per-instruction and lets us defer SSA→registers (the
33//! downstream `mem2reg` pass in RFC 0001's pipeline) to wave 3.
34
35#![cfg(feature = "cuda-oxide-backend")]
36
37use std::fmt;
38
39/// Stable SSA value identifier inside a [`LoweredFunction`].
40///
41/// Allocated by the lowering pass when it walks a Cranelift function;
42/// not guaranteed to equal Cranelift's own `Value` numbering. The
43/// fingerprinting in [`crate::ir::TensorWasmKernelBlueprint::fingerprint`]
44/// uses the `LoweredFunction`'s structure (op order, types, edges), not
45/// these numeric ids, so the ids being non-stable across runs is safe.
46pub type LoweredValueId = u32;
47
48/// Stable basic-block identifier inside a [`LoweredFunction`].
49pub type LoweredBlockId = u32;
50
51/// A scalar or vector type carried by a [`LoweredOp`] operand or result.
52///
53/// Mirrors the subset of Cranelift types that PTX can express. The wave 1
54/// lowering rejects any Cranelift type outside this set (see the
55/// "Unsupported in v0.4" notes in [`crate::pliron_dialect`]).
56#[derive(Debug, Clone, PartialEq, Eq, Hash)]
57pub enum LoweredType {
58    /// 8-bit integer.
59    I8,
60    /// 16-bit integer.
61    I16,
62    /// 32-bit integer.
63    I32,
64    /// 64-bit integer.
65    I64,
66    /// 32-bit IEEE-754 float.
67    F32,
68    /// 64-bit IEEE-754 float.
69    F64,
70    /// Opaque device pointer (unified-memory or device-resident).
71    Ptr,
72    /// Boolean — lowered to PTX `pred` register.
73    Bool,
74    /// Fixed-width SIMD vector of `lanes` lanes of `lane_type`.
75    ///
76    /// `lane_type` is restricted to scalar variants (I8..F64); nested
77    /// vectors are not legal. Lane counts mirror Cranelift's v128 = 4xF32
78    /// / 8xI16 / 16xI8 etc.
79    V128 {
80        /// Per-lane scalar type.
81        lane_type: Box<LoweredType>,
82        /// Lane count.
83        lanes: u8,
84    },
85}
86
87impl LoweredType {
88    /// True if the type is a scalar integer (any width).
89    pub fn is_int(&self) -> bool {
90        matches!(self, Self::I8 | Self::I16 | Self::I32 | Self::I64)
91    }
92
93    /// True if the type is a scalar float.
94    pub fn is_float(&self) -> bool {
95        matches!(self, Self::F32 | Self::F64)
96    }
97
98    /// True if the type is a SIMD vector.
99    pub fn is_vector(&self) -> bool {
100        matches!(self, Self::V128 { .. })
101    }
102
103    /// Bit width of a scalar type. Returns `None` for `Ptr`, `Bool`,
104    /// and vectors (callers should handle those explicitly).
105    pub fn scalar_bits(&self) -> Option<u32> {
106        match self {
107            Self::I8 => Some(8),
108            Self::I16 => Some(16),
109            Self::I32 | Self::F32 => Some(32),
110            Self::I64 | Self::F64 => Some(64),
111            _ => None,
112        }
113    }
114}
115
116impl fmt::Display for LoweredType {
117    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
118        match self {
119            Self::I8 => f.write_str("i8"),
120            Self::I16 => f.write_str("i16"),
121            Self::I32 => f.write_str("i32"),
122            Self::I64 => f.write_str("i64"),
123            Self::F32 => f.write_str("f32"),
124            Self::F64 => f.write_str("f64"),
125            Self::Ptr => f.write_str("ptr"),
126            Self::Bool => f.write_str("bool"),
127            Self::V128 { lane_type, lanes } => write!(f, "v{lanes}x{lane_type}"),
128        }
129    }
130}
131
132/// A single lowered operation.
133///
134/// Each variant maps to one row of the [Cranelift → dialect-mir mapping
135/// table](crate::pliron_dialect#mapping-table). The variants are grouped
136/// by lowering family so the per-family `lower_*` modules add cleanly.
137#[derive(Debug, Clone, PartialEq)]
138pub enum LoweredOp {
139    // ---- Arith family (lower_arith) -------------------------------------
140    /// Integer add. Wrap on overflow (Cranelift semantics).
141    AddI {
142        /// Operand/result type.
143        ty: LoweredType,
144        /// Left operand.
145        lhs: LoweredValueId,
146        /// Right operand.
147        rhs: LoweredValueId,
148        /// Result.
149        result: LoweredValueId,
150    },
151    /// Integer subtract.
152    SubI {
153        /// Operand/result type.
154        ty: LoweredType,
155        /// Left operand.
156        lhs: LoweredValueId,
157        /// Right operand.
158        rhs: LoweredValueId,
159        /// Result.
160        result: LoweredValueId,
161    },
162    /// Integer multiply.
163    MulI {
164        /// Operand/result type.
165        ty: LoweredType,
166        /// Left operand.
167        lhs: LoweredValueId,
168        /// Right operand.
169        rhs: LoweredValueId,
170        /// Result.
171        result: LoweredValueId,
172    },
173    /// Signed integer divide.
174    DivS {
175        /// Operand/result type.
176        ty: LoweredType,
177        /// Dividend.
178        lhs: LoweredValueId,
179        /// Divisor.
180        rhs: LoweredValueId,
181        /// Quotient.
182        result: LoweredValueId,
183    },
184    /// Unsigned integer divide.
185    DivU {
186        /// Operand/result type.
187        ty: LoweredType,
188        /// Dividend.
189        lhs: LoweredValueId,
190        /// Divisor.
191        rhs: LoweredValueId,
192        /// Quotient.
193        result: LoweredValueId,
194    },
195    /// Signed integer remainder.
196    RemS {
197        /// Operand/result type.
198        ty: LoweredType,
199        /// Dividend.
200        lhs: LoweredValueId,
201        /// Divisor.
202        rhs: LoweredValueId,
203        /// Remainder.
204        result: LoweredValueId,
205    },
206    /// Unsigned integer remainder.
207    RemU {
208        /// Operand/result type.
209        ty: LoweredType,
210        /// Dividend.
211        lhs: LoweredValueId,
212        /// Divisor.
213        rhs: LoweredValueId,
214        /// Remainder.
215        result: LoweredValueId,
216    },
217
218    // ---- Float family (lower_float) -------------------------------------
219    /// IEEE-754 float add.
220    AddF {
221        /// Operand/result type (F32 or F64).
222        ty: LoweredType,
223        /// Left operand.
224        lhs: LoweredValueId,
225        /// Right operand.
226        rhs: LoweredValueId,
227        /// Result.
228        result: LoweredValueId,
229    },
230    /// IEEE-754 float subtract.
231    SubF {
232        /// Operand/result type (F32 or F64).
233        ty: LoweredType,
234        /// Left operand.
235        lhs: LoweredValueId,
236        /// Right operand.
237        rhs: LoweredValueId,
238        /// Result.
239        result: LoweredValueId,
240    },
241    /// IEEE-754 float multiply.
242    MulF {
243        /// Operand/result type (F32 or F64).
244        ty: LoweredType,
245        /// Left operand.
246        lhs: LoweredValueId,
247        /// Right operand.
248        rhs: LoweredValueId,
249        /// Result.
250        result: LoweredValueId,
251    },
252    /// IEEE-754 float divide. PTX `div.rn.f32` rounding by default.
253    DivF {
254        /// Operand/result type (F32 or F64).
255        ty: LoweredType,
256        /// Dividend.
257        lhs: LoweredValueId,
258        /// Divisor.
259        rhs: LoweredValueId,
260        /// Quotient.
261        result: LoweredValueId,
262    },
263    /// Fused multiply-add: `result = a*b + c` (single rounding).
264    Fma {
265        /// Operand/result type (F32 or F64).
266        ty: LoweredType,
267        /// Multiplicand A.
268        a: LoweredValueId,
269        /// Multiplicand B.
270        b: LoweredValueId,
271        /// Addend.
272        c: LoweredValueId,
273        /// Result.
274        result: LoweredValueId,
275    },
276    /// Float negation.
277    FNeg {
278        /// Operand/result type (F32 or F64).
279        ty: LoweredType,
280        /// Source.
281        src: LoweredValueId,
282        /// Result.
283        result: LoweredValueId,
284    },
285    /// Float absolute value.
286    FAbs {
287        /// Operand/result type (F32 or F64).
288        ty: LoweredType,
289        /// Source.
290        src: LoweredValueId,
291        /// Result.
292        result: LoweredValueId,
293    },
294
295    // ---- Memory family (lower_memory) -----------------------------------
296    /// Load from device pointer `base + offset`.
297    Load {
298        /// Loaded type.
299        ty: LoweredType,
300        /// Base device pointer.
301        base: LoweredValueId,
302        /// Byte offset (signed).
303        offset: i32,
304        /// Result.
305        result: LoweredValueId,
306    },
307    /// Store `value` to device pointer `base + offset`.
308    Store {
309        /// Stored type.
310        ty: LoweredType,
311        /// Value to store.
312        value: LoweredValueId,
313        /// Base device pointer.
314        base: LoweredValueId,
315        /// Byte offset (signed).
316        offset: i32,
317    },
318    /// SSA-local alloca for Cranelift stack slots. Lowered to registers
319    /// by the downstream `mem2reg` pass.
320    StackAlloc {
321        /// Element type.
322        ty: LoweredType,
323        /// Size in bytes (for arrays); 0 means single-value.
324        bytes: u32,
325        /// SSA pointer to the alloca.
326        result: LoweredValueId,
327    },
328
329    // ---- Control flow family (lower_cf) ---------------------------------
330    /// Unconditional branch with block-arg passing.
331    Br {
332        /// Target block.
333        target: LoweredBlockId,
334        /// Values passed as block parameters to `target`.
335        args: Vec<LoweredValueId>,
336    },
337    /// Conditional branch.
338    CondBr {
339        /// Boolean condition.
340        cond: LoweredValueId,
341        /// Target when `cond` is true.
342        then_target: LoweredBlockId,
343        /// Args passed to the then-target.
344        then_args: Vec<LoweredValueId>,
345        /// Target when `cond` is false.
346        else_target: LoweredBlockId,
347        /// Args passed to the else-target.
348        else_args: Vec<LoweredValueId>,
349    },
350    /// Multi-way switch (Cranelift `br_table`).
351    Switch {
352        /// Value being switched on.
353        value: LoweredValueId,
354        /// Default target when no case matches.
355        default_target: LoweredBlockId,
356        /// `(case_value, target)` pairs.
357        cases: Vec<(u32, LoweredBlockId)>,
358    },
359    /// Return from the function.
360    Return {
361        /// Values returned (matches signature length).
362        values: Vec<LoweredValueId>,
363    },
364
365    // ---- Vector family (lower_vector) -----------------------------------
366    /// Per-lane minimum.
367    VMin {
368        /// Per-lane type.
369        lane_ty: LoweredType,
370        /// Signedness of the comparison for INTEGER lanes.
371        ///
372        /// jit MED fix (finding 5): Cranelift's `smin` (signed) and `umin`
373        /// (unsigned) previously both collapsed onto `VMin` with no
374        /// signedness, so an unsigned min would lower to a signed PTX `min`
375        /// (`min.s32` vs `min.u32`) — a miscompile for operands straddling
376        /// the sign boundary. `signed` now carries the distinction.
377        ///
378        /// For FLOAT lanes (`lane_ty.is_float()`) this field is ignored
379        /// (float min/max has no signedness); the lowering sets it to
380        /// `true` for floats by convention.
381        signed: bool,
382        /// Left operand vector.
383        lhs: LoweredValueId,
384        /// Right operand vector.
385        rhs: LoweredValueId,
386        /// Result vector.
387        result: LoweredValueId,
388    },
389    /// Per-lane maximum.
390    VMax {
391        /// Per-lane type.
392        lane_ty: LoweredType,
393        /// Signedness of the comparison for INTEGER lanes (see
394        /// [`LoweredOp::VMin`] — jit MED fix finding 5). Ignored for float
395        /// lanes.
396        signed: bool,
397        /// Left operand vector.
398        lhs: LoweredValueId,
399        /// Right operand vector.
400        rhs: LoweredValueId,
401        /// Result vector.
402        result: LoweredValueId,
403    },
404    /// Broadcast scalar across all lanes.
405    VSplat {
406        /// Per-lane type.
407        lane_ty: LoweredType,
408        /// Source scalar.
409        src: LoweredValueId,
410        /// Result vector.
411        result: LoweredValueId,
412    },
413    /// Per-lane select.
414    VSelect {
415        /// Per-lane type.
416        lane_ty: LoweredType,
417        /// Per-lane mask vector.
418        cond: LoweredValueId,
419        /// Value when mask lane is true.
420        then_v: LoweredValueId,
421        /// Value when mask lane is false.
422        else_v: LoweredValueId,
423        /// Result vector.
424        result: LoweredValueId,
425    },
426    /// Warp-wide AND reduction (Cranelift `vall_true`).
427    VAllTrue {
428        /// Source mask vector.
429        src: LoweredValueId,
430        /// Boolean result.
431        result: LoweredValueId,
432    },
433    /// Warp-wide OR reduction (Cranelift `vany_true`).
434    VAnyTrue {
435        /// Source mask vector.
436        src: LoweredValueId,
437        /// Boolean result.
438        result: LoweredValueId,
439    },
440
441    // ---- Conversion family (lower_conv) ---------------------------------
442    /// Three-operand select (Cranelift `select`). Maps to PTX `selp`.
443    Select {
444        /// Result type.
445        ty: LoweredType,
446        /// Boolean condition.
447        cond: LoweredValueId,
448        /// Value when true.
449        then_v: LoweredValueId,
450        /// Value when false.
451        else_v: LoweredValueId,
452        /// Result.
453        result: LoweredValueId,
454    },
455    /// Reinterpret bits. Free in PTX.
456    Bitcast {
457        /// Source type.
458        from_ty: LoweredType,
459        /// Destination type (same bit width).
460        to_ty: LoweredType,
461        /// Source.
462        src: LoweredValueId,
463        /// Result.
464        result: LoweredValueId,
465    },
466    /// Width-reducing integer truncation (Cranelift `ireduce`).
467    TruncI {
468        /// Source integer type (wider).
469        from_ty: LoweredType,
470        /// Destination integer type (narrower).
471        to_ty: LoweredType,
472        /// Source.
473        src: LoweredValueId,
474        /// Result.
475        result: LoweredValueId,
476    },
477    /// Zero-extending integer widening (Cranelift `uextend`).
478    ExtendU {
479        /// Source integer type (narrower).
480        from_ty: LoweredType,
481        /// Destination integer type (wider).
482        to_ty: LoweredType,
483        /// Source.
484        src: LoweredValueId,
485        /// Result.
486        result: LoweredValueId,
487    },
488    /// Sign-extending integer widening (Cranelift `sextend`).
489    ExtendS {
490        /// Source integer type (narrower).
491        from_ty: LoweredType,
492        /// Destination integer type (wider).
493        to_ty: LoweredType,
494        /// Source.
495        src: LoweredValueId,
496        /// Result.
497        result: LoweredValueId,
498    },
499}
500
501impl LoweredOp {
502    /// Result value id if this op produces one. `None` for `Store`, `Br`,
503    /// `CondBr`, `Switch`, `Return`.
504    pub fn result(&self) -> Option<LoweredValueId> {
505        match self {
506            Self::AddI { result, .. }
507            | Self::SubI { result, .. }
508            | Self::MulI { result, .. }
509            | Self::DivS { result, .. }
510            | Self::DivU { result, .. }
511            | Self::RemS { result, .. }
512            | Self::RemU { result, .. }
513            | Self::AddF { result, .. }
514            | Self::SubF { result, .. }
515            | Self::MulF { result, .. }
516            | Self::DivF { result, .. }
517            | Self::Fma { result, .. }
518            | Self::FNeg { result, .. }
519            | Self::FAbs { result, .. }
520            | Self::Load { result, .. }
521            | Self::StackAlloc { result, .. }
522            | Self::VMin { result, .. }
523            | Self::VMax { result, .. }
524            | Self::VSplat { result, .. }
525            | Self::VSelect { result, .. }
526            | Self::VAllTrue { result, .. }
527            | Self::VAnyTrue { result, .. }
528            | Self::Select { result, .. }
529            | Self::Bitcast { result, .. }
530            | Self::TruncI { result, .. }
531            | Self::ExtendU { result, .. }
532            | Self::ExtendS { result, .. } => Some(*result),
533            Self::Store { .. }
534            | Self::Br { .. }
535            | Self::CondBr { .. }
536            | Self::Switch { .. }
537            | Self::Return { .. } => None,
538        }
539    }
540
541    /// True if this op transfers control (terminator).
542    pub fn is_terminator(&self) -> bool {
543        matches!(
544            self,
545            Self::Br { .. } | Self::CondBr { .. } | Self::Switch { .. } | Self::Return { .. }
546        )
547    }
548}
549
550/// Signature of a [`LoweredFunction`]: param and return types.
551#[derive(Debug, Clone, PartialEq, Eq)]
552pub struct LoweredSignature {
553    /// Parameter types in order.
554    pub params: Vec<LoweredType>,
555    /// Return types (multi-return supported).
556    pub returns: Vec<LoweredType>,
557}
558
559impl LoweredSignature {
560    /// Construct an empty signature.
561    pub fn new() -> Self {
562        Self {
563            params: Vec::new(),
564            returns: Vec::new(),
565        }
566    }
567}
568
569impl Default for LoweredSignature {
570    fn default() -> Self {
571        Self::new()
572    }
573}
574
575/// One basic block in a [`LoweredFunction`].
576#[derive(Debug, Clone, PartialEq)]
577pub struct LoweredBlock {
578    /// Block identifier.
579    pub id: LoweredBlockId,
580    /// Block parameters: `(value_id, type)` pairs. Entry-block params come
581    /// from the function signature.
582    pub params: Vec<(LoweredValueId, LoweredType)>,
583    /// Operations in order. The last op must be a terminator (see
584    /// [`LoweredOp::is_terminator`]).
585    pub ops: Vec<LoweredOp>,
586}
587
588impl LoweredBlock {
589    /// Construct an empty block with the given id.
590    pub fn new(id: LoweredBlockId) -> Self {
591        Self {
592            id,
593            params: Vec::new(),
594            ops: Vec::new(),
595        }
596    }
597
598    /// True if the block ends in a terminator op.
599    pub fn is_well_formed(&self) -> bool {
600        self.ops
601            .last()
602            .map(LoweredOp::is_terminator)
603            .unwrap_or(false)
604    }
605}
606
607/// A complete lowered function, ready for the wave 3
608/// `LoweredFunction -> pliron::Operation` converter.
609#[derive(Debug, Clone, PartialEq)]
610pub struct LoweredFunction {
611    /// Function name. Used as the PTX entry symbol.
612    pub name: String,
613    /// Signature (param + return types).
614    pub signature: LoweredSignature,
615    /// Blocks in order. The entry block is identified by `entry`.
616    pub blocks: Vec<LoweredBlock>,
617    /// Entry block id.
618    pub entry: LoweredBlockId,
619}
620
621impl LoweredFunction {
622    /// Construct an empty function with the given name and signature.
623    pub fn new(name: impl Into<String>, signature: LoweredSignature) -> Self {
624        Self {
625            name: name.into(),
626            signature,
627            blocks: Vec::new(),
628            entry: 0,
629        }
630    }
631
632    /// Lookup a block by id.
633    pub fn block(&self, id: LoweredBlockId) -> Option<&LoweredBlock> {
634        self.blocks.iter().find(|b| b.id == id)
635    }
636
637    /// True if every block ends in a terminator.
638    pub fn is_well_formed(&self) -> bool {
639        !self.blocks.is_empty() && self.blocks.iter().all(LoweredBlock::is_well_formed)
640    }
641}
642
643#[cfg(test)]
644mod tests {
645    use super::*;
646
647    #[test]
648    fn lowered_type_scalar_bits() {
649        assert_eq!(LoweredType::I32.scalar_bits(), Some(32));
650        assert_eq!(LoweredType::F64.scalar_bits(), Some(64));
651        assert_eq!(LoweredType::Ptr.scalar_bits(), None);
652        assert_eq!(LoweredType::Bool.scalar_bits(), None);
653        assert_eq!(
654            LoweredType::V128 {
655                lane_type: Box::new(LoweredType::F32),
656                lanes: 4,
657            }
658            .scalar_bits(),
659            None
660        );
661    }
662
663    #[test]
664    fn lowered_type_classification() {
665        assert!(LoweredType::I32.is_int());
666        assert!(!LoweredType::I32.is_float());
667        assert!(LoweredType::F32.is_float());
668        assert!(!LoweredType::F32.is_int());
669        assert!(LoweredType::V128 {
670            lane_type: Box::new(LoweredType::F32),
671            lanes: 4
672        }
673        .is_vector());
674    }
675
676    #[test]
677    fn lowered_type_display() {
678        assert_eq!(LoweredType::I32.to_string(), "i32");
679        assert_eq!(LoweredType::F32.to_string(), "f32");
680        assert_eq!(LoweredType::Ptr.to_string(), "ptr");
681        let v = LoweredType::V128 {
682            lane_type: Box::new(LoweredType::F32),
683            lanes: 4,
684        };
685        assert_eq!(v.to_string(), "v4xf32");
686    }
687
688    #[test]
689    fn op_result_arith() {
690        let op = LoweredOp::AddI {
691            ty: LoweredType::I32,
692            lhs: 1,
693            rhs: 2,
694            result: 3,
695        };
696        assert_eq!(op.result(), Some(3));
697        assert!(!op.is_terminator());
698    }
699
700    #[test]
701    fn op_result_store() {
702        let op = LoweredOp::Store {
703            ty: LoweredType::I32,
704            value: 1,
705            base: 2,
706            offset: 0,
707        };
708        assert_eq!(op.result(), None);
709        assert!(!op.is_terminator());
710    }
711
712    #[test]
713    fn op_is_terminator() {
714        assert!(LoweredOp::Br {
715            target: 0,
716            args: vec![]
717        }
718        .is_terminator());
719        assert!(LoweredOp::Return { values: vec![] }.is_terminator());
720        assert!(LoweredOp::CondBr {
721            cond: 0,
722            then_target: 1,
723            then_args: vec![],
724            else_target: 2,
725            else_args: vec![]
726        }
727        .is_terminator());
728        assert!(LoweredOp::Switch {
729            value: 0,
730            default_target: 1,
731            cases: vec![]
732        }
733        .is_terminator());
734    }
735
736    #[test]
737    fn block_well_formed_requires_terminator() {
738        let mut block = LoweredBlock::new(0);
739        assert!(!block.is_well_formed());
740        block.ops.push(LoweredOp::AddI {
741            ty: LoweredType::I32,
742            lhs: 1,
743            rhs: 2,
744            result: 3,
745        });
746        assert!(!block.is_well_formed());
747        block.ops.push(LoweredOp::Return { values: vec![3] });
748        assert!(block.is_well_formed());
749    }
750
751    #[test]
752    fn function_lookup_and_well_formed() {
753        let mut func = LoweredFunction::new(
754            "kernel",
755            LoweredSignature {
756                params: vec![LoweredType::Ptr],
757                returns: vec![],
758            },
759        );
760        let mut b0 = LoweredBlock::new(0);
761        b0.ops.push(LoweredOp::Return { values: vec![] });
762        func.blocks.push(b0);
763        assert!(func.block(0).is_some());
764        assert!(func.block(99).is_none());
765        assert!(func.is_well_formed());
766    }
767
768    #[test]
769    fn empty_function_not_well_formed() {
770        let func = LoweredFunction::new("empty", LoweredSignature::default());
771        assert!(!func.is_well_formed());
772    }
773}