use alloc::vec::Vec;
use ir_lang::{Block, Function, Inst, Terminator, Value};
use crate::program::{Const, Label, Op, Program, Reg};
pub(crate) fn lower(func: &Function) -> Program {
let block_count = func.block_count();
let mut labels: Vec<u32> = alloc::vec![0; block_count];
let capacity = func.value_count() + block_count * 3;
let mut ops: Vec<Op> = Vec::with_capacity(capacity);
for block in func.blocks() {
labels[block.index()] = ops.len() as u32;
lower_block(func, block, &mut ops, &mut labels);
}
Program {
name: func.name().into(),
params: param_registers(func),
registers: func.value_count() as u32,
ops,
labels,
}
}
fn param_registers(func: &Function) -> Vec<Reg> {
func.block_params(func.entry())
.iter()
.map(|&value| register(value))
.collect()
}
fn register(value: Value) -> Reg {
Reg(value.index() as u32)
}
fn lower_block(func: &Function, block: Block, ops: &mut Vec<Op>, labels: &mut Vec<u32>) {
for &value in func.insts(block) {
let Some(inst) = func.inst(value) else {
continue;
};
ops.push(lower_inst(register(value), inst));
}
if let Some(terminator) = func.terminator(block) {
lower_terminator(func, terminator, ops, labels);
}
}
fn lower_inst(dst: Reg, inst: &Inst) -> Op {
match inst {
Inst::Iconst(value) => Op::Const {
dst,
value: Const::Int(*value),
},
Inst::Fconst(value) => Op::Const {
dst,
value: Const::Float(*value),
},
Inst::Bconst(value) => Op::Const {
dst,
value: Const::Bool(*value),
},
Inst::Bin(op, lhs, rhs) => Op::Bin {
op: *op,
dst,
lhs: register(*lhs),
rhs: register(*rhs),
},
Inst::Un(op, src) => Op::Un {
op: *op,
dst,
src: register(*src),
},
}
}
fn lower_terminator(
func: &Function,
terminator: &Terminator,
ops: &mut Vec<Op>,
labels: &mut Vec<u32>,
) {
match terminator {
Terminator::Return(value) => {
ops.push(Op::Return {
value: value.map(register),
});
}
Terminator::Jump(target, args) => {
move_arguments(func, *target, args, ops);
ops.push(Op::Jump {
target: block_label(*target),
});
}
Terminator::Branch {
cond,
then_block,
then_args,
else_block,
else_args,
} => {
let else_arm = Label(labels.len() as u32);
labels.push(0);
ops.push(Op::JumpUnless {
cond: register(*cond),
target: else_arm,
});
move_arguments(func, *then_block, then_args, ops);
ops.push(Op::Jump {
target: block_label(*then_block),
});
labels[else_arm.0 as usize] = ops.len() as u32;
move_arguments(func, *else_block, else_args, ops);
ops.push(Op::Jump {
target: block_label(*else_block),
});
}
}
}
fn block_label(block: Block) -> Label {
Label(block.index() as u32)
}
fn move_arguments(func: &Function, target: Block, args: &[Value], ops: &mut Vec<Op>) {
for (&arg, ¶m) in args.iter().zip(func.block_params(target)) {
let (dst, src) = (register(param), register(arg));
if dst != src {
ops.push(Op::Move { dst, src });
}
}
}
#[cfg(test)]
#[allow(
clippy::unwrap_used,
reason = "tests build known-valid functions, so compilation cannot fail"
)]
mod tests {
use crate::compile;
use crate::program::{Const, Label, Op, Reg};
use ir_lang::{BinOp, Builder, Type, UnOp};
#[test]
fn test_straight_line_lowers_to_one_op_per_value() {
let mut b = Builder::new("f", &[Type::Int], Type::Int);
let x = b.block_params(b.entry())[0];
let sq = b.bin(BinOp::Mul, x, x);
let neg = b.un(UnOp::Neg, sq);
b.ret(Some(neg));
let program = compile(&b.finish()).unwrap();
assert_eq!(
program.ops(),
[
Op::Bin {
op: BinOp::Mul,
dst: Reg(1),
lhs: Reg(0),
rhs: Reg(0)
},
Op::Un {
op: UnOp::Neg,
dst: Reg(2),
src: Reg(1)
},
Op::Return {
value: Some(Reg(2))
},
]
);
}
#[test]
fn test_jump_moves_arguments_into_target_parameters() {
let mut b = Builder::new("f", &[], Type::Int);
let exit = b.create_block(&[Type::Int]);
let n = b.iconst(7);
b.jump(exit, &[n]);
b.switch_to(exit);
let p = b.block_params(exit)[0];
b.ret(Some(p));
let arg = Reg(n.index() as u32);
let param = Reg(p.index() as u32);
let program = compile(&b.finish()).unwrap();
assert!(program.ops().contains(&Op::Move {
dst: param,
src: arg
}));
assert!(program.ops().contains(&Op::Jump { target: Label(1) }));
}
#[test]
fn test_branch_emits_two_exclusive_arms() {
let mut b = Builder::new("f", &[Type::Bool], Type::Unit);
let c = b.block_params(b.entry())[0];
let yes = b.create_block(&[]);
let no = b.create_block(&[]);
b.branch(c, yes, &[], no, &[]);
b.switch_to(yes);
b.ret(None);
b.switch_to(no);
b.ret(None);
let program = compile(&b.finish()).unwrap();
assert!(matches!(
program.ops()[0],
Op::JumpUnless { cond: Reg(0), .. }
));
assert!(program.ops().contains(&Op::Jump { target: Label(1) })); assert!(program.ops().contains(&Op::Jump { target: Label(2) })); }
#[test]
fn test_self_move_is_not_emitted() {
let mut b = Builder::new("f", &[Type::Int], Type::Int);
let x = b.block_params(b.entry())[0];
b.ret(Some(x));
let program = compile(&b.finish()).unwrap();
assert!(!program.ops().iter().any(|op| matches!(op, Op::Move { .. })));
}
#[test]
fn test_constants_lower_to_their_payloads() {
let mut b = Builder::new("k", &[], Type::Int);
let _ = b.fconst(1.5);
let _ = b.bconst(true);
let n = b.iconst(42);
b.ret(Some(n));
let program = compile(&b.finish()).unwrap();
assert!(program.ops().contains(&Op::Const {
dst: Reg(2),
value: Const::Int(42)
}));
assert!(program.ops().contains(&Op::Const {
dst: Reg(1),
value: Const::Bool(true)
}));
assert!(program.ops().contains(&Op::Const {
dst: Reg(0),
value: Const::Float(1.5)
}));
}
}