use rucc_diag::Span;
use rucc_ir::{
Block, BlockCall, Builder, Extra, Flags, Func, Imm, Inst, IntPred, Opcode, Type, Value,
};
pub const LINEAR: usize = 32;
pub fn switches(func: &mut Func) {
let found: Vec<Inst> = func
.blocks()
.filter_map(|block| func.terminator(block))
.filter(|&inst| func[inst].opcode == Opcode::Switch)
.collect();
for inst in found {
lower(func, inst);
}
}
fn lower(func: &mut Func, inst: Inst) {
let block = func.block_of(inst).expect("a terminator is in a block");
let span = func.span(inst);
let Extra::Switch(info) = func[inst].extra else { return };
let info = func[info];
let Some(&value) = func[func[inst].args].first() else { return };
let ty = func[value].ty.lane();
let calls: Vec<BlockCall> = func[info.targets].to_vec();
let cases: Vec<Imm> = func[info.cases].to_vec();
let Some((&default, arms)) = calls.split_first() else { return };
let clusters = group(func, clusters(func, &cases, arms, ty));
func.remove_inst(inst);
tree(func, &Lowering { value, ty, default, span }, block, &clusters);
}
struct Lowering {
value: Value,
ty: Type,
default: BlockCall,
span: Span,
}
#[derive(Clone, Debug)]
enum Cluster {
One {
value: i128,
call: BlockCall,
},
Run {
low: i128,
high: i128,
call: BlockCall,
},
Bits {
low: i128,
high: i128,
arms: Vec<(u64, BlockCall)>,
},
}
impl Cluster {
fn low(&self) -> i128 {
match *self {
Self::One { value, .. } => value,
Self::Run { low, .. } | Self::Bits { low, .. } => low,
}
}
fn high(&self) -> i128 {
match *self {
Self::One { value, .. } => value,
Self::Run { high, .. } | Self::Bits { high, .. } => high,
}
}
fn goes_to(&self, func: &Func, call: BlockCall) -> bool {
match *self {
Self::One { call: mine, .. } | Self::Run { call: mine, .. } => same(func, mine, call),
Self::Bits { .. } => false,
}
}
fn grow(&mut self, value: i128) {
let call = match *self {
Self::One { call, .. } | Self::Run { call, .. } => call,
Self::Bits { .. } => unreachable!("a bit test is never grown into a run"),
};
*self = Self::Run { low: self.low(), high: value, call };
}
}
const WORD: i128 = 64;
const MARGIN: usize = 3;
fn clusters(func: &Func, cases: &[Imm], arms: &[BlockCall], ty: Type) -> Vec<Cluster> {
let mut sorted: Vec<(i128, BlockCall)> =
cases.iter().zip(arms).map(|(&imm, &call)| (imm.signed(ty), call)).collect();
sorted.sort_by_key(|&(value, _)| value);
assert!(
sorted.windows(2).all(|pair| pair[0].0 != pair[1].0),
"a switch with two cases of the same value reached the back end"
);
let mut clusters: Vec<Cluster> = Vec::with_capacity(sorted.len());
for (value, call) in sorted {
match clusters.last_mut() {
Some(last) if last.high() + 1 == value && last.goes_to(func, call) => {
last.grow(value);
}
_ => clusters.push(Cluster::One { value, call }),
}
}
clusters
}
fn same(func: &Func, a: BlockCall, b: BlockCall) -> bool {
a.block == b.block && func[a.args] == func[b.args]
}
fn group(func: &Func, clusters: Vec<Cluster>) -> Vec<Cluster> {
let mut out: Vec<Cluster> = Vec::with_capacity(clusters.len());
let mut at = 0;
while at < clusters.len() {
let reach = reach(&clusters, at);
match bits(func, &clusters[at..at + reach]) {
Some(cluster) => {
out.push(cluster);
at += reach;
}
None => {
out.push(clusters[at].clone());
at += 1;
}
}
}
out
}
fn reach(clusters: &[Cluster], at: usize) -> usize {
let Cluster::One { value: first, .. } = clusters[at] else { return 0 };
let mut reach = 0;
while let Some(Cluster::One { value, .. }) = clusters.get(at + reach) {
if value - first >= WORD {
break;
}
reach += 1;
}
reach
}
fn bits(func: &Func, group: &[Cluster]) -> Option<Cluster> {
let low = group.first()?.low();
let mut arms: Vec<(u64, BlockCall)> = Vec::new();
for cluster in group {
let Cluster::One { value, call } = *cluster else { return None };
let bit = 1u64 << (value - low);
match arms.iter_mut().find(|&&mut (_, mine)| same(func, mine, call)) {
Some((mask, _)) => *mask |= bit,
None => arms.push((bit, call)),
}
}
if group.len() < arms.len() + MARGIN {
return None;
}
Some(Cluster::Bits { low, high: group.last()?.high(), arms })
}
fn tree(func: &mut Func, of: &Lowering, at: Block, clusters: &[Cluster]) {
if clusters.len() <= LINEAR {
chain(func, of, at, clusters);
return;
}
let (below, above) = clusters.split_at(clusters.len() / 2);
let pivot = above[0].low();
let left = func.create_block();
let right = func.create_block();
let mut build = Builder::new(func, at).at(of.span);
let want = build.iconst(of.ty, pivot);
let under = build.icmp(IntPred::Slt, of.value, want);
build.br_if(under, left, &[], right, &[]);
tree(func, of, left, below);
tree(func, of, right, above);
}
fn chain(func: &mut Func, of: &Lowering, at: Block, clusters: &[Cluster]) {
let Some((last, rest)) = clusters.split_last() else {
let args: Vec<Value> = func[of.default.args].to_vec();
Builder::new(func, at).at(of.span).jump(of.default.block, &args);
return;
};
let mut at = at;
for cluster in rest {
let next = func.create_block();
test(func, of, at, cluster, next, &[]);
at = next;
}
let onward: Vec<Value> = func[of.default.args].to_vec();
test(func, of, at, last, of.default.block, &onward);
}
fn test(
func: &mut Func,
of: &Lowering,
at: Block,
cluster: &Cluster,
next: Block,
onward: &[Value],
) {
if matches!(cluster, Cluster::Bits { .. }) {
scattered(func, of, at, cluster, next, onward);
return;
}
let call = match *cluster {
Cluster::One { call, .. } | Cluster::Run { call, .. } => call,
Cluster::Bits { .. } => unreachable!("a bit test was dealt with above"),
};
let taken: Vec<Value> = func[call.args].to_vec();
let mut build = Builder::new(func, at).at(of.span);
let matched = match *cluster {
Cluster::One { value, .. } => {
let want = build.iconst(of.ty, value);
build.icmp(IntPred::Eq, of.value, want)
}
Cluster::Run { low, high, .. } => {
let base = shifted_down(&mut build, of, low);
let width = build.iconst(of.ty, high - low);
build.icmp(IntPred::Ule, base, width)
}
Cluster::Bits { .. } => unreachable!("a bit test was dealt with above"),
};
build.br_if(matched, call.block, &taken, next, onward);
}
fn shifted_down(build: &mut Builder<'_>, of: &Lowering, low: i128) -> Value {
if low == 0 {
return of.value;
}
let start = build.iconst(of.ty, low);
build.binary(Opcode::Sub, of.value, start, Flags::default())
}
fn scattered(
func: &mut Func,
of: &Lowering,
at: Block,
cluster: &Cluster,
next: Block,
onward: &[Value],
) {
let Cluster::Bits { low, high, arms } = cluster else {
unreachable!("only a bit test is written as one");
};
let (low, high) = (*low, *high);
let all = arms.iter().fold(0u64, |seen, &(mask, _)| seen | mask);
let covered = arms.len() > 1 && all == span_mask(low, high);
let tests = arms.len() - usize::from(covered);
let (spare, onto_spare) = if covered {
let call = arms[arms.len() - 1].1;
(call.block, func[call.args].to_vec())
} else {
(of.default.block, func[of.default.args].to_vec())
};
let inside = func.create_block();
let mut blocks: Vec<Block> = vec![inside];
blocks.extend((1..tests).map(|_| func.create_block()));
let taken: Vec<Vec<Value>> = arms.iter().map(|&(_, call)| func[call.args].to_vec()).collect();
let mut build = Builder::new(func, at).at(of.span);
let base = shifted_down(&mut build, of, low);
let width = build.iconst(of.ty, high - low);
let ok = build.icmp(IntPred::Ule, base, width);
build.br_if(ok, inside, &[], next, onward);
let word = Type::int(u64::BITS);
let mut build = Builder::new(func, inside).at(of.span);
let amount = if of.ty == word { base } else { build.unary(Opcode::ZExt, base, word) };
let one = build.iconst(word, 1);
let bit = build.binary(Opcode::Shl, one, amount, Flags::default());
for (index, &(mask, call)) in arms[..tests].iter().enumerate() {
let want = build.iconst(word, i128::from(mask as i64));
let hit = build.binary(Opcode::And, bit, want, Flags::default());
let none = build.iconst(word, 0);
let matched = build.icmp(IntPred::Ne, hit, none);
let last = index + 1 == tests;
let onto = if last { spare } else { blocks[index + 1] };
let args = if last { &onto_spare[..] } else { &[][..] };
build.br_if(matched, call.block, &taken[index], onto, args);
if !last {
build = Builder::new(func, blocks[index + 1]).at(of.span);
}
}
}
fn span_mask(low: i128, high: i128) -> u64 {
let width = u32::try_from(high - low).expect("a group narrower than a word");
if width + 1 >= u64::BITS { u64::MAX } else { (1u64 << (width + 1)) - 1 }
}
#[must_use]
pub fn blocks_for(clusters: usize) -> usize {
clusters.saturating_sub(1)
}
#[cfg(test)]
mod tests {
use std::collections::HashMap;
use rucc_base::Interner;
use rucc_ir::{
Block, BlockCall, Builder, Extra, Func, Imm, InstData, IntPred, Module, Opcode, Signature,
SwitchInfo, Type, Value,
};
use rucc_target::{Arch, Env, Os, TargetInfo, Triple};
use super::{LINEAR, blocks_for, switches};
fn target() -> TargetInfo {
TargetInfo::new(Triple::new(Arch::X86_64, Os::Linux, Env::Gnu))
}
struct Built {
names: Interner,
func: Func,
operand: Value,
arms: Vec<Block>,
default: Block,
}
fn built(cases: &[i128]) -> Built {
let arms: Vec<usize> = (0..cases.len()).collect();
built_sharing(cases, &arms, Type::int(32))
}
fn built_sharing(cases: &[i128], arms: &[usize], ty: Type) -> Built {
let mut names = Interner::new();
let int = Type::int(32);
let mut func =
Func::new(names.intern("sw"), Signature::new().with_params(&[ty]).with_returns(&[int]));
let entry = func.create_block();
let x = func.append_param(entry, ty);
let default = func.create_block();
let count = arms.iter().copied().max().map_or(0, |top| top + 1);
let blocks: Vec<Block> = (0..count).map(|_| func.create_block()).collect();
let table: Vec<(i128, Block)> =
cases.iter().copied().zip(arms.iter().map(|&at| blocks[at])).collect();
Builder::new(&mut func, entry).switch(x, default, &table);
for (index, &arm) in blocks.iter().enumerate() {
let mut build = Builder::new(&mut func, arm);
let what = i128::try_from(index).expect("a small number of arms");
let v = build.iconst(int, (what + 1) * 10);
build.ret(&[v]);
}
let mut build = Builder::new(&mut func, default);
let v = build.iconst(int, 0);
build.ret(&[v]);
Built { names, func, operand: x, arms: blocks, default }
}
fn count(func: &Func) -> usize {
func.blocks().count()
}
fn printed(func: &Func, names: &mut Interner) -> String {
let module = Module::new(names.intern("sw.c"), &target());
rucc_ir::print_func(&module, func, names)
}
fn verified(built: &mut Built) {
let module = Module::new(built.names.intern("sw.c"), &target());
rucc_ir::verify_func(&module, &built.func, &built.names)
.expect("the rewrite builds valid IR");
}
fn arrives(func: &Func, operand: Value, x: i128, ty: Type) -> Block {
let mut at = func.entry().expect("an entry block");
let mut held: HashMap<Value, i128> = HashMap::new();
held.insert(operand, Imm::int(x, ty).signed(ty));
loop {
let mut moved = None;
for inst in func.insts(at).collect::<Vec<_>>() {
let opcode = func[inst].opcode;
let extra = func[inst].extra;
let result = func[inst].first_result;
let args: Vec<i128> = func[func[inst].args]
.iter()
.map(|value| held.get(value).copied().unwrap_or(0))
.collect();
let wide = |value: Option<Value>| func[value.expect("a result")].ty;
let mut put = |value: Option<Value>, what: i128| {
let value = value.expect("a result");
let ty = func[value].ty;
held.insert(value, Imm::int(what, ty).signed(ty));
};
match opcode {
Opcode::IConst => {
let Extra::Imm(imm) = extra else { return at };
put(result, func[imm].signed(wide(result)));
}
Opcode::Sub => put(result, args[0] - args[1]),
Opcode::And => put(result, args[0] & args[1]),
Opcode::Shl => put(result, args[0] << args[1]),
Opcode::ZExt => {
let from = func[func[func[inst].args][0]].ty;
let raw = Imm::int(args[0], from).unsigned();
put(result, i128::try_from(raw).expect("a value narrower than a word"));
}
Opcode::ICmp => {
let Extra::IntPred(pred) = extra else { return at };
let of = func[func[func[inst].args][0]].ty;
let unsigned = |v: i128| Imm::int(v, of).unsigned();
let answer = match pred {
IntPred::Eq => args[0] == args[1],
IntPred::Ne => args[0] != args[1],
IntPred::Slt => args[0] < args[1],
IntPred::Ule => unsigned(args[0]) <= unsigned(args[1]),
other => panic!("the lowering does not write {}", other.name()),
};
held.insert(result.expect("a comparison has a result"), i128::from(answer));
}
Opcode::Jump => {
let call = func.successors(inst).next().expect("a jump has a target");
moved = Some(call.block);
}
Opcode::BrIf => {
let mut targets = func.successors(inst);
let taken = targets.next().expect("a branch has two targets");
let other = targets.next().expect("a branch has two targets");
moved = Some(if args[0] != 0 { taken.block } else { other.block });
}
_ => return at,
}
}
match moved {
Some(next) => at = next,
None => return at,
}
}
}
fn routes(built: &mut Built, cases: &[i128], arms: &[usize], probes: &[i128], ty: Type) {
switches(&mut built.func);
verified(built);
for &x in probes {
let wanted = cases
.iter()
.position(|&case| case == x)
.map_or(built.default, |at| built.arms[arms[at]]);
let got = arrives(&built.func, built.operand, x, ty);
assert_eq!(got, wanted, "the operand {x} went to the wrong block");
}
}
fn around(cases: &[i128], ty: Type) -> Vec<i128> {
let mut probes: Vec<i128> = Vec::new();
for &case in cases {
probes.extend([case - 1, case, case + 1]);
}
let bits = ty.bits();
probes.extend([0, -1, 1, i128::from(i32::MIN) >> (32 - bits), (1 << (bits - 1)) - 1]);
probes.retain(|&x| Imm::int(x, ty).signed(ty) == x);
probes.sort_unstable();
probes.dedup();
probes
}
#[test]
fn a_small_switch_is_a_compare_and_a_branch_for_each_case() {
let mut built = built(&[1, 2]);
let before = count(&built.func);
switches(&mut built.func);
assert_eq!(count(&built.func), before + blocks_for(2));
let text = printed(&built.func, &mut built.names);
assert!(!text.contains("switch"), "the switch is gone: {text}");
assert_eq!(text.matches("icmp eq").count(), 2, "one compare per case: {text}");
assert_eq!(text.matches("br_if").count(), 2, "one branch per case: {text}");
}
#[test]
fn the_last_case_falls_to_the_default_rather_than_to_a_block_of_its_own() {
let mut built = built(&[7]);
let before = count(&built.func);
switches(&mut built.func);
assert_eq!(count(&built.func), before);
assert_eq!(blocks_for(1), 0);
}
#[test]
fn a_switch_with_only_a_default_is_a_jump() {
let mut built = built(&[]);
switches(&mut built.func);
let entry = built.func.entry().expect("an entry block");
let term = built.func.terminator(entry).expect("a terminator");
assert_eq!(built.func[term].opcode, Opcode::Jump);
}
#[test]
fn what_comes_out_is_valid_ir() {
let mut built = built(&[1, 2, 3, 4]);
switches(&mut built.func);
verified(&mut built);
}
#[test]
fn a_function_with_no_switch_is_left_exactly_as_it_was() {
let mut names = Interner::new();
let int = Type::int(32);
let mut func =
Func::new(names.intern("f"), Signature::new().with_params(&[int]).with_returns(&[int]));
let entry = func.create_block();
let x = func.append_param(entry, int);
Builder::new(&mut func, entry).ret(&[x]);
let before = printed(&func, &mut names);
switches(&mut func);
assert_eq!(printed(&func, &mut names), before);
}
#[test]
fn a_run_of_cases_going_to_one_place_is_one_range_test() {
let cases = [3, 4, 5, 6, 7, 8, 9, 10];
let arms = [0; 8];
let mut built = built_sharing(&cases, &arms, Type::int(32));
switches(&mut built.func);
let text = printed(&built.func, &mut built.names);
assert_eq!(text.matches("icmp").count(), 1, "eight cases, one test: {text}");
assert_eq!(text.matches("icmp ule").count(), 1, "and the test is the range: {text}");
assert_eq!(text.matches("sub").count(), 1, "one subtraction to bring it to zero: {text}");
}
#[test]
fn a_run_that_starts_at_zero_needs_no_subtraction() {
let cases = [0, 1, 2, 3, 4];
let arms = [0; 5];
let mut built = built_sharing(&cases, &arms, Type::int(32));
switches(&mut built.func);
let text = printed(&built.func, &mut built.names);
assert_eq!(text.matches("icmp ule").count(), 1, "one range test: {text}");
assert!(!text.contains("sub"), "nothing to subtract from zero: {text}");
}
#[test]
fn the_tree_is_built_over_the_clusters_and_not_over_the_cases() {
let cases: Vec<i128> = (0..30).collect();
let arms: Vec<usize> = (0..30).map(|at: usize| at / 10).collect();
let mut built = built_sharing(&cases, &arms, Type::int(32));
switches(&mut built.func);
let text = printed(&built.func, &mut built.names);
assert_eq!(text.matches("icmp").count(), 3, "three runs, three tests: {text}");
assert!(!text.contains("icmp slt"), "three clusters is under the leaf size: {text}");
}
#[test]
fn a_long_sparse_switch_is_a_search_rather_than_a_walk() {
let count = 4 * LINEAR as i128;
let cases: Vec<i128> = (0..count).map(|at| at * 7).collect();
let mut built = built(&cases);
switches(&mut built.func);
let worst = deepest(&built.func);
assert!(worst <= LINEAR + 2, "{count} cases in {worst} comparisons at worst");
assert!(worst > LINEAR, "and the splits are being counted too");
}
fn deepest(func: &Func) -> usize {
fn walk(func: &Func, at: Block, seen: &mut HashMap<Block, usize>) -> usize {
if let Some(&known) = seen.get(&at) {
return known;
}
let here = func.insts(at).filter(|&inst| func[inst].opcode == Opcode::ICmp).count();
let term = func.terminator(at).expect("a terminator");
let onward: Vec<Block> = match func[term].opcode {
Opcode::Jump | Opcode::BrIf => {
func.successors(term).map(|call| call.block).collect()
}
_ => Vec::new(),
};
let below =
onward.into_iter().map(|block| walk(func, block, seen)).max().unwrap_or_default();
seen.insert(at, here + below);
here + below
}
walk(func, func.entry().expect("an entry block"), &mut HashMap::new())
}
#[test]
fn every_value_reaches_the_arm_its_case_named_in_a_small_switch() {
let cases = [1, 2, 3];
let arms = [0, 1, 2];
let ty = Type::int(32);
let mut built = built(&cases);
routes(&mut built, &cases, &arms, &around(&cases, ty), ty);
}
#[test]
fn every_value_reaches_the_arm_its_case_named_in_a_search() {
let count = 3 * LINEAR;
let cases: Vec<i128> = (0..count as i128).map(|at| at * 7).collect();
let arms: Vec<usize> = (0..count).collect();
let ty = Type::int(32);
let mut built = built(&cases);
routes(&mut built, &cases, &arms, &around(&cases, ty), ty);
}
#[test]
fn every_value_reaches_its_arm_when_the_cases_straddle_zero() {
let half = LINEAR as i128;
let cases: Vec<i128> = (-half..half).map(|at| at * 3).collect();
let arms: Vec<usize> = (0..2 * LINEAR).collect();
let ty = Type::int(32);
let mut built = built(&cases);
routes(&mut built, &cases, &arms, &around(&cases, ty), ty);
}
#[test]
fn every_value_reaches_its_arm_when_runs_and_singles_are_mixed() {
let cases: Vec<i128> =
vec![-9, -8, -7, -6, 0, 5, 6, 7, 8, 9, 10, 40, 41, 90, 91, 92, 93, 94, 95, 200];
let arms: Vec<usize> = vec![0, 0, 0, 0, 1, 2, 2, 2, 2, 2, 2, 3, 4, 5, 5, 5, 5, 5, 5, 6];
let ty = Type::int(32);
let mut built = built_sharing(&cases, &arms, ty);
routes(&mut built, &cases, &arms, &around(&cases, ty), ty);
}
#[test]
fn a_run_covering_the_whole_type_matches_everything() {
let cases: Vec<i128> = (-128..128).collect();
let arms = vec![0; cases.len()];
let ty = Type::int(8);
let mut built = built_sharing(&cases, &arms, ty);
switches(&mut built.func);
verified(&mut built);
let text = printed(&built.func, &mut built.names);
assert_eq!(text.matches("icmp").count(), 1, "one run, one test: {text}");
let entry = built.func.entry().expect("an entry block");
let operand = built.func[entry].params[0];
for x in [-128, -1, 0, 1, 127] {
assert_eq!(
arrives(&built.func, operand, x, ty),
built.arms[0],
"every value of the type is in the run"
);
}
}
#[test]
#[should_panic(expected = "two cases of the same value")]
fn a_case_value_written_twice_stops_the_compiler() {
let cases = [4, 9, 4];
let arms = [0, 1, 2];
let mut built = built_sharing(&cases, &arms, Type::int(32));
switches(&mut built.func);
}
#[test]
fn cases_that_share_a_block_but_not_its_arguments_are_not_a_run() {
let mut names = Interner::new();
let int = Type::int(32);
let mut func = Func::new(
names.intern("sw"),
Signature::new().with_params(&[int]).with_returns(&[int]),
);
let entry = func.create_block();
let x = func.append_param(entry, int);
let default = func.create_block();
let join = func.create_block();
let param = func.append_param(join, int);
let mut build = Builder::new(&mut func, entry);
let ten = build.iconst(int, 10);
let twenty = build.iconst(int, 20);
let none = func.push_values(&[]);
let first = func.push_values(&[ten]);
let second = func.push_values(&[twenty]);
let targets = func.push_block_calls(&[
BlockCall { block: default, args: none },
BlockCall { block: join, args: first },
BlockCall { block: join, args: second },
]);
let cases = func.push_imms(&[Imm::int(1, int), Imm::int(2, int)]);
let info = func.add_switch(SwitchInfo { targets, cases });
let args = func.push_values(&[x]);
let data = InstData { args, extra: Extra::Switch(info), ..InstData::new(Opcode::Switch) };
Builder::new(&mut func, entry).inst(data, &[]);
let mut build = Builder::new(&mut func, join);
build.ret(&[param]);
let mut build = Builder::new(&mut func, default);
let zero = build.iconst(int, 0);
build.ret(&[zero]);
switches(&mut func);
let text = printed(&func, &mut names);
assert_eq!(text.matches("icmp eq").count(), 2, "two cases, two equality tests: {text}");
assert!(!text.contains("icmp ule"), "and no range test over them: {text}");
}
#[test]
fn the_leaf_size_is_where_the_search_starts() {
let flat: Vec<i128> = (0..LINEAR as i128).map(|at| at * 5).collect();
let mut walked = built(&flat);
switches(&mut walked.func);
assert!(
!printed(&walked.func, &mut walked.names).contains("icmp slt"),
"a leaf's worth of clusters is still a chain"
);
let one_more: Vec<i128> = (0..LINEAR as i128 + 1).map(|at| at * 5).collect();
let mut split = built(&one_more);
switches(&mut split.func);
assert!(
printed(&split.func, &mut split.names).contains("icmp slt"),
"one more than a leaf splits"
);
}
#[test]
fn scattered_cases_sharing_one_arm_are_one_mask_and_one_test() {
let cases = [97, 101, 105, 111, 117];
let arms = [0; 5];
let mut built = built_sharing(&cases, &arms, Type::int(32));
switches(&mut built.func);
let text = printed(&built.func, &mut built.names);
assert!(!text.contains("icmp eq"), "no case is compared on its own: {text}");
assert_eq!(text.matches("shl").count(), 1, "one bit is picked out: {text}");
assert_eq!(text.matches("and").count(), 1, "and one mask is asked about it: {text}");
assert_eq!(text.matches("icmp ule").count(), 1, "one bound before the shift: {text}");
assert_eq!(text.matches("icmp ne").count(), 1, "one test for the one arm: {text}");
}
#[test]
fn every_value_reaches_its_arm_through_a_bit_test() {
let ty = Type::int(32);
let cases = [97, 101, 105, 111, 117];
let arms = [0; 5];
let mut built = built_sharing(&cases, &arms, ty);
routes(&mut built, &cases, &arms, &around(&cases, ty), ty);
}
#[test]
fn a_bit_test_carries_several_destinations_in_one_word() {
let ty = Type::int(32);
let cases: Vec<i128> = (0..9).map(|at| at * 3).collect();
let arms: Vec<usize> = (0..9).map(|at: usize| at % 3).collect();
let mut built = built_sharing(&cases, &arms, ty);
switches(&mut built.func);
let text = printed(&built.func, &mut built.names);
assert_eq!(text.matches("shl").count(), 1, "nine cases, one shift: {text}");
assert_eq!(text.matches("icmp ne").count(), 3, "three arms, three masks: {text}");
assert!(!text.contains("icmp eq"), "and no case compared on its own: {text}");
let mut built = built_sharing(&cases, &arms, ty);
routes(&mut built, &cases, &arms, &around(&cases, ty), ty);
}
#[test]
fn the_last_destination_needs_no_test_when_the_masks_cover_the_span() {
let ty = Type::int(32);
let cases: Vec<i128> = (0..6).collect();
let arms: Vec<usize> = (0..6).map(|at: usize| at % 2).collect();
let mut built = built_sharing(&cases, &arms, ty);
switches(&mut built.func);
let text = printed(&built.func, &mut built.names);
assert_eq!(text.matches("icmp ne").count(), 1, "two arms, one mask asked about: {text}");
let mut built = built_sharing(&cases, &arms, ty);
routes(&mut built, &cases, &arms, &around(&cases, ty), ty);
}
#[test]
fn a_group_that_does_not_pay_for_itself_stays_a_chain() {
let cases = [0, 3, 6];
let arms = [0, 1, 2];
let mut built = built_sharing(&cases, &arms, Type::int(32));
switches(&mut built.func);
let text = printed(&built.func, &mut built.names);
assert!(!text.contains("shl"), "three values and three arms buys nothing: {text}");
assert_eq!(text.matches("icmp eq").count(), 3, "so it stays a walk: {text}");
}
#[test]
fn a_bit_test_never_spans_more_than_a_word() {
let ty = Type::int(32);
let cases = [0, 2, 4, 6, 64];
let arms = [0; 5];
let mut built = built_sharing(&cases, &arms, ty);
switches(&mut built.func);
let text = printed(&built.func, &mut built.names);
assert_eq!(text.matches("shl").count(), 1, "one group, not two: {text}");
assert_eq!(text.matches("icmp eq").count(), 1, "and the value past it is compared: {text}");
let mut built = built_sharing(&cases, &arms, ty);
routes(&mut built, &cases, &arms, &around(&cases, ty), ty);
}
#[test]
fn a_bit_test_reaches_the_top_of_its_word() {
let ty = Type::int(32);
let cases = [0, 2, 4, 63];
let arms = [0; 4];
let mut built = built_sharing(&cases, &arms, ty);
routes(&mut built, &cases, &arms, &around(&cases, ty), ty);
}
#[test]
fn a_run_is_left_alone_rather_than_folded_into_a_mask() {
let ty = Type::int(32);
let cases = [0, 1, 2, 3, 10, 12, 14, 16];
let arms = [0, 0, 0, 0, 1, 1, 1, 1];
let mut built = built_sharing(&cases, &arms, ty);
switches(&mut built.func);
let text = printed(&built.func, &mut built.names);
assert_eq!(text.matches("icmp ule").count(), 2, "a run's bound and a group's: {text}");
assert_eq!(text.matches("shl").count(), 1, "and only the group is a mask: {text}");
let mut built = built_sharing(&cases, &arms, ty);
routes(&mut built, &cases, &arms, &around(&cases, ty), ty);
}
#[test]
fn every_value_reaches_its_arm_when_a_bit_test_starts_below_zero() {
let ty = Type::int(32);
let cases = [-20, -17, -14, -11, -8, -5];
let arms = [0, 1, 0, 1, 0, 1];
let mut built = built_sharing(&cases, &arms, ty);
routes(&mut built, &cases, &arms, &around(&cases, ty), ty);
}
}