#![cfg(feature = "cuda-oxide-backend")]
use std::collections::HashMap;
use cranelift_codegen::ir::{Block, Function, Inst, Opcode};
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum RejectReason {
Atomic(&'static str),
StrictFp(&'static str),
TableOp(&'static str),
GcOp(&'static str),
MemoryResize(&'static str),
LargeMemcpy(&'static str),
HostCall(&'static str),
FloatOp(&'static str),
IntDivRem(&'static str),
BackEdge(&'static str),
Trap(&'static str),
}
impl RejectReason {
pub fn opcode_mnemonic(&self) -> &'static str {
match self {
Self::Atomic(m)
| Self::StrictFp(m)
| Self::TableOp(m)
| Self::GcOp(m)
| Self::MemoryResize(m)
| Self::LargeMemcpy(m)
| Self::HostCall(m)
| Self::FloatOp(m)
| Self::IntDivRem(m)
| Self::BackEdge(m)
| Self::Trap(m) => m,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Rejection {
pub inst: Inst,
pub block: Block,
pub reason: RejectReason,
}
pub fn scan_function(func: &Function) -> Vec<Rejection> {
let mut rejections = Vec::new();
let block_order = block_layout_order(func);
for block in func.layout.blocks() {
let block_idx = block_order.get(&block).copied().unwrap_or(usize::MAX);
for inst in func.layout.block_insts(block) {
if let Some(reason) = classify_opcode(func.dfg.insts[inst].opcode()) {
rejections.push(Rejection {
inst,
block,
reason,
});
} else if let Some(reason) = back_edge_reason(func, inst, block_idx, &block_order) {
rejections.push(Rejection {
inst,
block,
reason,
});
}
}
}
rejections
}
pub fn check_function(func: &Function) -> Option<Rejection> {
let block_order = block_layout_order(func);
for block in func.layout.blocks() {
let block_idx = block_order.get(&block).copied().unwrap_or(usize::MAX);
for inst in func.layout.block_insts(block) {
if let Some(reason) = classify_opcode(func.dfg.insts[inst].opcode()) {
return Some(Rejection {
inst,
block,
reason,
});
}
if let Some(reason) = back_edge_reason(func, inst, block_idx, &block_order) {
return Some(Rejection {
inst,
block,
reason,
});
}
}
}
None
}
fn block_layout_order(func: &Function) -> HashMap<Block, usize> {
func.layout
.blocks()
.enumerate()
.map(|(idx, block)| (block, idx))
.collect()
}
fn back_edge_reason(
func: &Function,
inst: Inst,
block_idx: usize,
block_order: &HashMap<Block, usize>,
) -> Option<RejectReason> {
let data = &func.dfg.insts[inst];
let opcode = data.opcode();
if !opcode.is_branch() {
return None;
}
let mut is_back_edge = false;
for block_call in data.branch_destination(&func.dfg.jump_tables) {
let target = block_call.block(&func.dfg.value_lists);
if let Some(&target_idx) = block_order.get(&target) {
if target_idx <= block_idx {
is_back_edge = true;
}
}
}
if is_back_edge {
Some(RejectReason::BackEdge(branch_mnemonic(opcode)))
} else {
None
}
}
fn branch_mnemonic(op: Opcode) -> &'static str {
match op {
Opcode::Jump => "jump",
Opcode::Brif => "brif",
Opcode::BrTable => "br_table",
_ => "branch",
}
}
fn classify_opcode(op: Opcode) -> Option<RejectReason> {
match op {
Opcode::AtomicLoad => Some(RejectReason::Atomic("atomic_load")),
Opcode::AtomicStore => Some(RejectReason::Atomic("atomic_store")),
Opcode::AtomicRmw => Some(RejectReason::Atomic("atomic_rmw")),
Opcode::AtomicCas => Some(RejectReason::Atomic("atomic_cas")),
Opcode::FcvtToSintSat => Some(RejectReason::StrictFp("fcvt_to_sint_sat")),
Opcode::FcvtToUintSat => Some(RejectReason::StrictFp("fcvt_to_uint_sat")),
Opcode::Call => Some(RejectReason::HostCall("call")),
Opcode::CallIndirect => Some(RejectReason::HostCall("call_indirect")),
Opcode::ReturnCall => Some(RejectReason::HostCall("return_call")),
Opcode::ReturnCallIndirect => Some(RejectReason::HostCall("return_call_indirect")),
Opcode::Sdiv => Some(RejectReason::IntDivRem("sdiv")),
Opcode::Udiv => Some(RejectReason::IntDivRem("udiv")),
Opcode::Srem => Some(RejectReason::IntDivRem("srem")),
Opcode::Urem => Some(RejectReason::IntDivRem("urem")),
Opcode::Trap => Some(RejectReason::Trap("trap")),
Opcode::Trapz => Some(RejectReason::Trap("trapz")),
Opcode::Trapnz => Some(RejectReason::Trap("trapnz")),
Opcode::Debugtrap => Some(RejectReason::Trap("debugtrap")),
op if is_float_opcode(op) => Some(RejectReason::FloatOp(float_mnemonic(op))),
_ => None,
}
}
fn is_float_opcode(op: Opcode) -> bool {
matches!(
op,
Opcode::Fadd
| Opcode::Fsub
| Opcode::Fmul
| Opcode::Fdiv
| Opcode::Fma
| Opcode::Fneg
| Opcode::Fabs
| Opcode::Fcopysign
| Opcode::Fmin
| Opcode::Fmax
| Opcode::Sqrt
| Opcode::Ceil
| Opcode::Floor
| Opcode::Trunc
| Opcode::Nearest
| Opcode::Fcmp
| Opcode::Fpromote
| Opcode::Fdemote
| Opcode::FcvtFromSint
| Opcode::FcvtFromUint
| Opcode::FcvtToSint
| Opcode::FcvtToUint
)
}
fn float_mnemonic(op: Opcode) -> &'static str {
match op {
Opcode::Fadd => "fadd",
Opcode::Fsub => "fsub",
Opcode::Fmul => "fmul",
Opcode::Fdiv => "fdiv",
Opcode::Fma => "fma",
Opcode::Fneg => "fneg",
Opcode::Fabs => "fabs",
Opcode::Fcopysign => "fcopysign",
Opcode::Fmin => "fmin",
Opcode::Fmax => "fmax",
Opcode::Sqrt => "sqrt",
Opcode::Ceil => "ceil",
Opcode::Floor => "floor",
Opcode::Trunc => "trunc",
Opcode::Nearest => "nearest",
Opcode::Fcmp => "fcmp",
Opcode::Fpromote => "fpromote",
Opcode::Fdemote => "fdemote",
Opcode::FcvtFromSint => "fcvt_from_sint",
Opcode::FcvtFromUint => "fcvt_from_uint",
Opcode::FcvtToSint => "fcvt_to_sint",
Opcode::FcvtToUint => "fcvt_to_uint",
_ => "float_op",
}
}
#[cfg(test)]
mod tests {
use super::*;
use cranelift_codegen::ir::immediates::Offset32;
use cranelift_codegen::ir::instructions::InstructionData;
use cranelift_codegen::ir::{
AtomicRmwOp, FuncRef, MemFlags, SigRef, Signature, UserFuncName, Value, ValueList,
};
use cranelift_codegen::isa::CallConv;
fn empty_func() -> (Function, Block) {
let mut func = Function::with_name_signature(
UserFuncName::user(0, 0),
Signature::new(CallConv::SystemV),
);
let block = func.dfg.make_block();
func.layout.append_block(block);
(func, block)
}
fn append(func: &mut Function, block: Block, data: InstructionData) -> Inst {
let inst = func.dfg.make_inst(data);
func.layout.append_inst(inst, block);
inst
}
fn dummy_val() -> Value {
Value::from_u32(0)
}
#[test]
fn admissible_iadd_return_yields_empty() {
let (mut func, block) = empty_func();
append(
&mut func,
block,
InstructionData::Binary {
opcode: Opcode::Iadd,
args: [dummy_val(), dummy_val()],
},
);
append(
&mut func,
block,
InstructionData::MultiAry {
opcode: Opcode::Return,
args: ValueList::new(),
},
);
assert_eq!(scan_function(&func), vec![]);
assert_eq!(check_function(&func), None);
}
#[test]
fn atomic_load_is_rejected() {
let (mut func, block) = empty_func();
append(
&mut func,
block,
InstructionData::LoadNoOffset {
opcode: Opcode::AtomicLoad,
flags: MemFlags::new(),
arg: dummy_val(),
},
);
let rejections = scan_function(&func);
assert_eq!(rejections.len(), 1);
assert_eq!(rejections[0].reason, RejectReason::Atomic("atomic_load"));
assert_eq!(rejections[0].block, block);
}
#[test]
fn atomic_store_is_rejected() {
let (mut func, block) = empty_func();
append(
&mut func,
block,
InstructionData::StoreNoOffset {
opcode: Opcode::AtomicStore,
flags: MemFlags::new(),
args: [dummy_val(), dummy_val()],
},
);
let rejections = scan_function(&func);
assert_eq!(rejections.len(), 1);
assert_eq!(rejections[0].reason, RejectReason::Atomic("atomic_store"));
}
#[test]
fn atomic_rmw_is_rejected() {
let (mut func, block) = empty_func();
append(
&mut func,
block,
InstructionData::AtomicRmw {
opcode: Opcode::AtomicRmw,
flags: MemFlags::new(),
op: AtomicRmwOp::Add,
args: [dummy_val(), dummy_val()],
},
);
let rejections = scan_function(&func);
assert_eq!(rejections.len(), 1);
assert_eq!(rejections[0].reason, RejectReason::Atomic("atomic_rmw"));
}
#[test]
fn atomic_cas_is_rejected() {
let (mut func, block) = empty_func();
append(
&mut func,
block,
InstructionData::AtomicCas {
opcode: Opcode::AtomicCas,
flags: MemFlags::new(),
args: [dummy_val(), dummy_val(), dummy_val()],
},
);
let rejections = scan_function(&func);
assert_eq!(rejections.len(), 1);
assert_eq!(rejections[0].reason, RejectReason::Atomic("atomic_cas"));
}
#[test]
fn fcvt_to_sint_sat_is_rejected() {
let (mut func, block) = empty_func();
append(
&mut func,
block,
InstructionData::Unary {
opcode: Opcode::FcvtToSintSat,
arg: dummy_val(),
},
);
let rejections = scan_function(&func);
assert_eq!(rejections.len(), 1);
assert_eq!(
rejections[0].reason,
RejectReason::StrictFp("fcvt_to_sint_sat")
);
}
#[test]
fn fcvt_to_uint_sat_is_rejected() {
let (mut func, block) = empty_func();
append(
&mut func,
block,
InstructionData::Unary {
opcode: Opcode::FcvtToUintSat,
arg: dummy_val(),
},
);
let rejections = scan_function(&func);
assert_eq!(rejections.len(), 1);
assert_eq!(
rejections[0].reason,
RejectReason::StrictFp("fcvt_to_uint_sat")
);
}
#[test]
fn call_is_rejected() {
let (mut func, block) = empty_func();
append(
&mut func,
block,
InstructionData::Call {
opcode: Opcode::Call,
func_ref: FuncRef::from_u32(0),
args: ValueList::new(),
},
);
let rejections = scan_function(&func);
assert_eq!(rejections.len(), 1);
assert_eq!(rejections[0].reason, RejectReason::HostCall("call"));
}
#[test]
fn call_indirect_is_rejected() {
let (mut func, block) = empty_func();
append(
&mut func,
block,
InstructionData::CallIndirect {
opcode: Opcode::CallIndirect,
sig_ref: SigRef::from_u32(0),
args: ValueList::new(),
},
);
let rejections = scan_function(&func);
assert_eq!(rejections.len(), 1);
assert_eq!(
rejections[0].reason,
RejectReason::HostCall("call_indirect")
);
}
#[test]
fn table_op_variant_is_constructible() {
let r = RejectReason::TableOp("table_get");
assert_eq!(r.opcode_mnemonic(), "table_get");
}
#[test]
fn gc_op_variant_is_constructible() {
let r = RejectReason::GcOp("ref_func");
assert_eq!(r.opcode_mnemonic(), "ref_func");
}
#[test]
fn memory_resize_variant_is_constructible() {
let r = RejectReason::MemoryResize("memory_grow");
assert_eq!(r.opcode_mnemonic(), "memory_grow");
}
#[test]
fn large_memcpy_variant_is_constructible() {
let r = RejectReason::LargeMemcpy("memory_copy");
assert_eq!(r.opcode_mnemonic(), "memory_copy");
}
#[test]
fn multiple_rejections_returned_in_layout_order() {
let (mut func, block) = empty_func();
append(
&mut func,
block,
InstructionData::LoadNoOffset {
opcode: Opcode::AtomicLoad,
flags: MemFlags::new(),
arg: dummy_val(),
},
);
append(
&mut func,
block,
InstructionData::Call {
opcode: Opcode::Call,
func_ref: FuncRef::from_u32(0),
args: ValueList::new(),
},
);
let rejections = scan_function(&func);
assert_eq!(rejections.len(), 2);
assert_eq!(rejections[0].reason, RejectReason::Atomic("atomic_load"));
assert_eq!(rejections[1].reason, RejectReason::HostCall("call"));
}
#[test]
fn check_function_returns_first_rejection() {
let (mut func, block) = empty_func();
append(
&mut func,
block,
InstructionData::Call {
opcode: Opcode::Call,
func_ref: FuncRef::from_u32(0),
args: ValueList::new(),
},
);
append(
&mut func,
block,
InstructionData::LoadNoOffset {
opcode: Opcode::AtomicLoad,
flags: MemFlags::new(),
arg: dummy_val(),
},
);
let first = check_function(&func).expect("function has rejections");
assert_eq!(first.reason, RejectReason::HostCall("call"));
}
#[test]
fn offset_field_is_irrelevant_to_detection() {
let _ = Offset32::new(0);
}
#[test]
fn float_arith_is_rejected() {
for (opcode, mnemonic) in [
(Opcode::Fadd, "fadd"),
(Opcode::Fmul, "fmul"),
(Opcode::Fdiv, "fdiv"),
] {
let (mut func, block) = empty_func();
append(
&mut func,
block,
InstructionData::Binary {
opcode,
args: [dummy_val(), dummy_val()],
},
);
let first = check_function(&func).expect("float op must be rejected");
assert_eq!(first.reason, RejectReason::FloatOp(mnemonic));
}
}
#[test]
fn float_sqrt_is_rejected() {
let (mut func, block) = empty_func();
append(
&mut func,
block,
InstructionData::Unary {
opcode: Opcode::Sqrt,
arg: dummy_val(),
},
);
let first = check_function(&func).expect("sqrt must be rejected");
assert_eq!(first.reason, RejectReason::FloatOp("sqrt"));
}
#[test]
fn int_div_rem_is_rejected() {
for (opcode, mnemonic) in [
(Opcode::Sdiv, "sdiv"),
(Opcode::Udiv, "udiv"),
(Opcode::Srem, "srem"),
(Opcode::Urem, "urem"),
] {
let (mut func, block) = empty_func();
append(
&mut func,
block,
InstructionData::Binary {
opcode,
args: [dummy_val(), dummy_val()],
},
);
let first = check_function(&func).expect("int div/rem must be rejected");
assert_eq!(first.reason, RejectReason::IntDivRem(mnemonic));
}
}
#[test]
fn plain_iadd_still_admissible() {
let (mut func, block) = empty_func();
append(
&mut func,
block,
InstructionData::Binary {
opcode: Opcode::Iadd,
args: [dummy_val(), dummy_val()],
},
);
assert_eq!(check_function(&func), None);
}
#[test]
fn trap_is_rejected() {
let (mut func, block) = empty_func();
append(
&mut func,
block,
InstructionData::Trap {
opcode: Opcode::Trap,
code: cranelift_codegen::ir::TrapCode::User(1),
},
);
let first = check_function(&func).expect("trap must be rejected");
assert_eq!(first.reason, RejectReason::Trap("trap"));
}
#[test]
fn loop_back_edge_is_rejected_forward_jump_is_not() {
use cranelift_codegen::ir::BlockCall;
let mut func = Function::with_name_signature(
UserFuncName::user(0, 0),
Signature::new(CallConv::SystemV),
);
let entry = func.dfg.make_block();
let body = func.dfg.make_block();
func.layout.append_block(entry);
func.layout.append_block(body);
let fwd_call = BlockCall::new(body, &[], &mut func.dfg.value_lists);
let fwd = func.dfg.make_inst(InstructionData::Jump {
opcode: Opcode::Jump,
destination: fwd_call,
});
func.layout.append_inst(fwd, entry);
let back_call = BlockCall::new(entry, &[], &mut func.dfg.value_lists);
let back = func.dfg.make_inst(InstructionData::Jump {
opcode: Opcode::Jump,
destination: back_call,
});
func.layout.append_inst(back, body);
let rejections = scan_function(&func);
assert_eq!(
rejections.len(),
1,
"exactly the back-edge is rejected, not the forward jump"
);
assert!(matches!(rejections[0].reason, RejectReason::BackEdge(_)));
assert_eq!(rejections[0].block, body);
}
#[test]
fn self_loop_is_back_edge() {
use cranelift_codegen::ir::BlockCall;
let (mut func, block) = empty_func();
let call = BlockCall::new(block, &[], &mut func.dfg.value_lists);
let jmp = func.dfg.make_inst(InstructionData::Jump {
opcode: Opcode::Jump,
destination: call,
});
func.layout.append_inst(jmp, block);
let first = check_function(&func).expect("self-loop must be rejected");
assert!(matches!(first.reason, RejectReason::BackEdge(_)));
}
}