#![cfg(feature = "cuda-oxide-backend")]
use cranelift_codegen::cursor::{Cursor, FuncCursor};
use cranelift_codegen::ir::immediates::Offset32;
use cranelift_codegen::ir::{
types, AbiParam, BlockCall, Function, InstBuilder, InstructionData, MemFlags, Opcode,
Signature, UserFuncName,
};
use cranelift_codegen::isa::CallConv;
use tensor_wasm_jit::lowered_ir::{LoweredOp, LoweredType};
use tensor_wasm_jit::lowering_driver::lower_function;
use tensor_wasm_jit::lowering_errors::LoweringError;
use tensor_wasm_jit::lowering_test_support::function_with_binary_op;
const PTR_TY: cranelift_codegen::ir::Type = types::I64;
#[test]
fn float_vector_add_kernel_is_rejected_by_preflight() {
let mut sig = Signature::new(CallConv::SystemV);
sig.params.push(AbiParam::new(PTR_TY));
sig.params.push(AbiParam::new(PTR_TY));
sig.params.push(AbiParam::new(PTR_TY));
let mut func =
Function::with_name_signature(UserFuncName::testcase("vector_add".as_bytes()), sig);
let block = func.dfg.make_block();
let out_ptr = func.dfg.append_block_param(block, PTR_TY);
let a_ptr = func.dfg.append_block_param(block, PTR_TY);
let b_ptr = func.dfg.append_block_param(block, PTR_TY);
func.layout.append_block(block);
let mut cursor = FuncCursor::new(&mut func).at_bottom(block);
let (load_a_inst, dfg) = cursor.ins().Load(
Opcode::Load,
types::F32,
MemFlags::trusted(),
Offset32::new(0),
a_ptr,
);
let v_a = dfg.first_result(load_a_inst);
let (load_b_inst, dfg) = cursor.ins().Load(
Opcode::Load,
types::F32,
MemFlags::trusted(),
Offset32::new(0),
b_ptr,
);
let v_b = dfg.first_result(load_b_inst);
let (fadd_inst, dfg) = cursor.ins().Binary(Opcode::Fadd, types::F32, v_a, v_b);
let v_sum = dfg.first_result(fadd_inst);
let _ = cursor.ins().Store(
Opcode::Store,
types::F32,
MemFlags::trusted(),
Offset32::new(0),
v_sum,
out_ptr,
);
cursor.ins().return_(&[]);
let err = lower_function(&func).expect_err("float kernel must be rejected by the preflight");
match err {
LoweringError::Rejected { reason, .. } => {
assert!(
reason.contains("FloatOp") || reason.contains("fadd"),
"expected a FloatOp rejection, got: {reason}"
);
}
other => panic!("expected LoweringError::Rejected, got {other:?}"),
}
}
#[test]
fn scalar_iadd_lowers_via_public_driver_api() {
let (func, _inst) = function_with_binary_op(Opcode::Iadd, types::I32);
let lowered = lower_function(&func).expect("scalar iadd must lower");
assert_eq!(lowered.blocks.len(), 1);
assert_eq!(
lowered.signature.params,
vec![LoweredType::I32, LoweredType::I32],
);
assert_eq!(lowered.signature.returns, vec![LoweredType::I32]);
let block = &lowered.blocks[0];
assert_eq!(block.ops.len(), 2, "AddI + Return");
assert!(matches!(block.ops[0], LoweredOp::AddI { .. }));
assert!(matches!(block.ops[1], LoweredOp::Return { .. }));
assert!(lowered.is_well_formed());
}
#[test]
fn scalar_fma_is_rejected_by_preflight() {
let mut sig = Signature::new(CallConv::SystemV);
sig.params.push(AbiParam::new(types::F32));
sig.params.push(AbiParam::new(types::F32));
sig.params.push(AbiParam::new(types::F32));
sig.returns.push(AbiParam::new(types::F32));
let mut func =
Function::with_name_signature(UserFuncName::testcase("fma_kernel".as_bytes()), sig);
let block = func.dfg.make_block();
let pa = func.dfg.append_block_param(block, types::F32);
let pb = func.dfg.append_block_param(block, types::F32);
let pc = func.dfg.append_block_param(block, types::F32);
func.layout.append_block(block);
let fma_inst = func.dfg.make_inst(InstructionData::Ternary {
opcode: Opcode::Fma,
args: [pa, pb, pc],
});
func.dfg.make_inst_results(fma_inst, types::F32);
func.layout.append_inst(fma_inst, block);
let v_result = func.dfg.first_result(fma_inst);
{
let mut cursor = FuncCursor::new(&mut func).at_bottom(block);
cursor.ins().return_(&[v_result]);
}
let err = lower_function(&func).expect_err("fma kernel must be rejected by the preflight");
match err {
LoweringError::Rejected { reason, .. } => {
assert!(
reason.contains("FloatOp") || reason.contains("fma"),
"expected a FloatOp rejection, got: {reason}"
);
}
other => panic!("expected LoweringError::Rejected, got {other:?}"),
}
}
#[test]
fn multi_block_brif_diamond_lowers_three_blocks() {
let mut sig = Signature::new(CallConv::SystemV);
sig.params.push(AbiParam::new(types::I32));
sig.params.push(AbiParam::new(types::I32));
sig.returns.push(AbiParam::new(types::I32));
let mut func =
Function::with_name_signature(UserFuncName::testcase("brif_diamond".as_bytes()), sig);
let entry = func.dfg.make_block();
let cond = func.dfg.append_block_param(entry, types::I32);
let payload = func.dfg.append_block_param(entry, types::I32);
let then_blk = func.dfg.make_block();
let then_param = func.dfg.append_block_param(then_blk, types::I32);
let else_blk = func.dfg.make_block();
let else_param = func.dfg.append_block_param(else_blk, types::I32);
func.layout.append_block(entry);
func.layout.append_block(then_blk);
func.layout.append_block(else_blk);
let then_call = BlockCall::new(then_blk, &[payload], &mut func.dfg.value_lists);
let else_call = BlockCall::new(else_blk, &[payload], &mut func.dfg.value_lists);
let brif = func.dfg.make_inst(InstructionData::Brif {
opcode: Opcode::Brif,
arg: cond,
blocks: [then_call, else_call],
});
func.dfg.make_inst_results(brif, types::INVALID);
func.layout.append_inst(brif, entry);
{
let mut cursor = FuncCursor::new(&mut func).at_bottom(then_blk);
cursor.ins().return_(&[then_param]);
}
{
let mut cursor = FuncCursor::new(&mut func).at_bottom(else_blk);
cursor.ins().return_(&[else_param]);
}
let lowered = lower_function(&func).expect("multi-block diamond must lower");
assert_eq!(lowered.blocks.len(), 3, "three blocks expected");
assert!(lowered.is_well_formed());
for lblock in &lowered.blocks {
assert!(
lblock.is_well_formed(),
"block {} not well-formed: ops = {:?}",
lblock.id,
lblock.ops,
);
}
let entry_lblock = &lowered.blocks[0];
assert_eq!(lowered.entry, entry_lblock.id);
assert_eq!(entry_lblock.ops.len(), 1, "entry has only the CondBr");
let (then_id, else_id) = (lowered.blocks[1].id, lowered.blocks[2].id);
match &entry_lblock.ops[0] {
LoweredOp::CondBr {
cond: _,
then_target,
then_args,
else_target,
else_args,
} => {
assert_eq!(*then_target, then_id);
assert_eq!(*else_target, else_id);
assert_eq!(then_args.len(), 1, "then receives one arg (payload)");
assert_eq!(else_args.len(), 1, "else receives one arg (payload)");
assert_eq!(
then_args[0], else_args[0],
"both successors receive the same payload id",
);
}
other => panic!("expected CondBr, got {other:?}"),
}
for succ in &lowered.blocks[1..] {
assert_eq!(succ.params.len(), 1, "successor has one param");
assert_eq!(succ.params[0].1, LoweredType::I32);
assert_eq!(succ.ops.len(), 1, "successor has only the Return");
match &succ.ops[0] {
LoweredOp::Return { values } => {
assert_eq!(values.len(), 1);
assert_eq!(
values[0], succ.params[0].0,
"successor returns its block param",
);
}
other => panic!("expected Return, got {other:?}"),
}
}
}
#[test]
fn atomic_load_surfaces_as_unsupported_opcode() {
let mut sig = Signature::new(CallConv::SystemV);
sig.params.push(AbiParam::new(PTR_TY));
let mut func = Function::with_name_signature(UserFuncName::testcase("atomic".as_bytes()), sig);
let block = func.dfg.make_block();
let ptr = func.dfg.append_block_param(block, PTR_TY);
func.layout.append_block(block);
let atomic = func.dfg.make_inst(InstructionData::LoadNoOffset {
opcode: Opcode::AtomicLoad,
flags: MemFlags::new(),
arg: ptr,
});
func.dfg.make_inst_results(atomic, types::I32);
func.layout.append_inst(atomic, block);
{
let mut cursor = FuncCursor::new(&mut func).at_bottom(block);
cursor.ins().return_(&[]);
}
let err = lower_function(&func).expect_err("atomic_load is unsupported");
match err {
LoweringError::UnsupportedOpcode { op, .. } => {
assert!(
op.contains("AtomicLoad"),
"expected AtomicLoad in the error op string, got {op:?}",
);
}
LoweringError::Rejected { reason, .. } => {
assert!(
reason.contains("atomic"),
"expected 'atomic' in the rejection reason, got {reason:?}",
);
}
other => panic!("expected UnsupportedOpcode or Rejected for atomic_load, got {other:?}",),
}
}