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 ¶m 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}