use std::cmp::Ordering;
use std::collections::HashSet;
use rucc_base::Symbol;
use rucc_ir::{
Block, BlockCall, Builder, Extra, Flags, Func, Imm, Inst, InstData, MemInfo, MemOrder, Opcode,
Restrict, Type, Value,
};
use rucc_cost::Goal;
use rucc_cost::heuristics::SWITCH_CONVERSION_MAX_GROWTH;
use crate::cfg::Cfg;
use crate::{Analyses, Fuel, Pass, Preserved, ReadOnly, Stats};
const CONVERTED: &str = "switch replaced by a range check and the arithmetic its arms were doing";
const TABLED: &str = "switch replaced by a range check and a load from a table of its answers";
const NO_FUEL: &str = "switch left alone, the pass ran out of fuel";
const TOO_FEW: &str = "switch left alone, it has too few labels for arithmetic to be cheaper";
const NOT_CONSECUTIVE: &str = "switch left alone, its labels are not consecutive";
const ARM_IS_SHARED: &str = "switch left alone, an arm is reached from somewhere other than it";
const ARM_DOES_WORK: &str = "switch left alone, an arm does more than work out a constant";
const ARMS_DIFFER: &str = "switch left alone, its arms do not all hand on the same thing";
const NOT_AFFINE: &str = "switch left alone, its answers are not a fixed multiple of the label \
plus a constant";
const WIDTHS_DIFFER: &str = "switch left alone, its answers are not as wide as its labels";
const TOO_SPARSE: &str = "switch left alone, a table of its answers would be mostly holes";
const LABEL_TOO_WIDE: &str = "switch left alone, its label is wider than a word";
const CELL_IS_ODD: &str =
"switch left alone, its answers are not a whole number of bytes of integer";
const LABELS: usize = 3;
const GROWTH: i128 = SWITCH_CONVERSION_MAX_GROWTH as i128;
#[derive(Debug)]
pub struct SwitchConv;
impl Pass for SwitchConv {
fn name(&self) -> &'static str {
"switch-conv"
}
fn describe(&self) -> &'static str {
"a switch whose arms give constants becomes a range check and arithmetic or a table load"
}
fn preserves(&self) -> Preserved {
Preserved::NONE
}
fn run(&self, func: &mut Func, an: &mut Analyses, fuel: &mut Fuel) -> Stats {
convert(func, an, fuel, None)
}
fn run_emitting(
&self,
func: &mut Func,
an: &mut Analyses,
fuel: &mut Fuel,
data: &mut ReadOnly<'_>,
) -> Stats {
convert(func, an, fuel, Some(data))
}
}
fn convert(
func: &mut Func,
an: &mut Analyses,
fuel: &mut Fuel,
mut data: Option<&mut ReadOnly<'_>>,
) -> Stats {
let mut stats = Stats::new();
if func.entry().is_none() {
return stats;
}
let cfg = an.cfg(func);
let found: Vec<Inst> = func
.blocks()
.filter_map(|block| func.terminator(block))
.filter(|&inst| func[inst].opcode == Opcode::Switch)
.collect();
let index_bits = data.as_ref().map(|data| data.pointer_bits());
let small = an.machine().goal() == Goal::Size;
let mut plans = Vec::new();
for inst in found {
match plan(func, cfg, inst, index_bits, small) {
Ok(plan) => plans.push(plan),
Err(why) => stats.missed(why),
}
}
let mut changed = false;
for plan in plans {
if !fuel.take() {
stats.missed(NO_FUEL);
continue;
}
let table = match (&plan.how, data.as_deref_mut()) {
(How::Table { cell, cells, .. }, Some(data)) => {
Some(data.table(cell.ty, cells.clone()))
}
_ => None,
};
stats.optimized(if table.is_some() { TABLED } else { CONVERTED });
apply(func, &plan, table);
changed = true;
}
if changed {
an.clear();
}
stats
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum Hands {
On(Block),
Back,
}
#[derive(Debug)]
struct Plan {
inst: Inst,
value: Value,
ty: Type,
hands: Hands,
args: Vec<Value>,
answer: usize,
how: How,
arms: Vec<Block>,
holes: Vec<i128>,
}
#[derive(Debug)]
enum How {
Line {
scale: i128,
offset: i128,
},
Table {
low: i128,
ty: Type,
cell: Cell,
cells: Vec<i128>,
index_bits: u32,
},
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
struct Cell {
ty: Type,
signed: bool,
}
fn plan(
func: &Func,
cfg: &Cfg,
inst: Inst,
index_bits: Option<u32>,
small: bool,
) -> Result<Plan, &'static str> {
let Extra::Switch(info) = func[inst].extra else { return Err(ARMS_DIFFER) };
let info = func[info];
let Some(&value) = func[func[inst].args].first() else { return Err(ARMS_DIFFER) };
let ty = func[value].ty;
if !ty.is_int() {
return Err(WIDTHS_DIFFER);
}
let calls: Vec<BlockCall> = func[info.targets].to_vec();
let labels: Vec<i128> = func[info.cases].iter().map(|imm| imm.signed(ty)).collect();
let Some((&default, arms)) = calls.split_first() else { return Err(ARMS_DIFFER) };
if arms.len() != labels.len() || arms.len() < LABELS {
return Err(TOO_FEW);
}
if arms.iter().any(|call| call.block == default.block) {
return Err(ARM_IS_SHARED);
}
let consecutive = labels.windows(2).all(|pair| pair[1].checked_sub(pair[0]) == Some(1));
if !consecutive && index_bits.is_none() {
return Err(NOT_CONSECUTIVE);
}
let mut hands = None;
let mut shared: Option<Vec<Value>> = None;
let mut answer = None;
let mut handed = Vec::new();
for call in arms {
if !call.args.is_empty() {
return Err(ARM_DOES_WORK);
}
if cfg.predecessors(call.block).len() != 1 {
return Err(ARM_IS_SHARED);
}
if func.block_name(call.block).is_some() {
return Err(ARM_IS_SHARED);
}
let (way, args) = tail(func, call.block)?;
if *hands.get_or_insert(way) != way {
return Err(ARMS_DIFFER);
}
let previous = shared.get_or_insert_with(|| args.clone());
if previous.len() != args.len() {
return Err(ARMS_DIFFER);
}
for (index, (&mine, &theirs)) in previous.iter().zip(&args).enumerate() {
if mine == theirs {
continue;
}
if *answer.get_or_insert(index) != index {
return Err(ARMS_DIFFER);
}
}
handed.push(args);
}
let (Some(hands), Some(args)) = (hands, shared) else { return Err(ARMS_DIFFER) };
let answer = answer.ok_or(NOT_AFFINE)?;
let mut answers = Vec::with_capacity(handed.len());
for args in &handed {
let Some(number) = constant(func, args[answer]) else { return Err(NOT_AFFINE) };
answers.push(number);
}
let kind = func[args[answer]].ty;
let line = if consecutive && kind == ty { line(&labels, &answers, ty) } else { None };
let (how, holes) = match (line, index_bits) {
(Some((scale, offset)), _) => (How::Line { scale, offset }, Vec::new()),
(None, Some(index_bits)) => {
let fill = fallback(func, default, hands, &args, answer);
let shape = Shape { ty, kind, index_bits, small };
table(&labels, &answers, shape, fill)?
}
(None, None) if kind != ty => return Err(WIDTHS_DIFFER),
(None, None) => return Err(NOT_AFFINE),
};
Ok(Plan {
inst,
value,
ty,
hands,
args,
answer,
how,
arms: arms.iter().map(|call| call.block).collect(),
holes,
})
}
fn fallback(
func: &Func,
default: BlockCall,
hands: Hands,
args: &[Value],
answer: usize,
) -> Option<i128> {
let theirs = if default.args.is_empty() {
let (way, theirs) = tail(func, default.block).ok()?;
if way != hands {
return None;
}
theirs
} else if hands == Hands::On(default.block) {
func[default.args].to_vec()
} else {
return None;
};
if theirs.len() != args.len() {
return None;
}
let agrees =
args.iter().zip(&theirs).enumerate().all(|(at, (mine, it))| at == answer || mine == it);
if !agrees {
return None;
}
constant(func, theirs[answer])
}
#[derive(Clone, Copy, Debug)]
struct Shape {
ty: Type,
kind: Type,
index_bits: u32,
small: bool,
}
fn table(
labels: &[i128],
answers: &[i128],
shape: Shape,
fill: Option<i128>,
) -> Result<(How, Vec<i128>), &'static str> {
let Shape { ty, kind, index_bits, small } = shape;
if ty.bits() > 64 {
return Err(LABEL_TOO_WIDE);
}
if !kind.is_int() || !matches!(kind.bits(), 8 | 16 | 32 | 64) {
return Err(CELL_IS_ODD);
}
let (Some(&low), Some(&high)) = (labels.iter().min(), labels.iter().max()) else {
return Err(TOO_FEW);
};
let span = high - low + 1;
if span > GROWTH * labels.len() as i128 {
return Err(TOO_SPARSE);
}
let mut cells = vec![None; usize::try_from(span).map_err(|_| TOO_SPARSE)?];
for (&label, &answer) in labels.iter().zip(answers) {
let at = usize::try_from(label - low).map_err(|_| TOO_SPARSE)?;
cells[at] = Some(answer);
}
let holes: Vec<i128> = match fill {
Some(_) => (low..=high).filter(|&label| cells[(label - low) as usize].is_none()).collect(),
None => Vec::new(),
};
let cells: Vec<i128> = cells.into_iter().map(|cell| cell.or(fill).unwrap_or(0)).collect();
let cell = if small { narrowest(&cells, kind) } else { Cell { ty: kind, signed: false } };
Ok((How::Table { low, ty: kind, cell, cells, index_bits }, holes))
}
fn narrowest(answers: &[i128], kind: Type) -> Cell {
let whole = 1i128 << kind.bits();
for bits in [8u32, 16, 32] {
if bits >= kind.bits() {
break;
}
let half = 1i128 << (bits - 1);
if answers.iter().all(|&answer| (-half..half).contains(&answer)) {
return Cell { ty: Type::int(bits), signed: true };
}
if answers.iter().all(|&answer| answer.rem_euclid(whole) < half * 2) {
return Cell { ty: Type::int(bits), signed: false };
}
}
Cell { ty: kind, signed: false }
}
fn tail(func: &Func, block: Block) -> Result<(Hands, Vec<Value>), &'static str> {
let Some(last) = func.terminator(block) else { return Err(ARM_DOES_WORK) };
for inst in func.insts(block) {
if inst != last && func[inst].opcode != Opcode::IConst {
return Err(ARM_DOES_WORK);
}
}
let args: Vec<Value> = match func[last].opcode {
Opcode::Jump => {
let Some(call) = func.successors(last).next() else { return Err(ARM_DOES_WORK) };
let args = func[call.args].to_vec();
return Ok((Hands::On(call.block), args));
}
Opcode::Return => func[func[last].args].to_vec(),
_ => return Err(ARM_DOES_WORK),
};
Ok((Hands::Back, args))
}
fn arithmetic(builder: &mut Builder<'_>, plan: &Plan, scale: i128, offset: i128) -> Value {
let scaled = match scale {
0 => builder.iconst(plan.ty, offset),
1 => plan.value,
scale => {
let by = builder.iconst(plan.ty, scale);
builder.binary(Opcode::Mul, plan.value, by, Flags::NONE)
}
};
if offset == 0 || scale == 0 {
scaled
} else {
let by = builder.iconst(plan.ty, offset);
builder.binary(Opcode::Add, scaled, by, Flags::NONE)
}
}
fn look_up(
builder: &mut Builder<'_>,
plan: &Plan,
name: Symbol,
low: i128,
ty: Type,
index_bits: u32,
) -> Value {
let from = if low == 0 {
plan.value
} else {
let by = builder.iconst(plan.ty, low);
builder.binary(Opcode::Sub, plan.value, by, Flags::NONE)
};
let word = Type::int(index_bits);
let index = match plan.ty.bits().cmp(&index_bits) {
Ordering::Less => builder.unary(Opcode::ZExt, from, word),
Ordering::Greater => builder.unary(Opcode::Trunc, from, word),
Ordering::Equal => from,
};
let bytes = ty.bits() / 8;
let distance = if bytes == 1 {
index
} else {
let by = builder.iconst(word, i128::from(bytes));
builder.binary(Opcode::Mul, index, by, Flags::NONE)
};
let base = builder.value(
InstData { extra: Extra::Symbol(name), ..InstData::new(Opcode::GlobalAddr) },
Type::PTR,
);
let cell = builder.binary(Opcode::PtrAdd, base, distance, Flags::NONE);
let info = MemInfo {
size: u64::from(bytes),
align: bytes,
order: MemOrder::NotAtomic,
tbaa: None,
owns: 0,
restrict: Restrict::NONE,
};
builder.load(ty, cell, info, Flags::NONE)
}
fn constant(func: &Func, value: Value) -> Option<i128> {
crate::discharge::constant(func, value)
}
fn line(labels: &[i128], answers: &[i128], ty: Type) -> Option<(i128, i128)> {
let [first, second, ..] = *labels else { return None };
let [low, high, ..] = *answers else { return None };
debug_assert_eq!(second - first, 1, "the labels were checked to be consecutive");
let scale = high.checked_sub(low)?;
let offset = low.checked_sub(scale.checked_mul(first)?)?;
for (&label, &answer) in labels.iter().zip(answers) {
let want = scale.checked_mul(label)?.checked_add(offset)?;
if wrap(want, ty) != answer {
return None;
}
}
Some((scale, offset))
}
fn wrap(value: i128, ty: Type) -> i128 {
Imm::int(value, ty).signed(ty)
}
fn apply(func: &mut Func, plan: &Plan, table: Option<Symbol>) {
let span = func.span(plan.inst);
let hit = func.create_block();
let mut builder = Builder::new(func, hit).at(span);
let answer = match (&plan.how, table) {
(&How::Line { scale, offset }, _) => arithmetic(&mut builder, plan, scale, offset),
(&How::Table { low, ty, cell, index_bits, .. }, Some(name)) => {
let read = look_up(&mut builder, plan, name, low, cell.ty, index_bits);
match (cell.ty == ty, cell.signed) {
(true, _) => read,
(false, true) => builder.unary(Opcode::SExt, read, ty),
(false, false) => builder.unary(Opcode::ZExt, read, ty),
}
}
(How::Table { .. }, None) => unreachable!("a table was planned with nowhere to put it"),
};
let mut args = plan.args.clone();
args[plan.answer] = answer;
match plan.hands {
Hands::On(block) => builder.jump(block, &args),
Hands::Back => builder.ret(&args),
};
let Extra::Switch(info) = func[plan.inst].extra else { return };
let empty = func.push_values(&[]);
let mut calls: Vec<BlockCall> = func[func[info].targets].to_vec();
for call in &mut calls[1..] {
*call = BlockCall::new(hit, empty);
}
let mut cases: Vec<Imm> = func[func[info].cases].to_vec();
for &hole in &plan.holes {
calls.push(BlockCall::new(hit, empty));
cases.push(Imm::int(hole, plan.ty));
}
let targets = func.push_block_calls(&calls);
let cases = func.push_imms(&cases);
let info = func.add_switch(rucc_ir::SwitchInfo { targets, cases });
func[plan.inst].extra = Extra::Switch(info);
let mut gone = HashSet::new();
for &arm in &plan.arms {
if gone.insert(arm) {
func.remove_block(arm);
}
}
}
#[cfg(test)]
mod tests {
use std::collections::{HashMap, HashSet};
use rucc_base::Interner;
use rucc_cost::Goal;
use rucc_ir::{Block, Builder, Func, Opcode, Signature, Type, Value};
use super::SwitchConv;
use crate::stats::Kind;
use crate::{Fuel, Pass, ReadOnly, Stats, Table};
fn i32() -> Type {
Type::int(32)
}
fn convert(func: &mut Func) -> Stats {
SwitchConv.run(func, &mut crate::machine::fixtures::analyses(), &mut Fuel::unlimited())
}
fn tabled(func: &mut Func) -> (Stats, Vec<Table>) {
tabled_for(func, Goal::Speed)
}
fn tabled_for(func: &mut Func, goal: Goal) -> (Stats, Vec<Table>) {
let mut names = Interner::new();
let taken = HashSet::new();
let mut data = ReadOnly::new(&mut names, &taken, 64, 0);
let mut an = crate::Analyses::new(crate::Machine::with(None, goal));
let stats = SwitchConv.run_emitting(func, &mut an, &mut Fuel::unlimited(), &mut data);
(stats, data.into_tables())
}
fn returning(ty: Type, labels: &[i128], answers: &[i128]) -> Func {
let mut names = Interner::new();
let mut func = Func::new(names.intern("f"), Signature::new());
let head = func.create_block();
let value = func.append_param(head, ty);
let default = func.create_block();
let arms: Vec<Block> = answers.iter().map(|_| func.create_block()).collect();
for (&arm, &answer) in arms.iter().zip(answers) {
let mut build = Builder::new(&mut func, arm);
let it = build.iconst(ty, answer);
build.ret(&[it]);
}
let mut build = Builder::new(&mut func, default);
let it = build.iconst(ty, 999);
build.ret(&[it]);
let cases: Vec<(i128, Block)> = labels.iter().copied().zip(arms.iter().copied()).collect();
Builder::new(&mut func, head).switch(value, default, &cases);
func
}
fn cases(func: &Func) -> Vec<usize> {
let head = func.entry().expect("a function with blocks in it");
let term = func.terminator(head).expect("a head block has one");
func.successors(term).skip(1).map(|call| call.block.index()).collect()
}
fn arm(func: &Func) -> Block {
let blocks = cases(func);
let first = blocks[0];
assert!(blocks.iter().all(|&block| block == first), "the case edges did not all move");
Block::from_usize(first)
}
fn opcodes(func: &Func, block: Block) -> Vec<Opcode> {
func.insts(block).map(|inst| func[inst].opcode).collect()
}
fn answer(func: &Func, block: Block, label: i128) -> i128 {
looked_up(func, block, label, &[])
}
fn looked_up(func: &Func, block: Block, label: i128, tables: &[Table]) -> i128 {
let head = func.entry().expect("a function with blocks in it");
let mut values: HashMap<Value, i128> = HashMap::new();
values.insert(func[head].params[0], label);
for inst in func.insts(block) {
let data = func[inst];
let Some(result) = data.first_result else {
let args = func[data.args].to_vec();
let handed = match data.opcode {
Opcode::Return => args[0],
Opcode::Jump => {
func[func.successors(inst).next().expect("a jump goes").args][0]
}
other => panic!("a block this pass wrote ends in {other:?}"),
};
return values[&handed];
};
let args: Vec<i128> = func[data.args].iter().map(|arg| values[arg]).collect();
let it = match data.opcode {
Opcode::IConst => {
let (imm, ty) = crate::fold::constant(func, result).expect("a constant is one");
imm.signed(ty)
}
Opcode::Mul => args[0].wrapping_mul(args[1]),
Opcode::Add => args[0].wrapping_add(args[1]),
Opcode::Sub => args[0].wrapping_sub(args[1]),
Opcode::ZExt => {
let from = func[func[data.args][0]].ty;
super::wrap(args[0], from).rem_euclid(1 << from.bits())
}
Opcode::SExt => args[0],
Opcode::GlobalAddr => 0,
Opcode::PtrAdd => args[0] + args[1],
Opcode::Load => {
assert_eq!(tables.len(), 1, "a load with no single table to read");
let table = &tables[0];
let bytes = i128::from(table.ty.bits() / 8);
assert_eq!(args[0] % bytes, 0, "a load between two cells");
let at = usize::try_from(args[0] / bytes).expect("a load before the table");
*table.cells.get(at).expect("a load after the table")
}
other => panic!("this pass does not write {other:?}"),
};
let ty = func[result].ty;
values.insert(result, if ty.is_int() { super::wrap(it, ty) } else { it });
}
panic!("a block with no terminator");
}
fn fired(stats: &Stats) -> bool {
stats.total(Kind::Optimized) > 0
}
#[test]
fn labels_that_run_with_their_answers_become_one_addition() {
let mut func = returning(i32(), &[0, 1, 2, 3], &[1, 2, 3, 4]);
assert!(fired(&convert(&mut func)));
let arm = arm(&func);
assert_eq!(opcodes(&func, arm), [Opcode::IConst, Opcode::Add, Opcode::Return]);
for label in 0..4 {
assert_eq!(answer(&func, arm, label), label + 1);
}
}
#[test]
fn answers_that_are_a_multiple_of_the_label_become_a_multiplication() {
let mut func = returning(i32(), &[3, 4, 5, 6], &[30, 40, 50, 60]);
assert!(fired(&convert(&mut func)));
let arm = arm(&func);
assert_eq!(opcodes(&func, arm), [Opcode::IConst, Opcode::Mul, Opcode::Return]);
for label in 3..7 {
assert_eq!(answer(&func, arm, label), label * 10);
}
}
#[test]
fn answers_that_are_all_the_same_become_the_constant_they_all_were() {
let mut func = returning(i32(), &[7, 8, 9, 10], &[9, 9, 9, 9]);
assert!(fired(&convert(&mut func)));
let arm = arm(&func);
assert_eq!(opcodes(&func, arm), [Opcode::IConst, Opcode::Return]);
assert_eq!(answer(&func, arm, 8), 9);
}
#[test]
fn labels_that_run_below_zero_are_a_run_like_any_other() {
let mut func = returning(i32(), &[-2, -1, 0, 1], &[-4, -2, 0, 2]);
assert!(fired(&convert(&mut func)));
let arm = arm(&func);
for label in -2..2 {
assert_eq!(answer(&func, arm, label), label * 2);
}
}
#[test]
fn a_line_that_only_holds_by_wrapping_still_holds() {
let ty = Type::int(8);
let mut func = returning(ty, &[0, 1, 2], &[0, 100, -56]);
assert!(fired(&convert(&mut func)));
let arm = arm(&func);
assert_eq!(answer(&func, arm, 2), -56);
}
#[test]
fn labels_with_a_hole_in_them_are_left_alone_where_no_table_can_be_made() {
let mut func = returning(i32(), &[0, 1, 3], &[1, 2, 4]);
assert!(!fired(&convert(&mut func)));
assert_eq!(cases(&func).len(), 3);
}
#[test]
fn answers_that_are_not_a_line_are_left_alone_where_no_table_can_be_made() {
let mut func = returning(i32(), &[0, 1, 2], &[5, 9, 2]);
assert!(!fired(&convert(&mut func)));
}
#[test]
fn two_labels_are_not_enough_to_pay_for_the_arithmetic() {
let mut func = returning(i32(), &[0, 1], &[1, 2]);
assert!(!fired(&convert(&mut func)));
}
#[test]
fn an_answer_wider_than_its_label_is_left_alone_where_no_table_can_be_made() {
let mut names = Interner::new();
let mut func = Func::new(names.intern("f"), Signature::new());
let head = func.create_block();
let value = func.append_param(head, i32());
let default = func.create_block();
let arms: Vec<Block> = (0..3).map(|_| func.create_block()).collect();
for (index, &arm) in arms.iter().enumerate() {
let mut build = Builder::new(&mut func, arm);
let it = build.iconst(Type::int(64), index as i128 + 1);
build.ret(&[it]);
}
let mut build = Builder::new(&mut func, default);
let it = build.iconst(Type::int(64), 0);
build.ret(&[it]);
let cases: Vec<(i128, Block)> = (0..3).zip(arms.iter().copied()).collect();
Builder::new(&mut func, head).switch(value, default, &cases);
assert!(!fired(&convert(&mut func)));
}
#[test]
fn an_arm_something_else_reaches_is_left_alone() {
let mut func = returning(i32(), &[0, 1, 2], &[1, 2, 3]);
let default = Block::from_usize(1);
let arm = Block::from_usize(2);
let term = func.terminator(default).expect("the default returns");
func.remove_inst(term);
Builder::new(&mut func, default).jump(arm, &[]);
assert!(!fired(&convert(&mut func)));
}
#[test]
fn an_arm_that_is_also_the_default_is_left_alone() {
let mut names = Interner::new();
let mut func = Func::new(names.intern("f"), Signature::new());
let head = func.create_block();
let value = func.append_param(head, i32());
let shared = func.create_block();
let mut build = Builder::new(&mut func, shared);
let it = build.iconst(i32(), 1);
build.ret(&[it]);
let others: Vec<Block> = (0..2).map(|_| func.create_block()).collect();
for (index, &arm) in others.iter().enumerate() {
let mut build = Builder::new(&mut func, arm);
let it = build.iconst(i32(), index as i128 + 2);
build.ret(&[it]);
}
let cases = [(0, shared), (1, others[0]), (2, others[1])];
Builder::new(&mut func, head).switch(value, shared, &cases);
assert!(!fired(&convert(&mut func)));
}
#[test]
fn arms_that_join_keep_what_they_pass_beside_the_answer() {
let mut names = Interner::new();
let mut func = Func::new(names.intern("f"), Signature::new());
let head = func.create_block();
let value = func.append_param(head, i32());
let alongside = func.append_param(head, i32());
let join = func.create_block();
let handed = func.append_param(join, i32());
let carried = func.append_param(join, i32());
Builder::new(&mut func, join).ret(&[handed, carried]);
let default = func.create_block();
let mut build = Builder::new(&mut func, default);
let it = build.iconst(i32(), 999);
build.jump(join, &[it, alongside]);
let arms: Vec<Block> = (0..3).map(|_| func.create_block()).collect();
for (index, &arm) in arms.iter().enumerate() {
let mut build = Builder::new(&mut func, arm);
let it = build.iconst(i32(), index as i128 + 1);
build.jump(join, &[it, alongside]);
}
let cases: Vec<(i128, Block)> = (0..3).zip(arms.iter().copied()).collect();
Builder::new(&mut func, head).switch(value, default, &cases);
assert!(fired(&convert(&mut func)));
let arm = arm(&func);
assert_eq!(answer(&func, arm, 2), 3);
let term = func.terminator(arm).expect("the block ends in a jump");
let call = func.successors(term).next().expect("a jump goes somewhere");
assert_eq!(func[call.args][1], alongside);
}
#[test]
fn arms_that_hand_on_two_different_things_are_left_alone() {
let mut names = Interner::new();
let mut func = Func::new(names.intern("f"), Signature::new());
let head = func.create_block();
let value = func.append_param(head, i32());
let join = func.create_block();
let first = func.append_param(join, i32());
let second = func.append_param(join, i32());
Builder::new(&mut func, join).ret(&[first, second]);
let default = func.create_block();
let mut build = Builder::new(&mut func, default);
let it = build.iconst(i32(), 999);
build.jump(join, &[it, it]);
let arms: Vec<Block> = (0..3).map(|_| func.create_block()).collect();
for (index, &arm) in arms.iter().enumerate() {
let mut build = Builder::new(&mut func, arm);
let one = build.iconst(i32(), index as i128 + 1);
let two = build.iconst(i32(), index as i128 + 10);
build.jump(join, &[one, two]);
}
let cases: Vec<(i128, Block)> = (0..3).zip(arms.iter().copied()).collect();
Builder::new(&mut func, head).switch(value, default, &cases);
assert!(!fired(&convert(&mut func)));
}
#[test]
fn an_arm_that_does_something_is_left_alone() {
let mut names = Interner::new();
let mut func = Func::new(names.intern("f"), Signature::new());
let head = func.create_block();
let value = func.append_param(head, i32());
let default = func.create_block();
let mut build = Builder::new(&mut func, default);
let it = build.iconst(i32(), 999);
build.ret(&[it]);
let arms: Vec<Block> = (0..3).map(|_| func.create_block()).collect();
for (index, &arm) in arms.iter().enumerate() {
let mut build = Builder::new(&mut func, arm);
let it = build.iconst(i32(), index as i128 + 1);
let sum = build.binary(Opcode::Add, it, value, rucc_ir::Flags::NONE);
build.ret(&[sum]);
}
let cases: Vec<(i128, Block)> = (0..3).zip(arms.iter().copied()).collect();
Builder::new(&mut func, head).switch(value, default, &cases);
assert!(!fired(&convert(&mut func)));
}
#[test]
fn the_default_goes_where_it_went() {
let mut func = returning(i32(), &[0, 1, 2, 3], &[1, 2, 3, 4]);
let head = func.entry().expect("a function with blocks in it");
let before = func.terminator(head).expect("a head block has one");
let was = func.successors(before).next().expect("a switch has a default").block;
assert!(fired(&convert(&mut func)));
let after = func.terminator(head).expect("a head block has one");
let now = func.successors(after).next().expect("a switch has a default").block;
assert_eq!(was, now, "the default moved");
}
const LOOKUP: [Opcode; 6] = [
Opcode::ZExt,
Opcode::IConst,
Opcode::Mul,
Opcode::GlobalAddr,
Opcode::PtrAdd,
Opcode::Load,
];
#[test]
fn answers_that_are_not_a_line_are_one_load_from_a_table() {
let mut func = returning(i32(), &[0, 1, 2, 3], &[5, 9, 2, 7]);
let (stats, tables) = tabled(&mut func);
assert!(fired(&stats));
assert_eq!(tables.len(), 1);
assert_eq!(tables[0].ty, i32());
assert_eq!(tables[0].cells, [5, 9, 2, 7]);
let arm = arm(&func);
let mut want = LOOKUP.to_vec();
want.push(Opcode::Return);
assert_eq!(opcodes(&func, arm), want);
for (label, answer) in [(0, 5), (1, 9), (2, 2), (3, 7)] {
assert_eq!(looked_up(&func, arm, label, &tables), answer);
}
}
#[test]
fn a_hole_is_filled_with_what_a_default_that_only_answers_gives() {
let mut func = returning(i32(), &[1, 2, 4, 5], &[10, 20, 40, 55]);
let head = func.entry().expect("a function with blocks in it");
let before = func.terminator(head).expect("a head block has one");
let default = func.successors(before).next().expect("a switch has a default").block;
let (stats, tables) = tabled(&mut func);
assert!(fired(&stats));
assert_eq!(tables[0].cells, [10, 20, 999, 40, 55]);
assert_eq!(cases(&func).len(), 5, "the hole was not given a case");
let after = func.terminator(head).expect("a head block has one");
assert_eq!(func.successors(after).next().map(|call| call.block), Some(default));
let arm = arm(&func);
for (label, answer) in [(1, 10), (2, 20), (3, 999), (4, 40), (5, 55)] {
assert_eq!(looked_up(&func, arm, label, &tables), answer);
}
}
#[test]
fn a_hole_still_goes_to_a_default_that_does_more_than_answer() {
let mut func = returning(i32(), &[1, 2, 4, 5], &[10, 20, 40, 55]);
let head = func.entry().expect("a function with blocks in it");
let before = func.terminator(head).expect("a head block has one");
let default = func.successors(before).next().expect("a switch has a default").block;
let label = func[func[before].args][0];
let ret = func.terminator(default).expect("the default returns");
func.remove_inst(ret);
Builder::new(&mut func, default).ret(&[label]);
let (stats, tables) = tabled(&mut func);
assert!(fired(&stats));
assert_eq!(tables[0].cells, [10, 20, 0, 40, 55]);
assert_eq!(cases(&func).len(), 4, "a hole was given a case");
let after = func.terminator(head).expect("a head block has one");
assert_eq!(func.successors(after).next().map(|call| call.block), Some(default));
let arm = arm(&func);
for (label, answer) in [(1, 10), (2, 20), (4, 40), (5, 55)] {
assert_eq!(looked_up(&func, arm, label, &tables), answer);
}
}
#[test]
fn labels_below_zero_index_from_the_lowest_of_them() {
let ty = Type::int(8);
let labels = [-128, -3, -1, 0, 2, 127];
let answers = [7, -5, 11, 3, -100, 42];
let mut func = returning(ty, &labels, &answers);
let (stats, _) = tabled(&mut func);
assert!(!fired(&stats), "a table of mostly holes was made");
let labels = [-3, -2, -1, 0, 2];
let answers = [7, -5, 11, 3, -100];
let mut func = returning(ty, &labels, &answers);
let (stats, tables) = tabled(&mut func);
assert!(fired(&stats));
assert_eq!(tables[0].cells, [7, -5, 11, 3, -25, -100]);
let arm = arm(&func);
assert_eq!(opcodes(&func, arm)[..2], [Opcode::IConst, Opcode::Sub]);
for (&label, &answer) in labels.iter().zip(&answers).chain([(&1, &-25)]) {
assert_eq!(looked_up(&func, arm, label, &tables), answer);
}
}
#[test]
fn an_answer_wider_than_its_label_is_a_table_of_the_wider_type() {
let mut names = Interner::new();
let answers = [1i128 << 40, 3, -1, 1 << 33];
let mut func = Func::new(names.intern("f"), Signature::new());
let head = func.create_block();
let value = func.append_param(head, i32());
let default = func.create_block();
let arms: Vec<Block> = answers.iter().map(|_| func.create_block()).collect();
for (&arm, &answer) in arms.iter().zip(&answers) {
let mut build = Builder::new(&mut func, arm);
let it = build.iconst(Type::int(64), answer);
build.ret(&[it]);
}
let mut build = Builder::new(&mut func, default);
let it = build.iconst(Type::int(64), 0);
build.ret(&[it]);
let cases: Vec<(i128, Block)> = (10..14).zip(arms.iter().copied()).collect();
Builder::new(&mut func, head).switch(value, default, &cases);
let (stats, tables) = tabled(&mut func);
assert!(fired(&stats));
assert_eq!(tables[0].ty, Type::int(64));
let arm = arm(&func);
for (label, &answer) in (10..14).zip(&answers) {
assert_eq!(looked_up(&func, arm, label, &tables), answer);
}
}
#[test]
fn labels_too_far_apart_for_a_table_are_left_alone() {
let mut func = returning(i32(), &[0, 100, 200], &[1, 5, 3]);
let (stats, tables) = tabled(&mut func);
assert!(!fired(&stats));
assert!(tables.is_empty());
}
#[test]
fn a_line_is_still_arithmetic_where_a_table_could_be_made() {
let mut func = returning(i32(), &[0, 1, 2, 3], &[1, 2, 3, 4]);
let (stats, tables) = tabled(&mut func);
assert!(fired(&stats));
assert!(tables.is_empty(), "a table was made for a line");
}
#[test]
fn a_label_wider_than_a_word_gets_no_table() {
let mut func = returning(Type::int(128), &[0, 1, 2, 3], &[5, 9, 2, 7]);
let (stats, tables) = tabled(&mut func);
assert!(!fired(&stats));
assert!(tables.is_empty());
}
#[test]
fn the_answer_is_read_from_the_place_the_arms_disagree_about() {
let mut names = Interner::new();
let mut func = Func::new(names.intern("f"), Signature::new());
let head = func.create_block();
let value = func.append_param(head, i32());
let join = func.create_block();
let first = func.append_param(join, i32());
let second = func.append_param(join, i32());
Builder::new(&mut func, join).ret(&[second, first]);
let default = func.create_block();
let arms: Vec<Block> = (0..3).map(|_| func.create_block()).collect();
let mut build = Builder::new(&mut func, head);
let one = build.iconst(i32(), 1);
let cases: Vec<(i128, Block)> = (0..3).zip(arms.iter().copied()).collect();
build.switch(value, default, &cases);
let mut build = Builder::new(&mut func, default);
let it = build.iconst(i32(), 999);
build.jump(join, &[one, it]);
for (&arm, answer) in arms.iter().zip([10, 2, 3]) {
let mut build = Builder::new(&mut func, arm);
let it = build.iconst(i32(), answer);
build.jump(join, &[one, it]);
}
assert!(!fired(&convert(&mut func)), "ten, two and three were taken for a line");
let (stats, tables) = tabled(&mut func);
assert!(fired(&stats));
assert_eq!(tables[0].cells, [10, 2, 3]);
}
#[test]
fn a_table_for_size_has_cells_as_narrow_as_its_answers() {
let mut func = returning(i32(), &[0, 1, 2, 3], &[5, -9, 2, 7]);
let (stats, tables) = tabled_for(&mut func, Goal::Size);
assert!(fired(&stats));
assert_eq!(tables[0].ty, Type::int(8));
let at = arm(&func);
assert!(opcodes(&func, at).contains(&Opcode::SExt));
for (label, answer) in [(0, 5), (1, -9), (2, 2), (3, 7)] {
assert_eq!(looked_up(&func, at, label, &tables), answer);
}
let mut func = returning(i32(), &[0, 1, 2, 3], &[5, 200, 2, 255]);
let (_, tables) = tabled_for(&mut func, Goal::Size);
assert_eq!(tables[0].ty, Type::int(8));
let at = arm(&func);
assert!(opcodes(&func, at).contains(&Opcode::ZExt));
for (label, answer) in [(0, 5), (1, 200), (2, 2), (3, 255)] {
assert_eq!(looked_up(&func, at, label, &tables), answer);
}
let mut func = returning(i32(), &[0, 1, 2, 3], &[5, -300, 2, 40000]);
let (_, tables) = tabled_for(&mut func, Goal::Size);
assert_eq!(tables[0].ty, i32(), "a cell narrower than an answer that needs all of it");
let mut func = returning(i32(), &[0, 1, 2, 3], &[5, -9, 2, 7]);
let (_, tables) = tabled_for(&mut func, Goal::Speed);
assert_eq!(tables[0].ty, i32());
}
}