#![cfg(feature = "cuda-oxide-backend")]
use std::collections::HashMap;
use cranelift_codegen::ir::{self, InstructionData, Opcode};
use crate::lowered_ir::{LoweredOp, LoweredType, LoweredValueId};
#[derive(Debug, thiserror::Error, PartialEq, Eq)]
pub enum MemLowerError {
#[error("memory lowering: Cranelift value {0:?} not in value_map")]
UnmappedValue(ir::Value),
#[error("memory lowering: unsupported Cranelift type `{0}`")]
UnsupportedType(String),
#[error("memory lowering: SSA value-id counter overflowed u32::MAX")]
SsaOverflow,
}
#[derive(Debug)]
pub struct MemLowerContext<'a> {
pub linear_memory_base: LoweredValueId,
pub value_map: &'a HashMap<ir::Value, LoweredValueId>,
pub stack_slot_map: &'a mut HashMap<ir::StackSlot, LoweredValueId>,
pub next_value_id: &'a mut LoweredValueId,
}
impl<'a> MemLowerContext<'a> {
pub fn fresh_id(&mut self) -> Result<LoweredValueId, MemLowerError> {
let id = *self.next_value_id;
*self.next_value_id = id.checked_add(1).ok_or(MemLowerError::SsaOverflow)?;
Ok(id)
}
pub fn lookup_value(&self, v: ir::Value) -> Result<LoweredValueId, MemLowerError> {
self.value_map
.get(&v)
.copied()
.ok_or(MemLowerError::UnmappedValue(v))
}
}
pub fn lower_memory_inst(
inst: ir::Inst,
func: &ir::Function,
ctx: &mut MemLowerContext<'_>,
) -> Result<Option<Vec<LoweredOp>>, MemLowerError> {
let data = &func.dfg.insts[inst];
match data {
InstructionData::Load {
opcode: Opcode::Load,
arg,
offset,
..
} => {
let base = ctx.lookup_value(*arg)?;
let result_val = func.dfg.first_result(inst);
let ty = cranelift_type_to_lowered(func.dfg.value_type(result_val))?;
let result_id = ctx.fresh_id()?;
Ok(Some(vec![LoweredOp::Load {
ty,
base,
offset: (*offset).into(),
result: result_id,
}]))
}
InstructionData::Store {
opcode: Opcode::Store,
args,
offset,
..
} => {
let value_v = args[0];
let base_v = args[1];
let value = ctx.lookup_value(value_v)?;
let base = ctx.lookup_value(base_v)?;
let ty = cranelift_type_to_lowered(func.dfg.value_type(value_v))?;
Ok(Some(vec![LoweredOp::Store {
ty,
value,
base,
offset: (*offset).into(),
}]))
}
InstructionData::StackLoad {
opcode: Opcode::StackLoad,
stack_slot,
offset,
} => {
let mut emitted = Vec::with_capacity(2);
let result_val = func.dfg.first_result(inst);
let ty = cranelift_type_to_lowered(func.dfg.value_type(result_val))?;
let alloca_ptr = ensure_stack_alloca(*stack_slot, func, ty.clone(), ctx, &mut emitted)?;
let result_id = ctx.fresh_id()?;
emitted.push(LoweredOp::Load {
ty,
base: alloca_ptr,
offset: (*offset).into(),
result: result_id,
});
Ok(Some(emitted))
}
InstructionData::StackStore {
opcode: Opcode::StackStore,
arg,
stack_slot,
offset,
} => {
let value = ctx.lookup_value(*arg)?;
let ty = cranelift_type_to_lowered(func.dfg.value_type(*arg))?;
let mut emitted = Vec::with_capacity(2);
let alloca_ptr = ensure_stack_alloca(*stack_slot, func, ty.clone(), ctx, &mut emitted)?;
emitted.push(LoweredOp::Store {
ty,
value,
base: alloca_ptr,
offset: (*offset).into(),
});
Ok(Some(emitted))
}
_ => Ok(None),
}
}
fn ensure_stack_alloca(
stack_slot: ir::StackSlot,
func: &ir::Function,
ty: LoweredType,
ctx: &mut MemLowerContext<'_>,
out: &mut Vec<LoweredOp>,
) -> Result<LoweredValueId, MemLowerError> {
if let Some(existing) = ctx.stack_slot_map.get(&stack_slot) {
return Ok(*existing);
}
let bytes = func.sized_stack_slots[stack_slot].size;
let ptr_id = ctx.fresh_id()?;
out.push(LoweredOp::StackAlloc {
ty,
bytes,
result: ptr_id,
});
ctx.stack_slot_map.insert(stack_slot, ptr_id);
Ok(ptr_id)
}
fn cranelift_type_to_lowered(ty: ir::Type) -> Result<LoweredType, MemLowerError> {
use cranelift_codegen::ir::types;
Ok(match ty {
types::I8 => LoweredType::I8,
types::I16 => LoweredType::I16,
types::I32 => LoweredType::I32,
types::I64 => LoweredType::I64,
types::F32 => LoweredType::F32,
types::F64 => LoweredType::F64,
other => return Err(MemLowerError::UnsupportedType(other.to_string())),
})
}
#[cfg(test)]
mod tests {
use super::*;
use cranelift_codegen::ir::immediates::Offset32;
use cranelift_codegen::ir::{
types, AbiParam, Function, MemFlags, Signature, StackSlotData, StackSlotKind, UserFuncName,
};
use cranelift_codegen::isa::CallConv;
fn skeleton() -> (
Function,
ir::Value,
ir::Value,
HashMap<ir::Value, LoweredValueId>,
) {
let mut sig = Signature::new(CallConv::SystemV);
sig.params.push(AbiParam::new(types::I64)); sig.params.push(AbiParam::new(types::I32)); let mut func = Function::with_name_signature(UserFuncName::default(), sig);
let block = func.dfg.make_block();
let base_v = func.dfg.append_block_param(block, types::I64);
let val_v = func.dfg.append_block_param(block, types::I32);
let mut map = HashMap::new();
map.insert(base_v, 100);
map.insert(val_v, 101);
(func, base_v, val_v, map)
}
#[test]
fn lower_load_produces_single_load_op() {
let (mut func, base_v, _val_v, value_map) = skeleton();
let inst = func.dfg.make_inst(InstructionData::Load {
opcode: Opcode::Load,
arg: base_v,
flags: MemFlags::new(),
offset: Offset32::new(8),
});
func.dfg.make_inst_results(inst, types::I32);
let mut stack_map = HashMap::new();
let mut next_id: LoweredValueId = 200;
let mut ctx = MemLowerContext {
linear_memory_base: 100,
value_map: &value_map,
stack_slot_map: &mut stack_map,
next_value_id: &mut next_id,
};
let out = lower_memory_inst(inst, &func, &mut ctx)
.expect("lowering must succeed")
.expect("inst is a memory op");
assert_eq!(out.len(), 1);
match &out[0] {
LoweredOp::Load {
ty,
base,
offset,
result,
} => {
assert_eq!(*ty, LoweredType::I32);
assert_eq!(*base, 100);
assert_eq!(*offset, 8);
assert_eq!(*result, 200);
}
other => panic!("expected Load, got {other:?}"),
}
assert_eq!(next_id, 201);
}
#[test]
fn fresh_id_overflow_is_structured_error_not_wrap() {
let (mut func, base_v, _val_v, value_map) = skeleton();
let inst = func.dfg.make_inst(InstructionData::Load {
opcode: Opcode::Load,
arg: base_v,
flags: MemFlags::new(),
offset: Offset32::new(0),
});
func.dfg.make_inst_results(inst, types::I32);
let mut stack_map = HashMap::new();
let mut next_id: LoweredValueId = u32::MAX;
let mut ctx = MemLowerContext {
linear_memory_base: 100,
value_map: &value_map,
stack_slot_map: &mut stack_map,
next_value_id: &mut next_id,
};
let err = lower_memory_inst(inst, &func, &mut ctx)
.expect_err("counter at u32::MAX must overflow, not wrap");
assert_eq!(err, MemLowerError::SsaOverflow);
}
#[test]
fn lower_store_produces_single_store_op() {
let (mut func, base_v, val_v, value_map) = skeleton();
let inst = func.dfg.make_inst(InstructionData::Store {
opcode: Opcode::Store,
args: [val_v, base_v],
flags: MemFlags::new(),
offset: Offset32::new(-4),
});
func.dfg.make_inst_results(inst, types::I32);
let mut stack_map = HashMap::new();
let mut next_id: LoweredValueId = 300;
let mut ctx = MemLowerContext {
linear_memory_base: 100,
value_map: &value_map,
stack_slot_map: &mut stack_map,
next_value_id: &mut next_id,
};
let out = lower_memory_inst(inst, &func, &mut ctx)
.expect("lowering must succeed")
.expect("inst is a memory op");
assert_eq!(out.len(), 1);
match &out[0] {
LoweredOp::Store {
ty,
value,
base,
offset,
} => {
assert_eq!(*ty, LoweredType::I32);
assert_eq!(*value, 101);
assert_eq!(*base, 100);
assert_eq!(*offset, -4);
}
other => panic!("expected Store, got {other:?}"),
}
assert_eq!(next_id, 300);
}
#[test]
fn lower_stack_load_first_touch_emits_alloca_then_load() {
let (mut func, _base_v, _val_v, value_map) = skeleton();
let ss =
func.create_sized_stack_slot(StackSlotData::new(StackSlotKind::ExplicitSlot, 16, 0));
let inst = func.dfg.make_inst(InstructionData::StackLoad {
opcode: Opcode::StackLoad,
stack_slot: ss,
offset: Offset32::new(0),
});
func.dfg.make_inst_results(inst, types::I32);
let mut stack_map = HashMap::new();
let mut next_id: LoweredValueId = 400;
let mut ctx = MemLowerContext {
linear_memory_base: 100,
value_map: &value_map,
stack_slot_map: &mut stack_map,
next_value_id: &mut next_id,
};
let out = lower_memory_inst(inst, &func, &mut ctx)
.expect("lowering must succeed")
.expect("inst is a memory op");
assert_eq!(out.len(), 2, "first touch emits alloca + load");
match &out[0] {
LoweredOp::StackAlloc { ty, bytes, result } => {
assert_eq!(*ty, LoweredType::I32);
assert_eq!(*bytes, 16);
assert_eq!(*result, 400);
}
other => panic!("expected StackAlloc, got {other:?}"),
}
match &out[1] {
LoweredOp::Load {
ty,
base,
offset,
result,
} => {
assert_eq!(*ty, LoweredType::I32);
assert_eq!(*base, 400, "Load reuses the alloca's pointer id");
assert_eq!(*offset, 0);
assert_eq!(*result, 401);
}
other => panic!("expected Load, got {other:?}"),
}
assert_eq!(stack_map.get(&ss).copied(), Some(400));
assert_eq!(next_id, 402);
}
#[test]
fn lower_stack_load_second_touch_reuses_alloca() {
let (mut func, _base_v, _val_v, value_map) = skeleton();
let ss =
func.create_sized_stack_slot(StackSlotData::new(StackSlotKind::ExplicitSlot, 8, 0));
let inst = func.dfg.make_inst(InstructionData::StackLoad {
opcode: Opcode::StackLoad,
stack_slot: ss,
offset: Offset32::new(0),
});
func.dfg.make_inst_results(inst, types::I32);
let mut stack_map = HashMap::new();
stack_map.insert(ss, 999);
let mut next_id: LoweredValueId = 500;
let mut ctx = MemLowerContext {
linear_memory_base: 100,
value_map: &value_map,
stack_slot_map: &mut stack_map,
next_value_id: &mut next_id,
};
let out = lower_memory_inst(inst, &func, &mut ctx)
.expect("lowering must succeed")
.expect("inst is a memory op");
assert_eq!(out.len(), 1, "subsequent touches skip the alloca");
match &out[0] {
LoweredOp::Load { base, result, .. } => {
assert_eq!(*base, 999);
assert_eq!(*result, 500);
}
other => panic!("expected Load, got {other:?}"),
}
assert_eq!(next_id, 501);
}
#[test]
fn lower_stack_store_first_touch_emits_alloca_then_store() {
let (mut func, _base_v, val_v, value_map) = skeleton();
let ss =
func.create_sized_stack_slot(StackSlotData::new(StackSlotKind::ExplicitSlot, 4, 0));
let inst = func.dfg.make_inst(InstructionData::StackStore {
opcode: Opcode::StackStore,
arg: val_v,
stack_slot: ss,
offset: Offset32::new(0),
});
func.dfg.make_inst_results(inst, types::I32);
let mut stack_map = HashMap::new();
let mut next_id: LoweredValueId = 600;
let mut ctx = MemLowerContext {
linear_memory_base: 100,
value_map: &value_map,
stack_slot_map: &mut stack_map,
next_value_id: &mut next_id,
};
let out = lower_memory_inst(inst, &func, &mut ctx)
.expect("lowering must succeed")
.expect("inst is a memory op");
assert_eq!(out.len(), 2, "first touch emits alloca + store");
match &out[0] {
LoweredOp::StackAlloc { ty, bytes, result } => {
assert_eq!(*ty, LoweredType::I32);
assert_eq!(*bytes, 4);
assert_eq!(*result, 600);
}
other => panic!("expected StackAlloc, got {other:?}"),
}
match &out[1] {
LoweredOp::Store {
ty,
value,
base,
offset,
} => {
assert_eq!(*ty, LoweredType::I32);
assert_eq!(*value, 101);
assert_eq!(*base, 600);
assert_eq!(*offset, 0);
}
other => panic!("expected Store, got {other:?}"),
}
assert_eq!(next_id, 601);
}
#[test]
fn lower_returns_none_for_non_memory_op() {
let mut sig = Signature::new(CallConv::SystemV);
sig.params.push(AbiParam::new(types::I32));
let mut func = Function::with_name_signature(UserFuncName::default(), sig);
let block = func.dfg.make_block();
let _arg = func.dfg.append_block_param(block, types::I32);
let inst = func.dfg.make_inst(InstructionData::UnaryImm {
opcode: Opcode::Iconst,
imm: 42i64.into(),
});
func.dfg.make_inst_results(inst, types::I32);
let value_map: HashMap<ir::Value, LoweredValueId> = HashMap::new();
let mut stack_map = HashMap::new();
let mut next_id: LoweredValueId = 700;
let mut ctx = MemLowerContext {
linear_memory_base: 0,
value_map: &value_map,
stack_slot_map: &mut stack_map,
next_value_id: &mut next_id,
};
let out = lower_memory_inst(inst, &func, &mut ctx).expect("lowering must succeed");
assert!(out.is_none(), "iconst is not a memory op");
assert_eq!(next_id, 700);
}
#[test]
fn unmapped_base_returns_error() {
let mut sig = Signature::new(CallConv::SystemV);
sig.params.push(AbiParam::new(types::I64));
let mut func = Function::with_name_signature(UserFuncName::default(), sig);
let block = func.dfg.make_block();
let base_v = func.dfg.append_block_param(block, types::I64);
let inst = func.dfg.make_inst(InstructionData::Load {
opcode: Opcode::Load,
arg: base_v,
flags: MemFlags::new(),
offset: Offset32::new(0),
});
func.dfg.make_inst_results(inst, types::I32);
let value_map: HashMap<ir::Value, LoweredValueId> = HashMap::new();
let mut stack_map = HashMap::new();
let mut next_id: LoweredValueId = 800;
let mut ctx = MemLowerContext {
linear_memory_base: 0,
value_map: &value_map,
stack_slot_map: &mut stack_map,
next_value_id: &mut next_id,
};
let err = lower_memory_inst(inst, &func, &mut ctx).expect_err("unmapped base must error");
assert_eq!(err, MemLowerError::UnmappedValue(base_v));
}
#[test]
fn cranelift_type_to_lowered_supports_scalars() {
assert_eq!(
cranelift_type_to_lowered(types::I8).unwrap(),
LoweredType::I8
);
assert_eq!(
cranelift_type_to_lowered(types::I32).unwrap(),
LoweredType::I32
);
assert_eq!(
cranelift_type_to_lowered(types::F64).unwrap(),
LoweredType::F64
);
}
#[test]
fn cranelift_type_to_lowered_rejects_vector() {
let err = cranelift_type_to_lowered(types::I32X4).unwrap_err();
assert!(
matches!(err, MemLowerError::UnsupportedType(_)),
"vector types are deferred to wave-2"
);
}
}