Skip to main content

tensor_wasm_jit/
lowering_driver.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 Craton Software Company
3
4//! Module-level lowering driver (wave-2 task W2.4).
5//!
6//! Walks a [`cranelift_codegen::ir::Function`], dispatches each instruction
7//! to the wave-1 per-family lowerers (L1-L6: arith / float / memory / cf /
8//! vector / conv), and assembles a [`LoweredFunction`].
9//!
10//! # Per-family dispatch
11//!
12//! The wave-1 lowerings were written *before* this driver. Each has a
13//! slightly different signature shape — some take `&mut` views of the value
14//! map (the arith family inserts its own result mapping), some take `&`
15//! views (float / vector / conv allocate a result id but leave the
16//! caller to bind the Cranelift result `Value` to it). The driver papers
17//! over those inconsistencies:
18//!
19//! - [`crate::lower_arith::lower_arith_inst`] — `&mut HashMap`, `&mut next_id`,
20//!   `Option<LoweredOp>`. **Self-binds** the result.
21//! - [`crate::lower_float::lower_float_inst`] — `&HashMap`, `&mut next_id`,
22//!   `Option<LoweredOp>`. Driver binds the result.
23//! - [`crate::lower_memory::lower_memory_inst`] — takes a
24//!   [`crate::lower_memory::MemLowerContext`], returns
25//!   `Result<Option<Vec<LoweredOp>>, MemLowerError>`. Driver binds the
26//!   load result (the last emitted op's `result()` if the Cranelift inst
27//!   produces a value).
28//! - [`crate::lower_cf::lower_cf_inst`] — `&HashMap, &HashMap`,
29//!   `Option<LoweredOp>`. No result to bind (cf ops are terminators).
30//! - [`crate::lower_vector::lower_vector_inst`] — `&HashMap`, `&mut next_id`,
31//!   `Option<LoweredOp>`. Driver binds the result.
32//! - [`crate::lower_conv::lower_conv_inst`] — `&HashMap`, `&mut next_id`,
33//!   `Option<LoweredOp>`. Driver binds the result.
34//!
35//! Families are tried in the order arith → float → memory → cf → vector →
36//! conv. The first one that returns a successful match wins. If none match,
37//! the driver returns [`LoweringError::UnsupportedOpcode`].
38
39#![cfg(feature = "cuda-oxide-backend")]
40
41use std::collections::HashMap;
42
43use cranelift_codegen::entity::EntityRef;
44use cranelift_codegen::ir as cl;
45
46use crate::lower_arith::lower_arith_inst;
47use crate::lower_cf::lower_cf_inst;
48use crate::lower_conv::lower_conv_inst;
49use crate::lower_float::lower_float_inst;
50use crate::lower_memory::{lower_memory_inst, MemLowerContext, MemLowerError};
51use crate::lower_signature::lower_signature;
52use crate::lower_vector::lower_vector_inst;
53use crate::lowered_ir::{LoweredBlock, LoweredFunction, LoweredOp, LoweredValueId};
54use crate::lowering_builder::LoweringBuilder;
55use crate::lowering_errors::{InstLocation, LoweringError};
56
57/// Lower a complete Cranelift [`cl::Function`] to a [`LoweredFunction`].
58///
59/// Walks the function in layout order, dispatches each instruction through
60/// the wave-1 per-family lowerers (arith → float → memory → cf → vector →
61/// conv), and assembles the resulting [`LoweredOp`]s into per-block
62/// [`LoweredBlock`]s.
63///
64/// # Errors
65///
66/// - [`LoweringError::UnsupportedOpcode`] when no wave-1 family matches an
67///   instruction. (Signature errors are also surfaced via this variant
68///   pending the W2.10 `LoweringError::Signature(#[from] ...)` follow-up;
69///   they carry the signature error's `Display` text in the `op` field.)
70/// - [`LoweringError::UnsupportedType`] when a memory-family op references
71///   a Cranelift type [`crate::lowered_ir::LoweredType`] cannot model.
72/// - [`LoweringError::UndefinedValue`] when a memory-family op references
73///   a Cranelift `Value` not yet in the value-map (driver bug or
74///   ill-formed input function).
75///
76/// # Notes
77///
78/// Block parameters of *non-entry* blocks are also seeded into the
79/// value-map up front — the wave-1 cf lowerings translate `BlockCall` args
80/// using the value map of the *call site*, but a downstream consumer that
81/// uses a block param as an operand needs that param to be in the map
82/// before its block is walked. We allocate ids for every block param in
83/// layout order before walking instructions.
84pub fn lower_function(func: &cl::Function) -> Result<LoweredFunction, LoweringError> {
85    // ---- 1. Lower the signature --------------------------------------
86    //
87    // Signature errors don't yet have a dedicated `LoweringError` variant
88    // (the `From<SignatureLoweringError>` `#[from]` impl is parked behind
89    // the W2.10 sync). For now we fold the signature error into
90    // `UnsupportedType` at a sentinel location — the message carries the
91    // signature error's `Display` text, which itself names the offending
92    // position and type.
93    let signature =
94        lower_signature(&func.signature).map_err(|err| LoweringError::UnsupportedType {
95            ty: err.to_string(),
96            location: InstLocation {
97                block: 0,
98                inst_index: 0,
99            },
100        })?;
101
102    // ---- 2. Reject-list preflight ------------------------------------
103    //
104    // jit fix (finding 10): the reject-list is now WIRED into every
105    // `lower_function` entry path, not a commented-out placeholder. This
106    // guarantees no function reaches the per-family lowerings (and the PTX
107    // emit / GPU launch behind them) without first passing the reject-list:
108    // floats, integer div/rem, loop back-edges, trap/unreachable, atomics,
109    // and host calls are refused up front (see `crate::reject_list`). The
110    // per-family lowerings remain a second line of defence (they return
111    // `None` for opcodes they don't recognise), but the reject-list is the
112    // single chokepoint that closes the "an entry path bypasses the
113    // soundness gate" hole.
114    if let Some(rejection) = crate::reject_list::check_function(func) {
115        return Err(LoweringError::Rejected {
116            reason: format!("{:?}", rejection.reason),
117            location: InstLocation::new(rejection.block, 0),
118        });
119    }
120
121    // ---- 3. Pre-allocate block ids in layout order -------------------
122    let mut builder = LoweringBuilder::new();
123    for block in func.layout.blocks() {
124        let _ = builder.get_or_alloc_block(block);
125    }
126
127    // ---- 4. Seed block params for every block ------------------------
128    //
129    // Every block param needs a `LoweredValueId` before we walk
130    // instructions: branch-arg lowering reads the param ids of the
131    // target block at the call site, but a block param also appears as
132    // an *operand* of instructions inside its own block. Allocating up
133    // front in layout order guarantees both lookups succeed.
134    let mut block_params: HashMap<cl::Block, Vec<(LoweredValueId, cl::Value)>> = HashMap::new();
135    for block in func.layout.blocks() {
136        let mut params = Vec::new();
137        for &param in func.dfg.block_params(block) {
138            let id = builder.get_or_alloc_value(param);
139            params.push((id, param));
140        }
141        block_params.insert(block, params);
142    }
143
144    // ---- 5. Walk each block, dispatch each instruction ---------------
145    //
146    // Each `LoweredBlock` carries its params (with their freshly-allocated
147    // ids and lowered types) and its ops. The cf lowering reads the
148    // builder's `block_map()` to resolve branch targets.
149    let mut lowered_blocks: Vec<LoweredBlock> = Vec::new();
150    for cl_block in func.layout.blocks() {
151        let block_id = builder
152            .lookup_block(cl_block)
153            .expect("block pre-allocated above");
154        let mut lblock = LoweredBlock::new(block_id);
155
156        // Carry the param `(id, lowered_type)` pairs into the block.
157        let params = block_params
158            .get(&cl_block)
159            .expect("block params seeded above");
160        for (id, cl_value) in params {
161            let cl_ty = func.dfg.value_type(*cl_value);
162            let lty =
163                crate::lower_signature::cranelift_type_to_lowered(cl_ty).ok_or_else(|| {
164                    LoweringError::UnsupportedType {
165                        ty: cl_ty.to_string(),
166                        location: InstLocation::new(cl_block, 0),
167                    }
168                })?;
169            lblock.params.push((*id, lty));
170        }
171
172        // Walk instructions in layout order. `enumerate` gives us the
173        // 0-based inst index used in diagnostics.
174        for (inst_index, inst) in func.layout.block_insts(cl_block).enumerate() {
175            let inst_index = inst_index as u32;
176            let location = InstLocation::new(cl_block, inst_index);
177
178            dispatch_inst(inst, func, &mut builder, &mut lblock, location)?;
179        }
180
181        lowered_blocks.push(lblock);
182    }
183
184    // ---- 6. Finalise -------------------------------------------------
185    let entry_cl_block = func
186        .layout
187        .entry_block()
188        .ok_or_else(|| LoweringError::MalformedTerminator { block_id: 0 })?;
189    let entry = builder
190        .lookup_block(entry_cl_block)
191        .expect("entry block pre-allocated above");
192
193    Ok(LoweredFunction {
194        name: function_name(func),
195        signature,
196        blocks: lowered_blocks,
197        entry,
198    })
199}
200
201/// Extract a human-readable name from a Cranelift function for use as the
202/// `LoweredFunction::name` (and thus the PTX entry symbol).
203///
204/// Cranelift's `UserFuncName::Testcase` carries a short byte-string that
205/// we surface via its `Display` impl (a leading `%` prefix); `User` carries
206/// a `(namespace, index)` pair which we render as `user<ns>_<idx>`. Names
207/// are best-effort diagnostic — fingerprinting in [`crate::ir`] hashes the
208/// [`LoweredFunction`] structure, not its name.
209fn function_name(func: &cl::Function) -> String {
210    match &func.name {
211        cl::UserFuncName::Testcase(t) => {
212            // `TestcaseName` doesn't expose its bytes publicly in
213            // cranelift-codegen 0.111; its `Display` impl renders the
214            // bytes as `%<utf8>`. We strip the leading `%` so the
215            // resulting name is a clean identifier.
216            let display = t.to_string();
217            display.strip_prefix('%').unwrap_or(&display).to_string()
218        }
219        cl::UserFuncName::User(u) => format!("user{}_{}", u.namespace, u.index),
220    }
221}
222
223/// Dispatch a single Cranelift instruction through the wave-1 per-family
224/// lowerers and append the resulting op(s) to `lblock`.
225///
226/// Returns [`LoweringError::UnsupportedOpcode`] if none of the six
227/// families recognises the instruction, or one of the other
228/// [`LoweringError`] variants for memory-family errors.
229fn dispatch_inst(
230    inst: cl::Inst,
231    func: &cl::Function,
232    builder: &mut LoweringBuilder,
233    lblock: &mut LoweredBlock,
234    location: InstLocation,
235) -> Result<(), LoweringError> {
236    // ---- L1: arith ---------------------------------------------------
237    //
238    // The arith family takes `&mut HashMap` + `&mut next_id` and inserts
239    // its own result mapping. Borrow split: we need to mutate both at
240    // once, so we pull them out together via `value_map_mut` and
241    // `next_value_id_mut`. Rust's borrow checker forbids two `&mut`s on
242    // the same `LoweringBuilder` simultaneously; the helper splits them
243    // into a single scope.
244    if let Some(op) = arith_attempt(inst, func, builder) {
245        lblock.ops.push(op);
246        return Ok(());
247    }
248
249    // ---- L2: float ---------------------------------------------------
250    if let Some(op) = float_attempt(inst, func, builder) {
251        bind_result_if_any(func, inst, &op, builder);
252        lblock.ops.push(op);
253        return Ok(());
254    }
255
256    // ---- L3: memory --------------------------------------------------
257    //
258    // Memory returns `Result<Option<Vec<...>>, MemLowerError>`. Errors
259    // surface as structured `LoweringError` variants; an `Ok(None)`
260    // means the family didn't match.
261    match memory_attempt(inst, func, builder, location.clone())? {
262        Some(ops) => {
263            // The Cranelift inst's first result (if any) needs binding to
264            // the last `result()`-bearing op in the emitted sequence. For
265            // a plain `load`, that's the Load itself. For `stack_load`,
266            // the emitted sequence is `[StackAlloc, Load]` and the Load's
267            // result is the one that maps to the Cranelift load result.
268            let cl_results = func.dfg.inst_results(inst);
269            if let Some(cl_result_v) = cl_results.first() {
270                if let Some(lid) = ops.iter().rev().find_map(LoweredOp::result) {
271                    builder.bind_value(*cl_result_v, lid);
272                }
273            }
274            for op in ops {
275                lblock.ops.push(op);
276            }
277            return Ok(());
278        }
279        None => {}
280    }
281
282    // ---- L4: control flow --------------------------------------------
283    //
284    // The cf family takes a read-only block_map. We snapshot it into a
285    // local so the borrow doesn't conflict with the rest of the builder.
286    if let Some(op) = cf_attempt(inst, func, builder) {
287        // cf ops are terminators — no result mapping needed.
288        lblock.ops.push(op);
289        return Ok(());
290    }
291
292    // ---- L5: vector --------------------------------------------------
293    if let Some(op) = vector_attempt(inst, func, builder) {
294        bind_result_if_any(func, inst, &op, builder);
295        lblock.ops.push(op);
296        return Ok(());
297    }
298
299    // ---- L6: conversion ---------------------------------------------
300    if let Some(op) = conv_attempt(inst, func, builder) {
301        bind_result_if_any(func, inst, &op, builder);
302        lblock.ops.push(op);
303        return Ok(());
304    }
305
306    // No family matched — this is the "we forgot to wire it up" path.
307    let opcode = func.dfg.insts[inst].opcode();
308    Err(LoweringError::UnsupportedOpcode {
309        op: format!("{opcode:?}"),
310        location,
311    })
312}
313
314/// Bind the Cranelift result `Value` of `inst` (if any) to the lowered op's
315/// `result()` id.
316///
317/// Called for families that allocate a result id but leave the binding to
318/// the caller (float / vector / conv). A no-op when the Cranelift inst
319/// produces no result or when the lowered op carries no result.
320fn bind_result_if_any(
321    func: &cl::Function,
322    inst: cl::Inst,
323    op: &LoweredOp,
324    builder: &mut LoweringBuilder,
325) {
326    if let Some(lid) = op.result() {
327        if let Some(&cl_result_v) = func.dfg.inst_results(inst).first() {
328            builder.bind_value(cl_result_v, lid);
329        }
330    }
331}
332
333// ---- Per-family attempt helpers --------------------------------------
334//
335// Each `*_attempt` helper exists to keep the borrows on `LoweringBuilder`
336// scoped to a single statement. Without these, the dispatch function
337// would have to juggle simultaneous `&mut`/`&` borrows of the same
338// builder across all six families in one big match expression.
339
340fn arith_attempt(
341    inst: cl::Inst,
342    func: &cl::Function,
343    builder: &mut LoweringBuilder,
344) -> Option<LoweredOp> {
345    // arith needs `&mut HashMap` + `&mut next_id`. The borrow checker
346    // refuses to hand out two `&mut` views into the same struct at
347    // once, so we go through a single split helper that returns both
348    // borrows from one call.
349    //
350    // `LoweringBuilder` doesn't expose such a helper directly, but
351    // `value_map_mut()` borrows `&mut self` and so does
352    // `next_value_id_mut()`. We sidestep this by calling them in
353    // sequence within a single `unsafe`-free block: we lift the
354    // `next_id` value into a local, run the family, then write the
355    // updated counter back.
356    let mut next_id_local: LoweredValueId = *builder.next_value_id_mut();
357    let result = lower_arith_inst(inst, func, builder.value_map_mut(), &mut next_id_local);
358    *builder.next_value_id_mut() = next_id_local;
359    result
360}
361
362fn float_attempt(
363    inst: cl::Inst,
364    func: &cl::Function,
365    builder: &mut LoweringBuilder,
366) -> Option<LoweredOp> {
367    let mut next_id_local: LoweredValueId = *builder.next_value_id_mut();
368    let result = lower_float_inst(inst, func, builder.value_map(), &mut next_id_local);
369    *builder.next_value_id_mut() = next_id_local;
370    result
371}
372
373fn memory_attempt(
374    inst: cl::Inst,
375    func: &cl::Function,
376    builder: &mut LoweringBuilder,
377    location: InstLocation,
378) -> Result<Option<Vec<LoweredOp>>, LoweringError> {
379    // jit MED fix (finding 6): use the builder's split-borrow accessor
380    // instead of cloning the whole `value_map` on every memory
381    // instruction. The previous code cloned `value_map` (O(values)) once
382    // per memory instruction (O(memory-instructions)) — i.e. O(V·M) total —
383    // and lifted the stack-slot map into a local that then had to be
384    // reinserted (jit finding 3's workaround). With the split borrow the
385    // memory context borrows the live maps directly: no per-instruction
386    // allocation, and any stack slot the memory lowering allocates is
387    // written straight into the builder's own map, so the one-slot-one-
388    // alloca property (jit finding 3) holds with no reinsert step.
389    let result = {
390        let (value_map, stack_slot_map, next_value_id) = builder.split_borrow_for_memory();
391        let mut ctx = MemLowerContext {
392            // The wave-1 memory lowering does not actually read this
393            // field yet; 0 is a placeholder. Wave 2's kernel-arg
394            // refinement will populate it properly.
395            linear_memory_base: 0,
396            value_map,
397            stack_slot_map,
398            next_value_id,
399        };
400        lower_memory_inst(inst, func, &mut ctx)
401    };
402
403    match result {
404        Ok(some_ops) => Ok(some_ops),
405        Err(MemLowerError::UnmappedValue(v)) => {
406            Err(LoweringError::UndefinedValue { value: v, location })
407        }
408        Err(MemLowerError::UnsupportedType(ty)) => {
409            Err(LoweringError::UnsupportedType { ty, location })
410        }
411    }
412}
413
414fn cf_attempt(
415    inst: cl::Inst,
416    func: &cl::Function,
417    builder: &mut LoweringBuilder,
418) -> Option<LoweredOp> {
419    // `lower_cf_inst` takes `&HashMap, &HashMap`. We can hand it the
420    // builder's accessor refs directly.
421    lower_cf_inst(inst, func, builder.value_map(), builder.block_map())
422}
423
424fn vector_attempt(
425    inst: cl::Inst,
426    func: &cl::Function,
427    builder: &mut LoweringBuilder,
428) -> Option<LoweredOp> {
429    let mut next_id_local: LoweredValueId = *builder.next_value_id_mut();
430    let result = lower_vector_inst(inst, func, builder.value_map(), &mut next_id_local);
431    *builder.next_value_id_mut() = next_id_local;
432    result
433}
434
435fn conv_attempt(
436    inst: cl::Inst,
437    func: &cl::Function,
438    builder: &mut LoweringBuilder,
439) -> Option<LoweredOp> {
440    let mut next_id_local: LoweredValueId = *builder.next_value_id_mut();
441    let result = lower_conv_inst(inst, func, builder.value_map(), &mut next_id_local);
442    *builder.next_value_id_mut() = next_id_local;
443    result
444}
445
446// Silence the unused-import lint for `EntityRef`; it's pulled in for
447// readability when reading `block.index()` in adjacent code paths, but the
448// driver itself only calls `EntityRef` transitively through
449// `InstLocation::new`.
450#[allow(dead_code)]
451fn _entity_ref_witness() -> &'static str {
452    let _: fn(cl::Block) -> usize = |b| b.index();
453    "ok"
454}
455
456#[cfg(test)]
457mod tests {
458    use super::*;
459    use crate::lowered_ir::{LoweredOp, LoweredType};
460    use crate::lowering_test_support::function_with_binary_op;
461    use cranelift_codegen::cursor::{Cursor, FuncCursor};
462    use cranelift_codegen::ir::{
463        types, AbiParam, BlockCall, Function, InstBuilder, InstructionData, Opcode, Signature,
464        UserFuncName,
465    };
466    use cranelift_codegen::isa::CallConv;
467
468    /// `lower_function` on `fn(i32, i32) -> i32 { iadd; return }` produces
469    /// one block, two ops `[AddI, Return]`, and the signature `(I32, I32)
470    /// -> I32`.
471    #[test]
472    fn lowers_single_block_iadd_return() {
473        let (func, _inst) = function_with_binary_op(Opcode::Iadd, types::I32);
474        let lowered = lower_function(&func).expect("must lower");
475
476        assert_eq!(lowered.blocks.len(), 1, "one block expected");
477        assert_eq!(
478            lowered.signature.params,
479            vec![LoweredType::I32, LoweredType::I32]
480        );
481        assert_eq!(lowered.signature.returns, vec![LoweredType::I32]);
482
483        let block = &lowered.blocks[0];
484        assert_eq!(block.ops.len(), 2, "AddI + Return");
485        assert!(matches!(block.ops[0], LoweredOp::AddI { .. }));
486        assert!(matches!(block.ops[1], LoweredOp::Return { .. }));
487        // Well-formed: ends in a terminator.
488        assert!(block.is_well_formed());
489        assert!(lowered.is_well_formed());
490    }
491
492    /// Same shape, but `fadd` over `F32` → ops `[AddF, Return]`.
493    #[test]
494    fn lowers_single_block_fadd_return() {
495        let (func, _inst) = function_with_binary_op(Opcode::Fadd, types::F32);
496        let lowered = lower_function(&func).expect("must lower");
497
498        assert_eq!(lowered.blocks.len(), 1);
499        assert_eq!(
500            lowered.signature.params,
501            vec![LoweredType::F32, LoweredType::F32]
502        );
503        assert_eq!(lowered.signature.returns, vec![LoweredType::F32]);
504
505        let block = &lowered.blocks[0];
506        assert_eq!(block.ops.len(), 2);
507        match &block.ops[0] {
508            LoweredOp::AddF { ty, .. } => assert_eq!(*ty, LoweredType::F32),
509            other => panic!("expected AddF, got {other:?}"),
510        }
511        assert!(matches!(block.ops[1], LoweredOp::Return { .. }));
512    }
513
514    /// An opcode no family handles must surface as `UnsupportedOpcode`.
515    ///
516    /// We use `Opcode::Iconst` (a `UnaryImm` form) — it's intentionally
517    /// outside every wave-1 family's match set (arith handles binary ints,
518    /// not constants; conv handles type changes, not literals; the others
519    /// don't touch immediates).
520    #[test]
521    fn errors_on_unsupported_opcode_iconst() {
522        let mut sig = Signature::new(CallConv::SystemV);
523        sig.returns.push(AbiParam::new(types::I32));
524        let mut func =
525            Function::with_name_signature(UserFuncName::testcase("iconst_fn".as_bytes()), sig);
526        let block = func.dfg.make_block();
527        func.layout.append_block(block);
528
529        let mut cursor = FuncCursor::new(&mut func).at_bottom(block);
530        let v = cursor.ins().iconst(types::I32, 7);
531        cursor.ins().return_(&[v]);
532
533        let err = lower_function(&func).expect_err("iconst is not in any wave-1 family");
534        match err {
535            LoweringError::UnsupportedOpcode { op, .. } => {
536                assert!(
537                    op.contains("Iconst"),
538                    "expected Iconst in the error op, got {op:?}",
539                );
540            }
541            other => panic!("expected UnsupportedOpcode, got {other:?}"),
542        }
543    }
544
545    /// Multi-block function: entry block jumps to a second block which
546    /// returns. The lowered function must have two blocks, both with
547    /// well-formed terminators, and the entry id must match the entry
548    /// block's allocated id.
549    #[test]
550    fn lowers_multi_block_jump_then_return() {
551        let mut sig = Signature::new(CallConv::SystemV);
552        sig.params.push(AbiParam::new(types::I32));
553        sig.returns.push(AbiParam::new(types::I32));
554        let mut func =
555            Function::with_name_signature(UserFuncName::testcase("two_block".as_bytes()), sig);
556
557        // Two blocks. Entry takes one i32 param; the second block also
558        // takes one i32 param (which we'll forward via the jump's block
559        // args). The second block returns the value it received.
560        let entry = func.dfg.make_block();
561        let entry_param = func.dfg.append_block_param(entry, types::I32);
562        let next = func.dfg.make_block();
563        let next_param = func.dfg.append_block_param(next, types::I32);
564
565        func.layout.append_block(entry);
566        func.layout.append_block(next);
567
568        // Entry: jump next(entry_param).
569        let block_call = BlockCall::new(next, &[entry_param], &mut func.dfg.value_lists);
570        let jump_inst = func.dfg.make_inst(InstructionData::Jump {
571            opcode: Opcode::Jump,
572            destination: block_call,
573        });
574        func.dfg.make_inst_results(jump_inst, types::INVALID);
575        func.layout.append_inst(jump_inst, entry);
576
577        // Next: return next_param.
578        {
579            let mut cursor = FuncCursor::new(&mut func).at_bottom(next);
580            cursor.ins().return_(&[next_param]);
581        }
582
583        let lowered = lower_function(&func).expect("multi-block must lower");
584        assert_eq!(lowered.blocks.len(), 2, "two blocks expected");
585
586        // Entry block must have an op `Br { target: <next-id> }`.
587        let entry_lblock = &lowered.blocks[0];
588        assert_eq!(entry_lblock.ops.len(), 1, "entry has one op (Br)");
589        match &entry_lblock.ops[0] {
590            LoweredOp::Br { target, args } => {
591                assert_eq!(*target, lowered.blocks[1].id);
592                assert_eq!(args.len(), 1);
593            }
594            other => panic!("expected Br, got {other:?}"),
595        }
596
597        // Next block must have a Return op.
598        let next_lblock = &lowered.blocks[1];
599        assert_eq!(next_lblock.ops.len(), 1, "next has one op (Return)");
600        assert!(matches!(next_lblock.ops[0], LoweredOp::Return { .. }));
601
602        // Well-formed overall.
603        assert!(lowered.is_well_formed());
604        // The entry id of the lowered function must equal the first block's id.
605        assert_eq!(lowered.entry, entry_lblock.id);
606    }
607
608    /// An empty `Function::new()` has no entry block → must surface as
609    /// `MalformedTerminator { block_id: 0 }` (the closest existing
610    /// variant — wave 2 doesn't yet carry a dedicated "no entry block"
611    /// error).
612    #[test]
613    fn errors_on_function_without_entry_block() {
614        // SystemV is the workspace default that `lower_signature` accepts,
615        // so the signature step passes; the failure surfaces at the
616        // entry-block lookup.
617        let func = Function::new();
618        let err = lower_function(&func).expect_err("no entry block");
619        match err {
620            LoweringError::MalformedTerminator { block_id } => {
621                assert_eq!(block_id, 0);
622            }
623            other => panic!("expected MalformedTerminator, got {other:?}"),
624        }
625    }
626
627    /// Signature errors surface through the driver. An I128 param can't
628    /// be lowered; the driver wraps the signature error's display string
629    /// in an `UnsupportedType` variant at a sentinel location.
630    #[test]
631    fn errors_on_unsupported_signature_type() {
632        let mut sig = Signature::new(CallConv::SystemV);
633        sig.params.push(AbiParam::new(types::I128));
634        let func = Function::with_name_signature(UserFuncName::default(), sig);
635
636        let err = lower_function(&func).expect_err("I128 param must reject");
637        match err {
638            LoweringError::UnsupportedType { ty, .. } => {
639                assert!(
640                    ty.contains("i128"),
641                    "expected i128 in the error display, got {ty:?}",
642                );
643            }
644            other => panic!("expected UnsupportedType, got {other:?}"),
645        }
646    }
647
648    /// Sanity: the `function_name` helper handles `UserFuncName::Testcase`
649    /// and `UserFuncName::User` shapes.
650    #[test]
651    fn function_name_renders_testcase_and_user() {
652        let f1 = Function::with_name_signature(
653            UserFuncName::testcase("k1".as_bytes()),
654            Signature::new(CallConv::SystemV),
655        );
656        assert_eq!(function_name(&f1), "k1");
657
658        let f2 = Function::with_name_signature(
659            UserFuncName::user(2, 5),
660            Signature::new(CallConv::SystemV),
661        );
662        assert_eq!(function_name(&f2), "user2_5");
663    }
664}