use super::types::{
BasicBlock, BinOp, BlockId, CastKind, Constant, Function, Local, LocalDecl, Mutability,
Operand, Place, Rvalue, Statement, Terminator, Type, UnOp,
};
use std::collections::HashMap;
pub struct MirBuilder {
current_function: Option<Function>,
next_local: usize,
next_block: usize,
local_map: HashMap<String, Local>,
}
impl MirBuilder {
#[must_use]
pub fn new() -> Self {
Self {
current_function: None,
next_local: 0,
next_block: 0,
local_map: HashMap::new(),
}
}
pub fn start_function(&mut self, name: String, return_ty: Type) -> &mut Self {
self.current_function = Some(Function {
name,
params: Vec::new(),
return_ty,
locals: Vec::new(),
blocks: Vec::new(),
entry_block: BlockId(0),
});
self.next_local = 0;
self.next_block = 0;
self.local_map.clear();
self
}
pub fn add_param(&mut self, name: String, ty: Type) -> Local {
let local = self.alloc_local(ty, true, Some(name.clone()));
if let Some(ref mut func) = self.current_function {
func.params.push(local);
}
self.local_map.insert(name, local);
local
}
pub fn alloc_local(&mut self, ty: Type, mutable: bool, name: Option<String>) -> Local {
let id = Local(self.next_local);
self.next_local += 1;
let decl = LocalDecl {
id,
ty,
mutable,
name: name.clone(),
};
if let Some(ref mut func) = self.current_function {
func.locals.push(decl);
}
if let Some(n) = name {
self.local_map.insert(n, id);
}
id
}
pub fn get_local(&self, name: &str) -> Option<Local> {
self.local_map.get(name).copied()
}
pub fn new_block(&mut self) -> BlockId {
let id = BlockId(self.next_block);
self.next_block += 1;
if let Some(ref mut func) = self.current_function {
func.blocks.push(BasicBlock {
id,
statements: Vec::new(),
terminator: Terminator::Unreachable,
});
}
id
}
pub fn block_mut(&mut self, id: BlockId) -> Option<&mut BasicBlock> {
self.current_function
.as_mut()
.and_then(|f| f.blocks.get_mut(id.0))
}
pub fn push_statement(&mut self, block: BlockId, stmt: Statement) {
if let Some(bb) = self.block_mut(block) {
bb.statements.push(stmt);
}
}
pub fn set_terminator(&mut self, block: BlockId, term: Terminator) {
if let Some(bb) = self.block_mut(block) {
bb.terminator = term;
}
}
pub fn finish_function(&mut self) -> Option<Function> {
self.current_function.take()
}
pub fn assign(&mut self, block: BlockId, place: Place, rvalue: Rvalue) {
self.push_statement(block, Statement::Assign(place, rvalue));
}
pub fn storage_live(&mut self, block: BlockId, local: Local) {
self.push_statement(block, Statement::StorageLive(local));
}
pub fn storage_dead(&mut self, block: BlockId, local: Local) {
self.push_statement(block, Statement::StorageDead(local));
}
pub fn goto(&mut self, block: BlockId, target: BlockId) {
self.set_terminator(block, Terminator::Goto(target));
}
pub fn branch(
&mut self,
block: BlockId,
cond: Operand,
then_block: BlockId,
else_block: BlockId,
) {
self.set_terminator(
block,
Terminator::If {
condition: cond,
then_block,
else_block,
},
);
}
pub fn return_(&mut self, block: BlockId, value: Option<Operand>) {
self.set_terminator(block, Terminator::Return(value));
}
pub fn call_term(
&mut self,
block: BlockId,
func: Operand,
args: Vec<Operand>,
dest: Option<(Place, BlockId)>,
) {
self.set_terminator(
block,
Terminator::Call {
func,
args,
destination: dest,
},
);
}
pub fn switch(
&mut self,
block: BlockId,
discriminant: Operand,
targets: Vec<(Constant, BlockId)>,
default: Option<BlockId>,
) {
self.set_terminator(
block,
Terminator::Switch {
discriminant,
targets,
default,
},
);
}
}
impl MirBuilder {
pub fn binary_op(
&mut self,
block: BlockId,
dest: Local,
op: BinOp,
left: Operand,
right: Operand,
) {
let rvalue = Rvalue::BinaryOp(op, left, right);
self.assign(block, Place::Local(dest), rvalue);
}
pub fn unary_op(&mut self, block: BlockId, dest: Local, op: UnOp, operand: Operand) {
let rvalue = Rvalue::UnaryOp(op, operand);
self.assign(block, Place::Local(dest), rvalue);
}
pub fn call(
&mut self,
block: BlockId,
dest: Local,
func: Operand,
args: Vec<Operand>,
) -> BlockId {
let next_block = self.new_block();
self.call_term(block, func, args, Some((Place::Local(dest), next_block)));
next_block
}
pub fn cast(
&mut self,
block: BlockId,
dest: Local,
kind: CastKind,
operand: Operand,
target_ty: Type,
) {
let rvalue = Rvalue::Cast(kind, operand, target_ty);
self.assign(block, Place::Local(dest), rvalue);
}
pub fn ref_(&mut self, block: BlockId, dest: Local, mutability: Mutability, place: Place) {
let rvalue = Rvalue::Ref(mutability, place);
self.assign(block, Place::Local(dest), rvalue);
}
pub fn move_(&mut self, block: BlockId, dest: Place, source: Place) {
let rvalue = Rvalue::Use(Operand::Move(source));
self.assign(block, dest, rvalue);
}
pub fn copy(&mut self, block: BlockId, dest: Place, source: Place) {
let rvalue = Rvalue::Use(Operand::Copy(source));
self.assign(block, dest, rvalue);
}
pub fn const_(&mut self, block: BlockId, dest: Place, constant: Constant) {
let rvalue = Rvalue::Use(Operand::Constant(constant));
self.assign(block, dest, rvalue);
}
}
impl Default for MirBuilder {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::*;
#[test]
fn test_new_builder() {
let builder = MirBuilder::new();
assert_eq!(builder.next_local, 0);
assert_eq!(builder.next_block, 0);
assert!(builder.local_map.is_empty());
assert!(builder.current_function.is_none());
}
#[test]
fn test_default_builder() {
let builder1 = MirBuilder::new();
let builder2 = MirBuilder::default();
assert_eq!(builder1.next_local, builder2.next_local);
assert_eq!(builder1.next_block, builder2.next_block);
}
#[test]
fn test_start_function() {
let mut builder = MirBuilder::new();
builder.start_function("test".to_string(), Type::Bool);
let func = builder
.current_function
.as_ref()
.expect("operation should succeed in test");
assert_eq!(func.name, "test");
assert_eq!(func.return_ty, Type::Bool);
assert!(func.params.is_empty());
assert!(func.locals.is_empty());
assert!(func.blocks.is_empty());
assert_eq!(func.entry_block, BlockId(0));
}
#[test]
fn test_add_param() {
let mut builder = MirBuilder::new();
builder.start_function("test".to_string(), Type::Unit);
let param1 = builder.add_param("x".to_string(), Type::I32);
let param2 = builder.add_param("y".to_string(), Type::F64);
assert_eq!(param1, Local(0));
assert_eq!(param2, Local(1));
assert_eq!(builder.get_local("x"), Some(Local(0)));
assert_eq!(builder.get_local("y"), Some(Local(1)));
assert_eq!(builder.get_local("z"), None);
let func = builder
.current_function
.as_ref()
.expect("operation should succeed in test");
assert_eq!(func.params.len(), 2);
assert_eq!(func.locals.len(), 2);
}
#[test]
fn test_alloc_local_anonymous() {
let mut builder = MirBuilder::new();
builder.start_function("test".to_string(), Type::Unit);
let local1 = builder.alloc_local(Type::I32, false, None);
let local2 = builder.alloc_local(Type::Bool, true, None);
assert_eq!(local1, Local(0));
assert_eq!(local2, Local(1));
let func = builder
.current_function
.as_ref()
.expect("operation should succeed in test");
assert_eq!(func.locals.len(), 2);
assert!(!func.locals[0].mutable);
assert!(func.locals[1].mutable);
assert!(func.locals[0].name.is_none());
assert!(func.locals[1].name.is_none());
}
#[test]
fn test_alloc_local_named() {
let mut builder = MirBuilder::new();
builder.start_function("test".to_string(), Type::Unit);
let local = builder.alloc_local(Type::String, true, Some("var".to_string()));
assert_eq!(local, Local(0));
assert_eq!(builder.get_local("var"), Some(Local(0)));
let func = builder
.current_function
.as_ref()
.expect("operation should succeed in test");
assert_eq!(func.locals[0].name, Some("var".to_string()));
assert_eq!(func.locals[0].ty, Type::String);
assert!(func.locals[0].mutable);
}
#[test]
fn test_new_block() {
let mut builder = MirBuilder::new();
builder.start_function("test".to_string(), Type::Unit);
let block1 = builder.new_block();
let block2 = builder.new_block();
assert_eq!(block1, BlockId(0));
assert_eq!(block2, BlockId(1));
let func = builder
.current_function
.as_ref()
.expect("operation should succeed in test");
assert_eq!(func.blocks.len(), 2);
assert_eq!(func.blocks[0].id, block1);
assert_eq!(func.blocks[1].id, block2);
}
#[test]
fn test_block_mut() {
let mut builder = MirBuilder::new();
builder.start_function("test".to_string(), Type::Unit);
let block = builder.new_block();
let block_ref = builder.block_mut(block);
assert!(block_ref.is_some());
assert_eq!(
block_ref.expect("operation should succeed in test").id,
block
);
let invalid_block = builder.block_mut(BlockId(999));
assert!(invalid_block.is_none());
}
#[test]
fn test_push_statement() {
let mut builder = MirBuilder::new();
builder.start_function("test".to_string(), Type::Unit);
let block = builder.new_block();
let local = builder.alloc_local(Type::I32, false, None);
let stmt = Statement::StorageLive(local);
builder.push_statement(block, stmt);
let func = builder
.current_function
.as_ref()
.expect("operation should succeed in test");
assert_eq!(func.blocks[0].statements.len(), 1);
assert!(matches!(
func.blocks[0].statements[0],
Statement::StorageLive(_)
));
}
#[test]
fn test_set_terminator() {
let mut builder = MirBuilder::new();
builder.start_function("test".to_string(), Type::Unit);
let block = builder.new_block();
builder.set_terminator(block, Terminator::Return(None));
let func = builder
.current_function
.as_ref()
.expect("operation should succeed in test");
assert!(matches!(
func.blocks[0].terminator,
Terminator::Return(None)
));
}
#[test]
fn test_finish_function() {
let mut builder = MirBuilder::new();
builder.start_function("test".to_string(), Type::I32);
let func = builder.finish_function();
assert!(func.is_some());
assert_eq!(func.expect("operation should succeed in test").name, "test");
assert!(builder.current_function.is_none());
let no_func = builder.finish_function();
assert!(no_func.is_none());
}
#[test]
fn test_assign() {
let mut builder = MirBuilder::new();
builder.start_function("test".to_string(), Type::Unit);
let block = builder.new_block();
let local = builder.alloc_local(Type::I32, false, None);
let rvalue = Rvalue::Use(Operand::Constant(Constant::Int(42, Type::I32)));
builder.assign(block, Place::Local(local), rvalue);
let func = builder
.current_function
.as_ref()
.expect("operation should succeed in test");
assert_eq!(func.blocks[0].statements.len(), 1);
assert!(matches!(
func.blocks[0].statements[0],
Statement::Assign(_, _)
));
}
#[test]
fn test_storage_live() {
let mut builder = MirBuilder::new();
builder.start_function("test".to_string(), Type::Unit);
let block = builder.new_block();
let local = builder.alloc_local(Type::I32, false, None);
builder.storage_live(block, local);
let func = builder
.current_function
.as_ref()
.expect("operation should succeed in test");
assert!(matches!(
func.blocks[0].statements[0],
Statement::StorageLive(_)
));
}
#[test]
fn test_storage_dead() {
let mut builder = MirBuilder::new();
builder.start_function("test".to_string(), Type::Unit);
let block = builder.new_block();
let local = builder.alloc_local(Type::I32, false, None);
builder.storage_dead(block, local);
let func = builder
.current_function
.as_ref()
.expect("operation should succeed in test");
assert!(matches!(
func.blocks[0].statements[0],
Statement::StorageDead(_)
));
}
#[test]
fn test_goto() {
let mut builder = MirBuilder::new();
builder.start_function("test".to_string(), Type::Unit);
let block1 = builder.new_block();
let block2 = builder.new_block();
builder.goto(block1, block2);
let func = builder
.current_function
.as_ref()
.expect("operation should succeed in test");
assert!(matches!(func.blocks[0].terminator, Terminator::Goto(_)));
if let Terminator::Goto(target) = &func.blocks[0].terminator {
assert_eq!(*target, block2);
}
}
#[test]
fn test_branch() {
let mut builder = MirBuilder::new();
builder.start_function("test".to_string(), Type::Unit);
let entry = builder.new_block();
let then_block = builder.new_block();
let else_block = builder.new_block();
let cond = Operand::Constant(Constant::Bool(true));
builder.branch(entry, cond, then_block, else_block);
let func = builder
.current_function
.as_ref()
.expect("operation should succeed in test");
if let Terminator::If {
condition: _,
then_block: tb,
else_block: eb,
} = &func.blocks[0].terminator
{
assert_eq!(*tb, then_block);
assert_eq!(*eb, else_block);
} else {
panic!("Expected If terminator");
}
}
#[test]
fn test_return() {
let mut builder = MirBuilder::new();
builder.start_function("test".to_string(), Type::Unit);
let block = builder.new_block();
let operand = Operand::Constant(Constant::Int(42, Type::I32));
builder.return_(block, Some(operand));
let func = builder
.current_function
.as_ref()
.expect("operation should succeed in test");
assert!(matches!(
func.blocks[0].terminator,
Terminator::Return(Some(_))
));
}
#[test]
fn test_return_unit() {
let mut builder = MirBuilder::new();
builder.start_function("test".to_string(), Type::Unit);
let block = builder.new_block();
builder.return_(block, None);
let func = builder
.current_function
.as_ref()
.expect("operation should succeed in test");
assert!(matches!(
func.blocks[0].terminator,
Terminator::Return(None)
));
}
#[test]
fn test_call_term() {
let mut builder = MirBuilder::new();
builder.start_function("test".to_string(), Type::Unit);
let block1 = builder.new_block();
let block2 = builder.new_block();
let local = builder.alloc_local(Type::I32, false, None);
let func_operand = Operand::Constant(Constant::String("callee".to_string()));
let args = vec![Operand::Constant(Constant::Int(1, Type::I32))];
builder.call_term(
block1,
func_operand,
args,
Some((Place::Local(local), block2)),
);
let func = builder
.current_function
.as_ref()
.expect("operation should succeed in test");
if let Terminator::Call {
func: _,
args,
destination,
} = &func.blocks[0].terminator
{
assert_eq!(args.len(), 1);
assert!(destination.is_some());
} else {
panic!("Expected Call terminator");
}
}
#[test]
fn test_switch() {
let mut builder = MirBuilder::new();
builder.start_function("test".to_string(), Type::Unit);
let entry = builder.new_block();
let case1 = builder.new_block();
let case2 = builder.new_block();
let default = builder.new_block();
let discriminant = Operand::Constant(Constant::Int(0, Type::I32));
let targets = vec![
(Constant::Int(1, Type::I32), case1),
(Constant::Int(2, Type::I32), case2),
];
builder.switch(entry, discriminant, targets, Some(default));
let func = builder
.current_function
.as_ref()
.expect("operation should succeed in test");
if let Terminator::Switch {
targets: t,
default: d,
..
} = &func.blocks[0].terminator
{
assert_eq!(t.len(), 2);
assert_eq!(*d, Some(default));
} else {
panic!("Expected Switch terminator");
}
}
#[test]
fn test_binary_op() {
let mut builder = MirBuilder::new();
builder.start_function("test".to_string(), Type::Unit);
let block = builder.new_block();
let dest = builder.alloc_local(Type::I32, false, None);
let left = Operand::Constant(Constant::Int(5, Type::I32));
let right = Operand::Constant(Constant::Int(3, Type::I32));
builder.binary_op(block, dest, BinOp::Add, left, right);
let func = builder
.current_function
.as_ref()
.expect("operation should succeed in test");
if let Statement::Assign(place, rvalue) = &func.blocks[0].statements[0] {
assert_eq!(*place, Place::Local(dest));
assert!(matches!(rvalue, Rvalue::BinaryOp(BinOp::Add, _, _)));
} else {
panic!("Expected Assign statement");
}
}
#[test]
fn test_unary_op() {
let mut builder = MirBuilder::new();
builder.start_function("test".to_string(), Type::Unit);
let block = builder.new_block();
let dest = builder.alloc_local(Type::I32, false, None);
let operand = Operand::Constant(Constant::Int(-5, Type::I32));
builder.unary_op(block, dest, UnOp::Neg, operand);
let func = builder
.current_function
.as_ref()
.expect("operation should succeed in test");
if let Statement::Assign(place, rvalue) = &func.blocks[0].statements[0] {
assert_eq!(*place, Place::Local(dest));
assert!(matches!(rvalue, Rvalue::UnaryOp(UnOp::Neg, _)));
} else {
panic!("Expected Assign statement");
}
}
#[test]
fn test_call() {
let mut builder = MirBuilder::new();
builder.start_function("test".to_string(), Type::Unit);
let block = builder.new_block();
let dest = builder.alloc_local(Type::I32, false, None);
let func_operand = Operand::Constant(Constant::String("helper".to_string()));
let args = vec![Operand::Constant(Constant::Int(42, Type::I32))];
let next_block = builder.call(block, dest, func_operand, args);
assert_eq!(next_block, BlockId(1));
let func = builder
.current_function
.as_ref()
.expect("operation should succeed in test");
assert_eq!(func.blocks.len(), 2);
if let Terminator::Call { destination, .. } = &func.blocks[0].terminator {
assert_eq!(destination, &Some((Place::Local(dest), next_block)));
} else {
panic!("Expected Call terminator");
}
}
#[test]
fn test_cast() {
let mut builder = MirBuilder::new();
builder.start_function("test".to_string(), Type::Unit);
let block = builder.new_block();
let dest = builder.alloc_local(Type::F64, false, None);
let operand = Operand::Constant(Constant::Int(42, Type::I32));
builder.cast(block, dest, CastKind::Numeric, operand, Type::F64);
let func = builder
.current_function
.as_ref()
.expect("operation should succeed in test");
if let Statement::Assign(place, rvalue) = &func.blocks[0].statements[0] {
assert_eq!(*place, Place::Local(dest));
if let Rvalue::Cast(kind, _, target_ty) = rvalue {
assert_eq!(*kind, CastKind::Numeric);
assert_eq!(*target_ty, Type::F64);
} else {
panic!("Expected Cast rvalue");
}
} else {
panic!("Expected Assign statement");
}
}
#[test]
fn test_ref() {
let mut builder = MirBuilder::new();
builder.start_function("test".to_string(), Type::Unit);
let block = builder.new_block();
let source = builder.alloc_local(Type::I32, true, None);
let dest = builder.alloc_local(
Type::Ref(Box::new(Type::I32), Mutability::Mutable),
false,
None,
);
builder.ref_(block, dest, Mutability::Mutable, Place::Local(source));
let func = builder
.current_function
.as_ref()
.expect("operation should succeed in test");
if let Statement::Assign(place, rvalue) = &func.blocks[0].statements[0] {
assert_eq!(*place, Place::Local(dest));
assert!(matches!(rvalue, Rvalue::Ref(Mutability::Mutable, _)));
} else {
panic!("Expected Assign statement");
}
}
#[test]
fn test_move() {
let mut builder = MirBuilder::new();
builder.start_function("test".to_string(), Type::Unit);
let block = builder.new_block();
let source = builder.alloc_local(Type::String, false, None);
let dest = builder.alloc_local(Type::String, false, None);
builder.move_(block, Place::Local(dest), Place::Local(source));
let func = builder
.current_function
.as_ref()
.expect("operation should succeed in test");
if let Statement::Assign(place, rvalue) = &func.blocks[0].statements[0] {
assert_eq!(*place, Place::Local(dest));
assert!(matches!(rvalue, Rvalue::Use(Operand::Move(_))));
} else {
panic!("Expected Assign statement");
}
}
#[test]
fn test_copy() {
let mut builder = MirBuilder::new();
builder.start_function("test".to_string(), Type::Unit);
let block = builder.new_block();
let source = builder.alloc_local(Type::I32, false, None);
let dest = builder.alloc_local(Type::I32, false, None);
builder.copy(block, Place::Local(dest), Place::Local(source));
let func = builder
.current_function
.as_ref()
.expect("operation should succeed in test");
if let Statement::Assign(place, rvalue) = &func.blocks[0].statements[0] {
assert_eq!(*place, Place::Local(dest));
assert!(matches!(rvalue, Rvalue::Use(Operand::Copy(_))));
} else {
panic!("Expected Assign statement");
}
}
#[test]
fn test_const() {
let mut builder = MirBuilder::new();
builder.start_function("test".to_string(), Type::Unit);
let block = builder.new_block();
let dest = builder.alloc_local(Type::Bool, false, None);
builder.const_(block, Place::Local(dest), Constant::Bool(true));
let func = builder
.current_function
.as_ref()
.expect("operation should succeed in test");
if let Statement::Assign(place, rvalue) = &func.blocks[0].statements[0] {
assert_eq!(*place, Place::Local(dest));
assert!(matches!(
rvalue,
Rvalue::Use(Operand::Constant(Constant::Bool(true)))
));
} else {
panic!("Expected Assign statement");
}
}
#[test]
fn test_build_simple_function() {
let mut builder = MirBuilder::new();
builder.start_function("add".to_string(), Type::I32);
let a = builder.add_param("a".to_string(), Type::I32);
let b = builder.add_param("b".to_string(), Type::I32);
let entry = builder.new_block();
let result = builder.alloc_local(Type::I32, false, Some("result".to_string()));
builder.storage_live(entry, result);
builder.binary_op(
entry,
result,
BinOp::Add,
Operand::Copy(Place::Local(a)),
Operand::Copy(Place::Local(b)),
);
builder.return_(entry, Some(Operand::Move(Place::Local(result))));
let func = builder
.finish_function()
.expect("operation should succeed in test");
assert_eq!(func.name, "add");
assert_eq!(func.params.len(), 2);
assert_eq!(func.blocks.len(), 1);
}
#[test]
fn test_build_if_else() {
let mut builder = MirBuilder::new();
builder.start_function("abs".to_string(), Type::I32);
let x = builder.add_param("x".to_string(), Type::I32);
let entry = builder.new_block();
let then_block = builder.new_block();
let else_block = builder.new_block();
let merge_block = builder.new_block();
let cond = builder.alloc_local(Type::Bool, false, None);
builder.binary_op(
entry,
cond,
BinOp::Lt,
Operand::Copy(Place::Local(x)),
Operand::Constant(Constant::Int(0, Type::I32)),
);
builder.branch(
entry,
Operand::Copy(Place::Local(cond)),
then_block,
else_block,
);
let neg_x = builder.alloc_local(Type::I32, false, None);
builder.unary_op(then_block, neg_x, UnOp::Neg, Operand::Copy(Place::Local(x)));
builder.goto(then_block, merge_block);
builder.goto(else_block, merge_block);
builder.return_(merge_block, Some(Operand::Copy(Place::Local(x))));
let func = builder
.finish_function()
.expect("operation should succeed in test");
assert_eq!(func.blocks.len(), 4);
}
#[test]
fn test_operations_without_function() {
let mut builder = MirBuilder::new();
let local = builder.alloc_local(Type::I32, false, None);
let block = builder.new_block();
builder.push_statement(block, Statement::StorageLive(local));
builder.set_terminator(block, Terminator::Return(None));
assert!(builder.current_function.is_none());
}
#[test]
fn test_complex_switch() {
let mut builder = MirBuilder::new();
builder.start_function("test_switch".to_string(), Type::I32);
let param = builder.add_param("value".to_string(), Type::I32);
let entry = builder.new_block();
let case1 = builder.new_block();
let case2 = builder.new_block();
let case3 = builder.new_block();
let default = builder.new_block();
let discriminant = Operand::Copy(Place::Local(param));
let targets = vec![
(Constant::Int(1, Type::I32), case1),
(Constant::Int(2, Type::I32), case2),
(Constant::Int(3, Type::I32), case3),
];
builder.switch(entry, discriminant, targets, Some(default));
builder.return_(case1, Some(Operand::Constant(Constant::Int(10, Type::I32))));
builder.return_(case2, Some(Operand::Constant(Constant::Int(20, Type::I32))));
builder.return_(case3, Some(Operand::Constant(Constant::Int(30, Type::I32))));
builder.return_(
default,
Some(Operand::Constant(Constant::Int(0, Type::I32))),
);
let func = builder
.finish_function()
.expect("operation should succeed in test");
assert_eq!(func.blocks.len(), 5);
if let Terminator::Switch {
targets,
default: d,
..
} = &func.blocks[0].terminator
{
assert_eq!(targets.len(), 3);
assert_eq!(*d, Some(default));
} else {
panic!("Expected Switch terminator");
}
}
#[test]
fn test_all_binary_ops() {
let mut builder = MirBuilder::new();
builder.start_function("test_binops".to_string(), Type::Unit);
let block = builder.new_block();
let ops = [
BinOp::Add,
BinOp::Sub,
BinOp::Mul,
BinOp::Div,
BinOp::Rem,
BinOp::Pow,
BinOp::BitAnd,
BinOp::BitOr,
BinOp::BitXor,
BinOp::Shl,
BinOp::Shr,
BinOp::Eq,
BinOp::Ne,
BinOp::Lt,
BinOp::Le,
BinOp::Gt,
BinOp::Ge,
BinOp::And,
BinOp::Or,
BinOp::NullCoalesce,
];
for (i, op) in ops.iter().enumerate() {
let dest = builder.alloc_local(Type::Bool, false, None);
let left = Operand::Constant(Constant::Int(i as i128, Type::I32));
let right = Operand::Constant(Constant::Int((i + 1) as i128, Type::I32));
builder.binary_op(block, dest, *op, left, right);
}
let func = builder
.finish_function()
.expect("operation should succeed in test");
assert_eq!(func.blocks[0].statements.len(), ops.len());
}
#[test]
fn test_all_unary_ops() {
let mut builder = MirBuilder::new();
builder.start_function("test_unops".to_string(), Type::Unit);
let block = builder.new_block();
let ops = vec![UnOp::Neg, UnOp::Not, UnOp::BitNot, UnOp::Ref];
for op in ops {
let dest = builder.alloc_local(Type::I32, false, None);
let operand = Operand::Constant(Constant::Int(42, Type::I32));
builder.unary_op(block, dest, op, operand);
}
let func = builder
.finish_function()
.expect("operation should succeed in test");
assert_eq!(func.blocks[0].statements.len(), 4);
}
#[test]
fn test_all_cast_kinds() {
let mut builder = MirBuilder::new();
builder.start_function("test_casts".to_string(), Type::Unit);
let block = builder.new_block();
let cast_kinds = vec![CastKind::Numeric, CastKind::Pointer, CastKind::Unsize];
for kind in cast_kinds {
let dest = builder.alloc_local(Type::F64, false, None);
let operand = Operand::Constant(Constant::Int(42, Type::I32));
builder.cast(block, dest, kind, operand, Type::F64);
}
let func = builder
.finish_function()
.expect("operation should succeed in test");
assert_eq!(func.blocks[0].statements.len(), 3);
}
#[test]
fn test_mutability_variants() {
let mut builder = MirBuilder::new();
builder.start_function("test_mutability".to_string(), Type::Unit);
let block = builder.new_block();
let source = builder.alloc_local(Type::I32, true, None);
let immut_ref = builder.alloc_local(
Type::Ref(Box::new(Type::I32), Mutability::Immutable),
false,
None,
);
builder.ref_(
block,
immut_ref,
Mutability::Immutable,
Place::Local(source),
);
let mut_ref = builder.alloc_local(
Type::Ref(Box::new(Type::I32), Mutability::Mutable),
false,
None,
);
builder.ref_(block, mut_ref, Mutability::Mutable, Place::Local(source));
let func = builder
.finish_function()
.expect("operation should succeed in test");
assert_eq!(func.blocks[0].statements.len(), 2);
}
#[test]
fn test_call_no_destination() {
let mut builder = MirBuilder::new();
builder.start_function("test".to_string(), Type::Unit);
let block = builder.new_block();
let func_operand = Operand::Constant(Constant::String("print".to_string()));
let args = vec![Operand::Constant(Constant::String("hello".to_string()))];
builder.call_term(block, func_operand, args, None);
let func = builder
.current_function
.as_ref()
.expect("operation should succeed in test");
if let Terminator::Call { destination, .. } = &func.blocks[0].terminator {
assert!(destination.is_none());
} else {
panic!("Expected Call terminator");
}
}
#[test]
fn test_switch_no_default() {
let mut builder = MirBuilder::new();
builder.start_function("test".to_string(), Type::Unit);
let entry = builder.new_block();
let case1 = builder.new_block();
let discriminant = Operand::Constant(Constant::Int(1, Type::I32));
let targets = vec![(Constant::Int(1, Type::I32), case1)];
builder.switch(entry, discriminant, targets, None);
let func = builder
.current_function
.as_ref()
.expect("operation should succeed in test");
if let Terminator::Switch { default, .. } = &func.blocks[0].terminator {
assert!(default.is_none());
} else {
panic!("Expected Switch terminator");
}
}
}
#[cfg(test)]
mod property_tests_builder {
use super::*;
use proptest::prelude::*;
proptest! {
#[test]
fn test_new_never_panics(_: u32) {
let _builder = MirBuilder::new();
}
#[test]
fn test_function_name_preserved(name: String, return_type_idx: usize) {
prop_assume!(!name.is_empty() && name.len() <= 100);
let types = [Type::Unit, Type::Bool, Type::I32, Type::F64, Type::String];
let return_type = types[return_type_idx % types.len()].clone();
let mut builder = MirBuilder::new();
builder.start_function(name.clone(), return_type.clone());
let func = builder.finish_function().expect("operation should succeed in test");
prop_assert_eq!(func.name, name);
prop_assert_eq!(func.return_ty, return_type);
}
#[test]
fn test_local_allocation_sequential(count in 0..=50usize) {
let mut builder = MirBuilder::new();
builder.start_function("test".to_string(), Type::Unit);
let mut locals = Vec::new();
for i in 0..count {
let local = builder.alloc_local(Type::I32, i % 2 == 0, None);
locals.push(local);
}
for (i, local) in locals.iter().enumerate() {
prop_assert_eq!(local.0, i);
}
}
#[test]
fn test_block_allocation_sequential(count in 0..=50usize) {
let mut builder = MirBuilder::new();
builder.start_function("test".to_string(), Type::Unit);
let mut blocks = Vec::new();
for _ in 0..count {
let block = builder.new_block();
blocks.push(block);
}
for (i, block) in blocks.iter().enumerate() {
prop_assert_eq!(block.0, i);
}
}
#[test]
fn test_named_locals_findable(name: String) {
prop_assume!(!name.is_empty() && name.len() <= 50);
let mut builder = MirBuilder::new();
builder.start_function("test".to_string(), Type::Unit);
let local = builder.alloc_local(Type::I32, false, Some(name.clone()));
prop_assert_eq!(builder.get_local(&name), Some(local));
}
#[test]
fn test_params_numbered_correctly(param_names in prop::collection::vec("[a-zA-Z0-9]+", 0..=10)) {
prop_assume!(param_names.iter().all(|n| !n.is_empty() && n.len() <= 50));
let mut builder = MirBuilder::new();
builder.start_function("test".to_string(), Type::Unit);
let mut params = Vec::new();
for name in ¶m_names {
let param = builder.add_param(name.clone(), Type::I32);
params.push(param);
}
for (i, param) in params.iter().enumerate() {
prop_assert_eq!(param.0, i);
}
for name in ¶m_names {
prop_assert!(builder.get_local(name).is_some());
}
}
#[test]
fn test_statement_count_increases(stmt_count in 0..=20usize) {
let mut builder = MirBuilder::new();
builder.start_function("test".to_string(), Type::Unit);
let block = builder.new_block();
for i in 0..stmt_count {
let local = builder.alloc_local(Type::I32, false, None);
builder.push_statement(block, Statement::StorageLive(local));
let func = builder.current_function.as_ref().expect("operation should succeed in test");
prop_assert_eq!(func.blocks[0].statements.len(), i + 1);
}
}
#[test]
fn test_binary_op_preserves_operator(op_idx: usize) {
let ops = [BinOp::Add, BinOp::Sub, BinOp::Mul, BinOp::Eq, BinOp::Lt];
let op = ops[op_idx % ops.len()];
let mut builder = MirBuilder::new();
builder.start_function("test".to_string(), Type::Unit);
let block = builder.new_block();
let dest = builder.alloc_local(Type::Bool, false, None);
let left = Operand::Constant(Constant::Int(1, Type::I32));
let right = Operand::Constant(Constant::Int(2, Type::I32));
builder.binary_op(block, dest, op, left, right);
let func = builder.finish_function().expect("operation should succeed in test");
if let Statement::Assign(_, Rvalue::BinaryOp(actual_op, _, _)) = &func.blocks[0].statements[0] {
prop_assert_eq!(*actual_op, op);
} else {
return Err(proptest::test_runner::TestCaseError::fail("Expected binary op assignment"));
}
}
#[test]
fn test_unary_op_preserves_operator(op_idx: usize) {
let ops = [UnOp::Neg, UnOp::Not, UnOp::BitNot];
let op = ops[op_idx % ops.len()];
let mut builder = MirBuilder::new();
builder.start_function("test".to_string(), Type::Unit);
let block = builder.new_block();
let dest = builder.alloc_local(Type::I32, false, None);
let operand = Operand::Constant(Constant::Int(42, Type::I32));
builder.unary_op(block, dest, op, operand);
let func = builder.finish_function().expect("operation should succeed in test");
if let Statement::Assign(_, Rvalue::UnaryOp(actual_op, _)) = &func.blocks[0].statements[0] {
prop_assert_eq!(*actual_op, op);
} else {
return Err(proptest::test_runner::TestCaseError::fail("Expected unary op assignment"));
}
}
#[test]
fn test_no_function_operations_safe(op_count in 0..=10usize) {
let mut builder = MirBuilder::new();
for _ in 0..op_count {
let local = builder.alloc_local(Type::I32, false, None);
let block = builder.new_block();
builder.push_statement(block, Statement::StorageLive(local));
builder.set_terminator(block, Terminator::Return(None));
}
prop_assert!(builder.current_function.is_none());
}
#[test]
fn test_block_mutation_consistent(valid_block_count in 0..=10usize, invalid_id in 100..=200usize) {
let mut builder = MirBuilder::new();
builder.start_function("test".to_string(), Type::Unit);
let mut valid_blocks = Vec::new();
for _ in 0..valid_block_count {
let block = builder.new_block();
valid_blocks.push(block);
}
for &block in &valid_blocks {
prop_assert!(builder.block_mut(block).is_some());
}
let invalid_block = BlockId(invalid_id);
prop_assert!(builder.block_mut(invalid_block).is_none());
}
}
}