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}