tensor_wasm_jit/lowering_test_support.rs
1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 Craton Software Company
3
4//! Reusable Cranelift IR test-fixture builders for the wave-1 lowering
5//! passes.
6//!
7//! Each helper here produces a small [`cranelift_codegen::ir::Function`]
8//! with the shape a single `lower_*` pass needs to exercise — an entry
9//! block whose params match the requested signature, one (or zero)
10//! instructions of interest, and a `return` terminator. The lowering
11//! passes (`lower_arith`, `lower_float`, `lower_memory`, `lower_cf`,
12//! `lower_vector`, `lower_conv`) consume these in their integration tests
13//! so they don't each re-invent the Cranelift-construction boilerplate.
14//!
15//! # Scope
16//!
17//! These builders **intentionally** do not use `cranelift-frontend`
18//! (which is not in `tensor-wasm-jit`'s `Cargo.toml`). They build
19//! functions by hand against the `cranelift-codegen` raw DFG + cursor
20//! API. The trade-off: the helpers are limited to single-block bodies
21//! with no SSA-from-frontend convenience. That is the right scope for
22//! wave-1 unit tests; the multi-block lowering tests in `lower_cf` build
23//! their fixtures inline.
24//!
25//! # Why fixtures here (vs inline in each `lower_*` module)
26//!
27//! Wave-1 agents L1-L6 wrote their first-cut fixtures inline because
28//! this module hadn't landed yet. Wave-2 integration tests (the
29//! detector → lowering → blueprint pipeline) need to share fixture
30//! shapes across passes, so this module is the long-term home. The
31//! inline fixtures in `lower_*` modules can migrate here in a future
32//! cleanup pass without changing the test logic — the function shapes
33//! these builders produce are the same shapes those tests already
34//! construct.
35
36#![cfg(feature = "cuda-oxide-backend")]
37
38use cranelift_codegen::cursor::{Cursor, FuncCursor};
39use cranelift_codegen::ir::immediates::Offset32;
40use cranelift_codegen::ir::{
41 AbiParam, Function, Inst, InstBuilder, MemFlags, Opcode, Signature, Type, UserFuncName,
42};
43use cranelift_codegen::isa::CallConv;
44
45/// Pointer-typed parameter used for `load` / `store` fixtures.
46///
47/// Cranelift addresses are integers, not a dedicated pointer type. The
48/// memory-lowering passes match on the 64-bit integer width because
49/// that's the address width PTX targets on sm_80+ (per
50/// [`crate::ptx_emit::DEFAULT_TARGET`]). Using a single named constant
51/// here keeps the `function_with_load` / `function_with_store` helpers
52/// and their unit tests aligned on the same width.
53const PTR_TY: Type = cranelift_codegen::ir::types::I64;
54
55/// Build an empty Cranelift function with the requested signature.
56///
57/// The returned [`Function`] has:
58///
59/// - A `SystemV` calling convention. (The actual CC doesn't matter for
60/// the IR-shape tests these fixtures support — the lowering passes
61/// don't inspect it. SystemV is the workspace default for "any sane
62/// CC will do".)
63/// - One entry block whose block-param list matches `params` 1:1 (same
64/// types, same order).
65/// - A terminating `return` of **zero values**. The caller is expected
66/// to either accept the empty return (e.g. void-returning functions)
67/// or to replace the terminator before lowering when a non-empty
68/// return is required. Replacing the terminator is uncommon in the
69/// wave-1 tests; most callers either use `function_with_*` (which
70/// already inserts a sensible return) or this helper for void
71/// fixtures.
72///
73/// The `name` argument is wrapped via [`UserFuncName::testcase`] so it
74/// shows up readably in `Function::display()` output during test
75/// failures.
76pub fn empty_function(name: &str, params: &[Type], returns: &[Type]) -> Function {
77 let mut sig = Signature::new(CallConv::SystemV);
78 for p in params {
79 sig.params.push(AbiParam::new(*p));
80 }
81 for r in returns {
82 sig.returns.push(AbiParam::new(*r));
83 }
84
85 let mut func = Function::with_name_signature(UserFuncName::testcase(name.as_bytes()), sig);
86 let block = func.dfg.make_block();
87 // Append block params 1:1 with the signature so the entry block is
88 // immediately usable by lowering passes that iterate block params.
89 let param_types: Vec<Type> = func.signature.params.iter().map(|p| p.value_type).collect();
90 for pty in param_types {
91 func.dfg.append_block_param(block, pty);
92 }
93 func.layout.append_block(block);
94
95 // Insert a zero-value `return` so the block has a terminator. Tests
96 // that need a non-empty return (e.g. function_with_binary_op) call
97 // their own InstBuilder::return_ instead of relying on this stub.
98 let mut cursor = FuncCursor::new(&mut func).at_bottom(block);
99 cursor.ins().return_(&[]);
100
101 func
102}
103
104/// Build a function `fn(lane_ty, lane_ty) -> lane_ty` whose entry block
105/// contains a single binary instruction of `opcode` over the two block
106/// params, followed by a return of the result.
107///
108/// Used by the arith / float / vector lowering tests to exercise
109/// per-opcode dispatch. The helper covers any opcode whose
110/// `InstructionFormat` is `Binary` (e.g. `iadd`, `isub`, `imul`, `fadd`,
111/// `fsub`, `fmul`, `fdiv`, `fma` — wait, `fma` is `Ternary`; the rule of
112/// thumb is "two operands, one result, no immediate"). Passing an
113/// opcode whose format is not `Binary` will panic in debug builds via
114/// `cranelift-codegen`'s own `debug_assert_eq!` inside the
115/// `InstBuilder::Binary` shim.
116///
117/// Returns the constructed [`Function`] and the [`Inst`] handle for the
118/// inserted binary op so tests can grep for it without re-walking the
119/// layout.
120pub fn function_with_binary_op(opcode: Opcode, lane_ty: Type) -> (Function, Inst) {
121 let mut func = empty_function_no_return("binop", &[lane_ty, lane_ty], &[lane_ty]);
122 let block = func.layout.entry_block().expect("entry block");
123 let params: Vec<_> = func.dfg.block_params(block).to_vec();
124 let p0 = params[0];
125 let p1 = params[1];
126
127 let mut cursor = FuncCursor::new(&mut func).at_bottom(block);
128 // Use the low-level `Binary` constructor so the helper can dispatch
129 // on a generic Opcode rather than naming one of cranelift's
130 // per-opcode shims. `ctrl_typevar = lane_ty` mirrors what
131 // `iadd` / `fadd` / etc. set internally.
132 let (inst, dfg) = cursor.ins().Binary(opcode, lane_ty, p0, p1);
133 let result = dfg.first_result(inst);
134 cursor.ins().return_(&[result]);
135
136 (func, inst)
137}
138
139/// Build a function `fn(lane_ty) -> lane_ty` whose entry block contains
140/// a single unary instruction of `opcode` over the single block param,
141/// followed by a return of the result.
142///
143/// Symmetric counterpart to [`function_with_binary_op`]; covers any
144/// opcode whose `InstructionFormat` is `Unary` (e.g. `fneg`, `fabs`,
145/// `ineg`, `bnot`). Same debug-assertion caveat applies.
146pub fn function_with_unary_op(opcode: Opcode, lane_ty: Type) -> (Function, Inst) {
147 let mut func = empty_function_no_return("unop", &[lane_ty], &[lane_ty]);
148 let block = func.layout.entry_block().expect("entry block");
149 let params: Vec<_> = func.dfg.block_params(block).to_vec();
150 let p0 = params[0];
151
152 let mut cursor = FuncCursor::new(&mut func).at_bottom(block);
153 let (inst, dfg) = cursor.ins().Unary(opcode, lane_ty, p0);
154 let result = dfg.first_result(inst);
155 cursor.ins().return_(&[result]);
156
157 (func, inst)
158}
159
160/// Build a function `fn(ptr) -> ty` whose entry block contains a single
161/// `load.<ty>` instruction reading from `(param0 + offset)`, followed
162/// by a return of the loaded value.
163///
164/// The pointer param is a 64-bit integer ([`PTR_TY`]) — see the
165/// constant's rustdoc for why we don't use a dedicated pointer type.
166/// `offset` is the byte displacement passed to Cranelift's `load`
167/// instruction; the lowering pass extracts it via
168/// `InstructionData::Load { offset, .. }`.
169///
170/// `MemFlags::trusted()` is used so the lowering doesn't have to model
171/// trap behaviour for the fixture — these tests are about IR shape, not
172/// trap semantics.
173pub fn function_with_load(ty: Type, offset: i32) -> (Function, Inst) {
174 let mut func = empty_function_no_return("load_fn", &[PTR_TY], &[ty]);
175 let block = func.layout.entry_block().expect("entry block");
176 let params: Vec<_> = func.dfg.block_params(block).to_vec();
177 let p0 = params[0];
178
179 let mut cursor = FuncCursor::new(&mut func).at_bottom(block);
180 // Use the low-level `Load` constructor so we get the `Inst` handle
181 // back directly. The shorthand `cursor.ins().load(...)` returns a
182 // `Value` and would require a follow-up `value_def` lookup.
183 let (inst, dfg) = cursor.ins().Load(
184 Opcode::Load,
185 ty,
186 MemFlags::trusted(),
187 Offset32::new(offset),
188 p0,
189 );
190 let result = dfg.first_result(inst);
191 cursor.ins().return_(&[result]);
192
193 (func, inst)
194}
195
196/// Build a function `fn(ty, ptr)` whose entry block contains a single
197/// `store.<ty>` instruction writing `param0` to `(param1 + offset)`,
198/// followed by an empty return.
199///
200/// Parameter order is `(value, pointer)` to match Cranelift's own
201/// `store(MemFlags, x, p, Offset)` builder signature — the convention
202/// the lowering passes already match against. `MemFlags::trusted()` is
203/// used for the same reason as in [`function_with_load`].
204pub fn function_with_store(ty: Type, offset: i32) -> (Function, Inst) {
205 let mut func = empty_function_no_return("store_fn", &[ty, PTR_TY], &[]);
206 let block = func.layout.entry_block().expect("entry block");
207 let params: Vec<_> = func.dfg.block_params(block).to_vec();
208 let p0 = params[0]; // value
209 let p1 = params[1]; // pointer
210
211 let mut cursor = FuncCursor::new(&mut func).at_bottom(block);
212 let (inst, _dfg) = cursor.ins().Store(
213 Opcode::Store,
214 ty,
215 MemFlags::trusted(),
216 Offset32::new(offset),
217 p0,
218 p1,
219 );
220 cursor.ins().return_(&[]);
221
222 (func, inst)
223}
224
225/// Internal helper: build a function with the given signature and an
226/// entry block sized to the signature, but **without** the trailing
227/// `return` that [`empty_function`] inserts. The `function_with_*`
228/// builders use this so they can append their own terminator after
229/// inserting the op of interest.
230fn empty_function_no_return(name: &str, params: &[Type], returns: &[Type]) -> Function {
231 let mut sig = Signature::new(CallConv::SystemV);
232 for p in params {
233 sig.params.push(AbiParam::new(*p));
234 }
235 for r in returns {
236 sig.returns.push(AbiParam::new(*r));
237 }
238 let mut func = Function::with_name_signature(UserFuncName::testcase(name.as_bytes()), sig);
239 let block = func.dfg.make_block();
240 let param_types: Vec<Type> = func.signature.params.iter().map(|p| p.value_type).collect();
241 for pty in param_types {
242 func.dfg.append_block_param(block, pty);
243 }
244 func.layout.append_block(block);
245 func
246}
247
248#[cfg(test)]
249mod tests {
250 use super::*;
251 use cranelift_codegen::ir::types::{F32, I32, I64};
252
253 /// One block, params match signature, terminator is a zero-value
254 /// return.
255 #[test]
256 fn empty_function_shape() {
257 let func = empty_function("noop", &[I32, F32], &[]);
258 // Exactly one block.
259 assert_eq!(func.layout.blocks().count(), 1);
260 let block = func.layout.entry_block().expect("entry block");
261 // Block params match the signature.
262 assert_eq!(func.dfg.num_block_params(block), 2);
263 let ptys: Vec<Type> = func
264 .dfg
265 .block_params(block)
266 .iter()
267 .map(|v| func.dfg.value_type(*v))
268 .collect();
269 assert_eq!(ptys, vec![I32, F32]);
270 // Final instruction is a return.
271 let last = func.layout.last_inst(block).expect("terminator");
272 assert_eq!(func.dfg.insts[last].opcode(), Opcode::Return);
273 }
274
275 /// `empty_function` honours a non-empty `returns` even though the
276 /// inserted `return` has zero values. Callers are responsible for
277 /// replacing the terminator when the signature demands a value; the
278 /// helper documents that contract and we just lock in the signature
279 /// shape here.
280 #[test]
281 fn empty_function_signature_records_returns() {
282 let func = empty_function("with_ret", &[I32], &[I64]);
283 assert_eq!(func.signature.params.len(), 1);
284 assert_eq!(func.signature.returns.len(), 1);
285 assert_eq!(func.signature.params[0].value_type, I32);
286 assert_eq!(func.signature.returns[0].value_type, I64);
287 }
288
289 /// Binary builder: one block, two params, terminator is `return`,
290 /// inserted instruction is the requested opcode.
291 #[test]
292 fn function_with_binary_op_iadd_shape() {
293 let (func, inst) = function_with_binary_op(Opcode::Iadd, I32);
294 assert_eq!(func.layout.blocks().count(), 1);
295 let block = func.layout.entry_block().unwrap();
296 assert_eq!(func.dfg.num_block_params(block), 2);
297 assert_eq!(func.dfg.insts[inst].opcode(), Opcode::Iadd);
298 // The instruction's operands are the two block params (in
299 // order) — verifies the helper wired param0 to lhs and param1
300 // to rhs, the convention the arith lowering expects.
301 let args = func.dfg.inst_args(inst);
302 let params = func.dfg.block_params(block);
303 assert_eq!(args[0], params[0]);
304 assert_eq!(args[1], params[1]);
305 // Terminator returns the binary result.
306 let last = func.layout.last_inst(block).unwrap();
307 assert_eq!(func.dfg.insts[last].opcode(), Opcode::Return);
308 }
309
310 /// Spot-check that `function_with_binary_op` is opcode-agnostic by
311 /// constructing an `fadd` over `F32` lanes.
312 #[test]
313 fn function_with_binary_op_fadd_shape() {
314 let (func, inst) = function_with_binary_op(Opcode::Fadd, F32);
315 assert_eq!(func.dfg.insts[inst].opcode(), Opcode::Fadd);
316 let block = func.layout.entry_block().unwrap();
317 let ptys: Vec<Type> = func
318 .dfg
319 .block_params(block)
320 .iter()
321 .map(|v| func.dfg.value_type(*v))
322 .collect();
323 assert_eq!(ptys, vec![F32, F32]);
324 }
325
326 /// Unary builder: one block, one param, terminator is `return`,
327 /// inserted instruction is the requested opcode.
328 #[test]
329 fn function_with_unary_op_fneg_shape() {
330 let (func, inst) = function_with_unary_op(Opcode::Fneg, F32);
331 assert_eq!(func.layout.blocks().count(), 1);
332 let block = func.layout.entry_block().unwrap();
333 assert_eq!(func.dfg.num_block_params(block), 1);
334 assert_eq!(func.dfg.insts[inst].opcode(), Opcode::Fneg);
335 let args = func.dfg.inst_args(inst);
336 assert_eq!(args[0], func.dfg.block_params(block)[0]);
337 let last = func.layout.last_inst(block).unwrap();
338 assert_eq!(func.dfg.insts[last].opcode(), Opcode::Return);
339 }
340
341 /// Load builder: one block, one pointer param, load opcode at the
342 /// expected offset, terminator returns the loaded value.
343 #[test]
344 fn function_with_load_shape() {
345 let (func, inst) = function_with_load(I32, 16);
346 assert_eq!(func.layout.blocks().count(), 1);
347 let block = func.layout.entry_block().unwrap();
348 assert_eq!(func.dfg.num_block_params(block), 1);
349 let ptys: Vec<Type> = func
350 .dfg
351 .block_params(block)
352 .iter()
353 .map(|v| func.dfg.value_type(*v))
354 .collect();
355 assert_eq!(ptys, vec![PTR_TY]);
356 assert_eq!(func.dfg.insts[inst].opcode(), Opcode::Load);
357 // Offset is reachable via the InstructionData accessor — the
358 // lowering pass will read it through the same path.
359 let offset = func.dfg.insts[inst]
360 .load_store_offset()
361 .expect("load instruction missing offset");
362 assert_eq!(offset, 16);
363 let last = func.layout.last_inst(block).unwrap();
364 assert_eq!(func.dfg.insts[last].opcode(), Opcode::Return);
365 }
366
367 /// Store builder: one block, two params (value + pointer), store
368 /// opcode at the expected offset, terminator is a zero-value
369 /// return.
370 #[test]
371 fn function_with_store_shape() {
372 let (func, inst) = function_with_store(I32, 32);
373 assert_eq!(func.layout.blocks().count(), 1);
374 let block = func.layout.entry_block().unwrap();
375 assert_eq!(func.dfg.num_block_params(block), 2);
376 let ptys: Vec<Type> = func
377 .dfg
378 .block_params(block)
379 .iter()
380 .map(|v| func.dfg.value_type(*v))
381 .collect();
382 assert_eq!(ptys, vec![I32, PTR_TY]);
383 assert_eq!(func.dfg.insts[inst].opcode(), Opcode::Store);
384 let offset = func.dfg.insts[inst]
385 .load_store_offset()
386 .expect("store instruction missing offset");
387 assert_eq!(offset, 32);
388 let last = func.layout.last_inst(block).unwrap();
389 assert_eq!(func.dfg.insts[last].opcode(), Opcode::Return);
390 }
391}