use std::cmp::Reverse;
use std::collections::{BinaryHeap, HashMap, HashSet};
use rucc_base::Interner;
use rucc_mir as mir;
use rucc_target::{BranchInsts, Fusion, Role};
const SCALE: u128 = mir::Weight::SCALE as u128;
pub fn blocks(
func: &mut mir::Func,
insts: &BranchInsts,
names: &mut Interner,
fusable: &HashSet<mir::Inst>,
reorder: bool,
) {
let table = table(insts, names);
let mut order = if reorder { traces(func) } else { order(func) };
let mut writer = Writer { func, insts, names, table, fusable };
let mut at = 0;
while at < order.len() {
if let Some(bridge) = writer.edges(order[at], order.get(at + 1).copied()) {
order.insert(at + 1, bridge);
}
at += 1;
}
func.set_block_order(&order);
}
fn order(func: &mir::Func) -> Vec<mir::Block> {
let mut order = Vec::with_capacity(func.block_count());
let mut seen = vec![false; func.block_count()];
if let Some(entry) = func.entry() {
seen[entry.index()] = true;
let mut stack = vec![(entry, 0usize)];
while let Some((block, next)) = stack.pop() {
let succs = &func[block].succs;
let Some(arm) = succs.len().checked_sub(next + 1) else {
order.push(block);
continue;
};
stack.push((block, next + 1));
let to = succs[arm].block;
if !std::mem::replace(&mut seen[to.index()], true) {
stack.push((to, 0));
}
}
order.reverse();
}
order.extend(func.blocks().filter(|block| !seen[block.index()]));
order
}
const ROUNDS: [(u64, u64); 4] = [(4_000, 5_000), (2_000, 2_000), (1_000, 500), (0, 0)];
fn traces(func: &mir::Func) -> Vec<mir::Block> {
let mut place = vec![usize::MAX; func.block_count()];
for (at, &block) in order(func).iter().enumerate() {
place[block.index()] = at;
}
let mut found: Vec<Vec<mir::Block>> = Vec::new();
let mut seen = vec![false; func.block_count()];
let entered = func.entry().map_or(mir::Weight::ONCE, |entry| func[entry].weight).raw();
let mut reached = vec![0; func.block_count()];
for (likely, often) in ROUNDS {
let floor =
u64::try_from(u128::from(entered) * u128::from(often) / SCALE).unwrap_or(u64::MAX);
let mut queue: BinaryHeap<Seed> = func
.blocks()
.filter(|&block| !seen[block.index()] && func[block].weight.raw() >= floor)
.map(|block| Seed {
reached: reached[block.index()],
weight: func[block].weight,
place: Reverse(place[block.index()]),
block,
})
.collect();
let mut start = func.entry().filter(|entry| !seen[entry.index()]);
while let Some(from) = start.take().or_else(|| next_seed(&mut queue, &seen, &reached)) {
let mut trace = Vec::new();
let mut block = from;
loop {
seen[block.index()] = true;
trace.push(block);
let next = along(func, block, &seen, likely, floor);
for call in &func[block].succs {
let to = call.block;
if seen[to.index()]
|| Some(to) == next
|| call.weight.raw() <= reached[to.index()]
{
continue;
}
reached[to.index()] = call.weight.raw();
if func[to].weight.raw() >= floor {
queue.push(Seed {
reached: call.weight.raw(),
weight: func[to].weight,
place: Reverse(place[to.index()]),
block: to,
});
}
}
let Some(next) = next else { break };
block = next;
}
found.push(trace);
}
}
connect(func, found)
}
fn connect(func: &mir::Func, traces: Vec<Vec<mir::Block>>) -> Vec<mir::Block> {
let mut head = vec![usize::MAX; func.block_count()];
for (at, trace) in traces.iter().enumerate() {
if let Some(&first) = trace.first() {
head[first.index()] = at;
}
}
let mut order = Vec::with_capacity(func.block_count());
let mut used = vec![false; traces.len()];
for from in 0..traces.len() {
if used[from] {
continue;
}
let mut at = from;
loop {
used[at] = true;
order.extend_from_slice(&traces[at]);
let Some(&last) = traces[at].last() else { break };
let mut best: Option<(u64, usize)> = None;
for call in &func[last].succs {
let to = head[call.block.index()];
if to == usize::MAX || used[to] {
continue;
}
let weight = call.weight.raw();
if best.is_none_or(|(found, over)| weight > found || (weight == found && to < over))
{
best = Some((weight, to));
}
}
let Some((_, next)) = best else { break };
at = next;
}
}
order
}
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord)]
struct Seed {
reached: u64,
weight: mir::Weight,
place: Reverse<usize>,
block: mir::Block,
}
fn next_seed(queue: &mut BinaryHeap<Seed>, seen: &[bool], reached: &[u64]) -> Option<mir::Block> {
while let Some(seed) = queue.pop() {
if !seen[seed.block.index()] && seed.reached >= reached[seed.block.index()] {
return Some(seed.block);
}
}
None
}
fn along(
func: &mir::Func,
block: mir::Block,
seen: &[bool],
likely: u64,
floor: u64,
) -> Option<mir::Block> {
let whole = func[block].weight;
let mut best: Option<&mir::BlockCall> = None;
for call in &func[block].succs {
if seen[call.block.index()]
|| call.weight.raw() < floor
|| call.weight.out_of(whole) < likely
{
continue;
}
if best.is_none_or(|found| call.weight > found.weight) {
best = Some(call);
}
}
best.map(|call| call.block)
}
fn table(insts: &BranchInsts, names: &mut Interner) -> HashMap<mir::Opcode, &'static Fusion> {
insts
.fused
.iter()
.map(|fusion| {
(mir::Opcode::new(names.intern(&format!("{}{}", insts.prefix, fusion.set))), fusion)
})
.collect()
}
#[must_use]
pub fn fusable(func: &mir::Func, insts: &BranchInsts, names: &mut Interner) -> HashSet<mir::Inst> {
let table = table(insts, names);
let branch = mir::Opcode::new(names.intern(&format!("{}{}", insts.prefix, insts.cond)));
let reads = crate::changes::Reads::of(func);
let mut found = HashSet::new();
for block in func.blocks() {
let insts: Vec<mir::Inst> = func.insts(block).collect();
let [.., compare, last] = insts[..] else { continue };
if func[last].opcode != branch || !table.contains_key(&func[compare].opcode) {
continue;
}
let operands = &func[func[compare].operands];
let Some(byte) = operands.first().filter(|operand| operand.role != Role::Use) else {
continue;
};
if !byte.reg.is_virtual() || reads.count(byte.reg) != 1 {
continue;
}
if func[func[last].operands].first().map(|operand| operand.reg) == Some(byte.reg) {
found.insert(compare);
}
}
found
}
struct Writer<'a> {
func: &'a mut mir::Func,
insts: &'a BranchInsts,
names: &'a mut Interner,
table: HashMap<mir::Opcode, &'static Fusion>,
fusable: &'a HashSet<mir::Inst>,
}
impl Writer<'_> {
fn edges(&mut self, block: mir::Block, next: Option<mir::Block>) -> Option<mir::Block> {
if self.leaves_indirectly(block) {
return None;
}
match self.func[block].succs.len() {
0 => None,
1 => {
self.one(block, next);
None
}
2 => self.two(block, next),
arms => panic!("a block with {arms} arms, and nothing lowers to one"),
}
}
fn leaves_indirectly(&mut self, block: mir::Block) -> bool {
let Some(last) = self.func.terminator(block) else { return false };
let indirect = self.opcode(self.insts.indirect);
self.func[last].opcode == indirect
}
fn one(&mut self, block: mir::Block, next: Option<mir::Block>) {
if Some(self.func[block].succs[0].block) == next {
return;
}
let opcode = self.opcode(self.insts.jump);
self.func.build(block, opcode).finish();
}
fn two(&mut self, block: mir::Block, next: Option<mir::Block>) -> Option<mir::Block> {
let fused = self.fused(block);
let condition = self.take(block);
let (if_true, if_false) = match fused {
Some((_, fusion)) => (fusion.if_true, fusion.if_false),
None => (self.insts.if_true, self.insts.if_false),
};
let arms: Vec<mir::Block> = self.func[block].succs.iter().map(|arm| arm.block).collect();
let (name, bridge) = if next == Some(arms[1]) {
(if_true, None)
} else if next == Some(arms[0]) {
self.func.succs_mut(block).swap(0, 1);
(if_false, None)
} else {
(if_true, Some(self.bridge(block)))
};
match fused {
Some((compare, fusion)) => self.keep_only_the_flags(compare, fusion),
None => {
let opcode = self.opcode(self.insts.test);
self.func.build(block, opcode).operand(condition).finish();
}
}
let opcode = self.opcode(name);
self.func.build(block, opcode).finish();
bridge
}
fn fused(&self, block: mir::Block) -> Option<(mir::Inst, &'static Fusion)> {
let insts: Vec<mir::Inst> = self.func.insts(block).collect();
let [.., compare, last] = insts[..] else { return None };
if !self.fusable.contains(&compare) {
return None;
}
let fusion = *self.table.get(&self.func[compare].opcode)?;
let byte = self.func[self.func[compare].operands].first()?.reg;
(self.func[self.func[last].operands].first()?.reg == byte).then_some((compare, fusion))
}
fn keep_only_the_flags(&mut self, compare: mir::Inst, fusion: &Fusion) {
let read: Vec<mir::Operand> =
self.func[self.func[compare].operands].iter().skip(1).copied().collect();
let operands = self.func.push_operands(&read);
self.func[compare].opcode = self.opcode(fusion.cmp);
self.func[compare].operands = operands;
}
fn take(&mut self, block: mir::Block) -> mir::Operand {
let branch = self.func.terminator(block).expect("a block with two arms has a branch");
let cond = self.opcode(self.insts.cond);
assert_eq!(
self.func[branch].opcode, cond,
"a block with two arms whose last instruction is not the branch"
);
let operands = self.func[branch].operands;
let condition = self.func[operands][0];
self.func.remove_inst(branch);
condition
}
fn bridge(&mut self, block: mir::Block) -> mir::Block {
let bridge = self.func.create_block();
let edge = self.func[block].succs[1].clone();
let weight = edge.weight;
self.func.set_weight(bridge, weight);
*self.func.succs_mut(bridge) = vec![edge];
self.func.succs_mut(block)[1] = mir::BlockCall::to(bridge).taken(weight);
bridge
}
fn opcode(&mut self, name: &str) -> mir::Opcode {
mir::Opcode::new(self.names.intern(&format!("{}{name}", self.insts.prefix)))
}
}
#[cfg(test)]
mod tests {
use rucc_mir::{BlockCall, Opcode, Operand, Reg};
use rucc_target::x86_64::{BRANCH, GPR, RAX, RCX, REGS};
use super::*;
fn blank(count: usize) -> (Interner, mir::Func, Vec<mir::Block>) {
let mut names = Interner::new();
let mut func = mir::Func::new(names.intern("f"));
let blocks = (0..count).map(|_| func.create_block()).collect();
(names, func, blocks)
}
fn branch(func: &mut mir::Func, names: &mut Interner, block: mir::Block, arms: &[mir::Block]) {
let opcode = Opcode::new(names.intern("x64.br_cond_8"));
func.build(block, opcode).operand(Operand::read(Reg::physical(RAX), GPR)).finish();
*func.succs_mut(block) = arms.iter().map(|&arm| BlockCall::to(arm)).collect();
}
fn laid_out(func: &mut mir::Func, names: &mut Interner) -> Vec<String> {
let fusable = fusable(func, &BRANCH, names);
blocks(func, &BRANCH, names, &fusable, false);
mir::print_func(func, names, ®S)
.lines()
.filter(|line| !line.trim().is_empty() && !line.starts_with("mfunc") && *line != "}")
.map(|line| line.trim().to_string())
.collect()
}
fn order_of(func: &mir::Func) -> Vec<usize> {
func.blocks().map(mir::Block::index).collect()
}
#[test]
fn a_block_that_falls_into_the_next_one_gets_no_jump_at_all() {
let (mut names, mut func, made) = blank(2);
*func.succs_mut(made[0]) = vec![BlockCall::to(made[1])];
let text = laid_out(&mut func, &mut names);
assert_eq!(text, ["block0:", "block1", "block1:"]);
}
#[test]
fn a_block_that_goes_somewhere_that_is_not_next_gets_a_jump() {
let (mut names, mut func, made) = blank(2);
*func.succs_mut(made[0]) = vec![BlockCall::to(made[1])];
*func.succs_mut(made[1]) = vec![BlockCall::to(made[0])];
let text = laid_out(&mut func, &mut names);
assert_eq!(text, ["block0:", "block1", "block1:", "x64.jmp block0"]);
}
#[test]
fn a_branch_that_falls_into_its_false_arm_jumps_when_the_condition_holds() {
let (mut names, mut func, made) = blank(3);
*func.succs_mut(made[0]) = vec![BlockCall::to(made[1])];
branch(&mut func, &mut names, made[1], &[made[0], made[2]]);
let text = laid_out(&mut func, &mut names);
assert_eq!(order_of(&func), [0, 1, 2]);
assert_eq!(
text,
[
"block0:",
"block1",
"block1:",
"x64.test_rr_8 $rax",
"x64.jcc_ne block0, block2",
"block2:",
]
);
}
#[test]
fn a_branch_that_falls_into_its_true_arm_jumps_when_the_condition_does_not_hold() {
let (mut names, mut func, made) = blank(3);
branch(&mut func, &mut names, made[0], &[made[1], made[2]]);
let text = laid_out(&mut func, &mut names);
assert_eq!(order_of(&func), [0, 1, 2]);
assert_eq!(
text,
["block0:", "x64.test_rr_8 $rax", "x64.jcc_e block2, block1", "block1:", "block2:"]
);
}
#[test]
fn a_block_that_leaves_through_a_register_is_given_no_jump_and_keeps_every_arm() {
let (mut names, mut func, made) = blank(4);
let jump = Opcode::new(names.intern("x64.jmp_reg"));
func.build(made[0], jump).operand(Operand::read(Reg::physical(RAX), GPR)).finish();
*func.succs_mut(made[0]) = made[1..].iter().map(|&arm| BlockCall::to(arm)).collect();
let text = laid_out(&mut func, &mut names);
assert_eq!(
text,
[
"block0:",
"x64.jmp_reg $rax, block1, block2, block3",
"block1:",
"block2:",
"block3:"
]
);
}
#[test]
fn a_branch_that_can_fall_into_neither_arm_is_given_a_block_to_jump_from() {
let (mut names, mut func, made) = blank(2);
*func.succs_mut(made[0]) = vec![BlockCall::to(made[1])];
branch(&mut func, &mut names, made[1], &[made[0], made[1]]);
let text = laid_out(&mut func, &mut names);
assert_eq!(order_of(&func), [0, 1, 2]);
assert_eq!(
text,
[
"block0:",
"block1",
"block1:",
"x64.test_rr_8 $rax",
"x64.jcc_ne block0, block2",
"block2:",
"x64.jmp block1",
]
);
}
#[test]
fn the_test_reads_the_register_the_branch_read() {
let (mut names, mut func, made) = blank(3);
branch(&mut func, &mut names, made[0], &[made[1], made[2]]);
let fusable = fusable(&func, &BRANCH, &mut names);
blocks(&mut func, &BRANCH, &mut names, &fusable, false);
let test = func.insts(made[0]).next().expect("a test");
let operands = func[test].operands;
assert_eq!(func[operands], [Operand::read(Reg::physical(RAX), GPR)]);
}
#[test]
fn a_block_nothing_reaches_is_laid_out_at_the_end_rather_than_deleted() {
let (mut names, mut func, made) = blank(4);
*func.succs_mut(made[0]) = vec![BlockCall::to(made[3])];
let fusable = fusable(&func, &BRANCH, &mut names);
blocks(&mut func, &BRANCH, &mut names, &fusable, false);
assert_eq!(order_of(&func), [0, 3, 1, 2]);
}
#[test]
fn a_function_with_no_blocks_is_left_alone() {
let mut names = Interner::new();
let mut func = mir::Func::new(names.intern("f"));
let fusable = fusable(&func, &BRANCH, &mut names);
blocks(&mut func, &BRANCH, &mut names, &fusable, false);
assert_eq!(func.block_count(), 0);
}
#[test]
#[should_panic(expected = "a block with 3 arms")]
fn a_block_with_three_arms_is_refused_rather_than_laid_out_wrongly() {
let (mut names, mut func, made) = blank(4);
branch(&mut func, &mut names, made[0], &[made[1], made[2], made[3]]);
let fusable = fusable(&func, &BRANCH, &mut names);
blocks(&mut func, &BRANCH, &mut names, &fusable, false);
}
#[test]
#[should_panic(expected = "whose last instruction is not the branch")]
fn a_block_with_two_arms_and_no_branch_in_it_is_refused() {
let (mut names, mut func, made) = blank(3);
let opcode = Opcode::new(names.intern("x64.nop"));
func.build(made[0], opcode).finish();
*func.succs_mut(made[0]) = vec![BlockCall::to(made[1]), BlockCall::to(made[2])];
let fusable = fusable(&func, &BRANCH, &mut names);
blocks(&mut func, &BRANCH, &mut names, &fusable, false);
}
fn compare(
func: &mut mir::Func,
names: &mut Interner,
block: mir::Block,
arms: &[mir::Block],
) -> Reg {
let byte = func.new_vreg(GPR);
let opcode = Opcode::new(names.intern("x64.cmp_set_l_32"));
func.build(block, opcode)
.def(byte, GPR)
.operand(Operand::read(Reg::physical(RAX), GPR))
.operand(Operand::read(Reg::physical(RCX), GPR))
.finish();
let opcode = Opcode::new(names.intern("x64.br_cond_8"));
func.build(block, opcode).operand(Operand::read(byte, GPR)).finish();
*func.succs_mut(block) = arms.iter().map(|&arm| BlockCall::to(arm)).collect();
byte
}
#[test]
fn a_branch_on_a_comparison_is_the_comparison_and_a_jump_on_what_it_found() {
let (mut names, mut func, made) = blank(3);
compare(&mut func, &mut names, made[0], &[made[1], made[2]]);
let text = laid_out(&mut func, &mut names);
assert_eq!(
text,
[
"block0:",
"x64.cmp_rr_32 $rax, $rcx",
"x64.jcc_ge block2, block1",
"block1:",
"block2:",
]
);
}
#[test]
fn a_comparison_whose_answer_something_else_reads_keeps_its_byte_and_its_test() {
let (mut names, mut func, made) = blank(3);
let byte = compare(&mut func, &mut names, made[0], &[made[1], made[2]]);
let opcode = Opcode::new(names.intern("x64.mov_rr_64"));
func.build(made[1], opcode)
.def(Reg::physical(RAX), GPR)
.operand(Operand::read(byte, GPR))
.finish();
let text = laid_out(&mut func, &mut names);
assert!(text.contains(&"x64.test_rr_8 %0".to_owned()), "{text:?}");
assert!(text.contains(&"x64.jcc_e block2, block1".to_owned()), "{text:?}");
}
#[test]
fn a_comparison_that_is_no_longer_in_front_of_its_branch_keeps_its_test() {
let (mut names, mut func, made) = blank(3);
compare(&mut func, &mut names, made[0], &[made[1], made[2]]);
let fusable = fusable(&func, &BRANCH, &mut names);
assert_eq!(fusable.len(), 1, "the comparison is one the byte's count allows");
let branch = func.terminator(made[0]).expect("a block with two arms has a branch");
let opcode = Opcode::new(names.intern("x64.mov_rr_64"));
let reload = func
.build_loose(opcode)
.def(Reg::physical(RCX), GPR)
.operand(Operand::read(Reg::physical(RAX), GPR))
.finish();
func.insert_before(branch, reload);
blocks(&mut func, &BRANCH, &mut names, &fusable, false);
let text = mir::print_func(&func, &names, ®S);
assert!(text.contains("x64.cmp_set_l_32"), "{text}");
assert!(text.contains("x64.test_rr_8"), "{text}");
assert!(!text.contains("x64.cmp_rr_32"), "{text}");
}
fn traced(func: &mut mir::Func, names: &mut Interner) -> Vec<String> {
let fusable = fusable(func, &BRANCH, names);
blocks(func, &BRANCH, names, &fusable, true);
mir::print_func(func, names, ®S)
.lines()
.filter(|line| !line.trim().is_empty() && !line.starts_with("mfunc") && *line != "}")
.map(|line| line.trim().to_string())
.collect()
}
fn runs(func: &mut mir::Func, block: mir::Block, weight: u64, arms: &[u64]) {
func.set_weight(block, mir::Weight::parts(weight));
for (index, &taken) in arms.iter().enumerate() {
func.succs_mut(block)[index].weight = mir::Weight::parts(taken);
}
}
#[test]
fn the_arm_that_is_nearly_always_taken_is_the_one_laid_out_next() {
let (mut names, mut func, made) = blank(3);
branch(&mut func, &mut names, made[0], &[made[1], made[2]]);
runs(&mut func, made[0], 10_000, &[200, 9_800]);
runs(&mut func, made[1], 200, &[]);
runs(&mut func, made[2], 9_800, &[]);
traced(&mut func, &mut names);
assert_eq!(order_of(&func), [0, 2, 1]);
let (mut names, mut func, made) = blank(3);
branch(&mut func, &mut names, made[0], &[made[1], made[2]]);
runs(&mut func, made[0], 10_000, &[9_800, 200]);
runs(&mut func, made[1], 9_800, &[]);
runs(&mut func, made[2], 200, &[]);
traced(&mut func, &mut names);
assert_eq!(order_of(&func), [0, 1, 2]);
}
#[test]
fn a_loop_is_laid_out_with_its_exit_behind_it_and_its_back_edge_running_backwards() {
let (mut names, mut func, made) = blank(4);
*func.succs_mut(made[0]) = vec![BlockCall::to(made[1])];
branch(&mut func, &mut names, made[1], &[made[2], made[3]]);
*func.succs_mut(made[2]) = vec![BlockCall::to(made[1])];
runs(&mut func, made[0], 10_000, &[10_000]);
runs(&mut func, made[1], 100_000, &[90_000, 10_000]);
runs(&mut func, made[2], 90_000, &[90_000]);
runs(&mut func, made[3], 10_000, &[]);
let text = traced(&mut func, &mut names);
assert_eq!(order_of(&func), [0, 1, 2, 3]);
assert_eq!(
text,
[
"block0:",
"block1",
"block1:",
"x64.test_rr_8 $rax",
"x64.jcc_e block3, block2",
"block2:",
"x64.jmp block1",
"block3:",
]
);
}
#[test]
fn a_block_only_the_cold_arm_reaches_goes_behind_the_rest_of_the_function() {
let (mut names, mut func, made) = blank(4);
branch(&mut func, &mut names, made[0], &[made[1], made[2]]);
*func.succs_mut(made[1]) = vec![BlockCall::to(made[2])];
*func.succs_mut(made[2]) = vec![BlockCall::to(made[3])];
runs(&mut func, made[0], 10_000, &[100, 9_900]);
runs(&mut func, made[1], 100, &[100]);
runs(&mut func, made[2], 10_000, &[10_000]);
runs(&mut func, made[3], 10_000, &[]);
assert_eq!(order(&func), [made[0], made[1], made[2], made[3]]);
traced(&mut func, &mut names);
assert_eq!(order_of(&func), [0, 2, 3, 1]);
}
#[test]
fn the_last_round_picks_up_a_block_nothing_reaches() {
let (mut names, mut func, made) = blank(3);
*func.succs_mut(made[0]) = vec![BlockCall::to(made[2])];
runs(&mut func, made[0], 10_000, &[10_000]);
runs(&mut func, made[1], 0, &[]);
runs(&mut func, made[2], 10_000, &[]);
traced(&mut func, &mut names);
assert_eq!(order_of(&func), [0, 2, 1]);
}
#[test]
fn the_entry_is_the_first_seed_even_when_something_else_runs_more_often() {
let (mut names, mut func, made) = blank(3);
*func.succs_mut(made[0]) = vec![BlockCall::to(made[1])];
branch(&mut func, &mut names, made[1], &[made[1], made[2]]);
runs(&mut func, made[0], 10_000, &[10_000]);
runs(&mut func, made[1], 100_000, &[90_000, 10_000]);
runs(&mut func, made[2], 10_000, &[]);
traced(&mut func, &mut names);
assert_eq!(func.blocks().next().map(mir::Block::index), Some(0));
}
#[test]
fn a_branch_whose_arms_are_even_is_still_laid_out_next_to_one_of_them() {
let (mut names, mut func, made) = blank(3);
branch(&mut func, &mut names, made[0], &[made[2], made[1]]);
runs(&mut func, made[0], 10_000, &[5_000, 5_000]);
runs(&mut func, made[1], 5_000, &[]);
runs(&mut func, made[2], 5_000, &[]);
traced(&mut func, &mut names);
assert_eq!(order_of(&func), [0, 2, 1]);
}
#[test]
fn a_chain_the_rounds_cut_in_half_is_run_back_together() {
let (mut names, mut func, made) = blank(5);
branch(&mut func, &mut names, made[0], &[made[2], made[1]]);
branch(&mut func, &mut names, made[2], &[made[4], made[3]]);
runs(&mut func, made[0], 10_000, &[5_000, 5_000]);
runs(&mut func, made[1], 5_000, &[]);
runs(&mut func, made[2], 5_000, &[3_000, 2_000]);
runs(&mut func, made[3], 2_000, &[]);
runs(&mut func, made[4], 3_000, &[]);
traced(&mut func, &mut names);
assert_eq!(order_of(&func), [0, 2, 4, 1, 3]);
}
}