use std::collections::{BTreeSet, HashMap, HashSet};
use rucc_base::Interner;
use rucc_mir::{Block, Func, Inst, Reg, Role};
use rucc_target::{FlagInsts, MachineInsts, RegClass, Timing, TimingInsts, Unit};
type Place = (Reg, RegClass);
pub const READY: usize = 100;
pub const LONGEST: usize = 2000;
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
pub struct Scheduled {
pub runs: usize,
pub moved: usize,
}
pub fn insts(
func: &mut Func,
timing: &TimingInsts,
machine: &MachineInsts,
flags: &FlagInsts,
names: &Interner,
accurate: bool,
pinned: &HashSet<Inst>,
) -> Scheduled {
let blocks: Vec<Block> = func.blocks().collect();
let mut done = Scheduled::default();
for block in blocks {
let was: Vec<Inst> = func.insts(block).collect();
if was.len() < 3 {
continue;
}
let mut now: Vec<Inst> = Vec::with_capacity(was.len());
let mut run: Vec<Inst> = Vec::new();
let last = was.last().copied();
for &inst in &was {
if Some(inst) == last
|| pinned.contains(&inst)
|| barrier(func, inst, timing, machine, names)
{
done.runs += usize::from(order(
func, &run, timing, machine, flags, names, accurate, &mut now,
));
run.clear();
now.push(inst);
} else {
run.push(inst);
}
}
done.runs +=
usize::from(order(func, &run, timing, machine, flags, names, accurate, &mut now));
let moved = was.iter().zip(&now).filter(|(before, after)| before != after).count();
if moved == 0 {
continue;
}
done.moved += moved;
for &inst in &was {
func.remove_inst(inst);
}
for &inst in &now {
func.append_inst(block, inst);
}
}
done
}
fn barrier(
func: &Func,
inst: Inst,
timing: &TimingInsts,
machine: &MachineInsts,
names: &Interner,
) -> bool {
let name = names.resolve(func[inst].opcode.name());
machine.calls(name)
|| !machine.has(name)
|| timing.of(name).is_none_or(|timing| timing.unit == Unit::Fixed)
|| func.cfi_after(inst).next().is_some()
}
#[allow(clippy::too_many_arguments)]
fn order(
func: &Func,
run: &[Inst],
timing: &TimingInsts,
machine: &MachineInsts,
flags: &FlagInsts,
names: &Interner,
accurate: bool,
into: &mut Vec<Inst>,
) -> bool {
if run.len() < 2 || run.len() > LONGEST {
into.extend_from_slice(run);
return false;
}
let nodes = graph(func, run, timing, machine, flags, names);
into.extend(list(&nodes, timing, accurate).into_iter().map(|at| run[at]));
true
}
#[derive(Debug)]
struct Node {
timing: Timing,
succs: Vec<(usize, u32)>,
preds: usize,
height: u32,
growth: i32,
}
fn graph(
func: &Func,
run: &[Inst],
timing: &TimingInsts,
machine: &MachineInsts,
flags: &FlagInsts,
names: &Interner,
) -> Vec<Node> {
let costs: Vec<Timing> = run
.iter()
.map(|&inst| {
timing.of(names.resolve(func[inst].opcode.name())).expect("a barrier otherwise")
})
.collect();
let mut nodes: Vec<Node> = costs
.iter()
.map(|&timing| Node { timing, succs: Vec::new(), preds: 0, height: 0, growth: 0 })
.collect();
let mut wrote: HashMap<Place, usize> = HashMap::new();
let mut read: HashMap<Place, Vec<usize>> = HashMap::new();
let mut wrote_flags: Option<usize> = None;
let mut read_flags: Vec<usize> = Vec::new();
let mut touched: Option<usize> = None;
for (at, &inst) in run.iter().enumerate() {
let name = names.resolve(func[inst].opcode.name());
let bare = name.strip_prefix(flags.prefix).unwrap_or(name);
for operand in &func[func[inst].operands] {
if operand.role == Role::Use {
let place = (operand.reg, operand.class);
if let Some(before) = wrote.get(&place) {
edge(&mut nodes, *before, at, costs[*before].latency);
}
read.entry(place).or_default().push(at);
}
}
if flags.reads(bare).is_some() {
if let Some(before) = wrote_flags {
edge(&mut nodes, before, at, costs[before].latency);
}
read_flags.push(at);
}
for operand in &func[func[inst].operands] {
if operand.role.is_def() {
let place = (operand.reg, operand.class);
if let Some(before) = wrote.insert(place, at) {
edge(&mut nodes, before, at, after(&costs, before));
}
for before in read.remove(&place).unwrap_or_default() {
if before != at {
edge(&mut nodes, before, at, 0);
}
}
}
}
if (flags.writes)(bare) {
if let Some(before) = wrote_flags.replace(at) {
edge(&mut nodes, before, at, after(&costs, before));
}
for before in read_flags.drain(..) {
if before != at {
edge(&mut nodes, before, at, 0);
}
}
}
if machine.touches_mem(name) || func[inst].mem.is_some() {
if let Some(before) = touched.replace(at) {
edge(&mut nodes, before, at, 0);
}
}
}
heights(&mut nodes);
growth(func, run, &mut nodes);
nodes
}
fn edge(nodes: &mut [Node], from: usize, to: usize, wait: u32) {
if let Some(found) = nodes[from].succs.iter_mut().find(|(succ, _)| *succ == to) {
found.1 = found.1.max(wait);
return;
}
nodes[from].succs.push((to, wait));
nodes[to].preds += 1;
}
fn after(costs: &[Timing], before: usize) -> u32 {
costs[before].latency.min(1)
}
fn heights(nodes: &mut [Node]) {
for at in (0..nodes.len()).rev() {
let mut height = nodes[at].timing.latency;
for index in 0..nodes[at].succs.len() {
let (succ, wait) = nodes[at].succs[index];
height = height.max(wait + nodes[succ].height);
}
nodes[at].height = height;
}
}
fn growth(func: &Func, run: &[Inst], nodes: &mut [Node]) {
let mut seen: HashSet<Place> = HashSet::new();
for (at, &inst) in run.iter().enumerate().rev() {
for operand in &func[func[inst].operands] {
if operand.role == Role::Use && seen.insert((operand.reg, operand.class)) {
nodes[at].growth -= 1;
}
}
for operand in &func[func[inst].operands] {
if operand.role.is_def() {
nodes[at].growth += 1;
}
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
struct Pick {
path: i64,
growth: i32,
waits: bool,
users: i64,
at: usize,
}
fn list(nodes: &[Node], timing: &TimingInsts, accurate: bool) -> Vec<usize> {
let mut preds: Vec<usize> = nodes.iter().map(|node| node.preds).collect();
let mut when: Vec<u32> = vec![0; nodes.len()];
let mut ready: BTreeSet<usize> = (0..nodes.len()).filter(|&at| preds[at] == 0).collect();
let mut out: Vec<usize> = Vec::with_capacity(nodes.len());
let mut cycle = 0;
let mut used: HashMap<Unit, u32> = HashMap::new();
let mut issued = 0;
let mut last: Option<usize> = None;
while !ready.is_empty() {
let mut best: Option<Pick> = None;
for &at in ready.iter().take(READY) {
if when[at] > cycle || (accurate && !fits(nodes[at].timing.unit, &used, issued, timing))
{
continue;
}
let pick = Pick {
path: -i64::from(nodes[at].height),
growth: nodes[at].growth,
waits: last.is_some_and(|last| nodes[last].succs.iter().any(|&(to, _)| to == at)),
users: -(nodes[at].succs.len() as i64),
at,
};
if best.is_none_or(|best| pick < best) {
best = Some(pick);
}
}
let Some(best) = best else {
let soonest = ready.iter().take(READY).map(|&at| when[at]).min().unwrap_or(cycle);
cycle = soonest.max(cycle + 1);
used.clear();
issued = 0;
continue;
};
let at = best.at;
ready.remove(&at);
out.push(at);
last = Some(at);
*used.entry(nodes[at].timing.unit).or_default() += 1;
issued += 1;
for index in 0..nodes[at].succs.len() {
let (succ, wait) = nodes[at].succs[index];
when[succ] = when[succ].max(cycle + wait);
preds[succ] -= 1;
if preds[succ] == 0 {
ready.insert(succ);
}
}
}
out
}
fn fits(unit: Unit, used: &HashMap<Unit, u32>, issued: u32, timing: &TimingInsts) -> bool {
issued < timing.width.max(1) && used.get(&unit).copied().unwrap_or(0) < timing.slots(unit)
}
#[cfg(test)]
mod tests {
use rucc_mir::{Constraint, Mem, Opcode, Operand};
use rucc_target::PhysReg;
use rucc_target::x86_64::{
self, FLAGS, GPR, MACHINE, R8, R9, R10, RAX, RCX, RDI, RDX, RSI, TIMING, XMM,
};
use super::*;
fn empty() -> (Interner, Func, Block) {
let mut names = Interner::new();
let mut func = Func::new(names.intern("f"));
let block = func.create_block();
(names, func, block)
}
fn op(names: &mut Interner, name: &str) -> Opcode {
Opcode::new(names.intern(&format!("{}{name}", MACHINE.prefix)))
}
fn reg(which: PhysReg) -> Reg {
Reg::physical(which)
}
fn alu(
func: &mut Func,
names: &mut Interner,
block: Block,
name: &str,
into: PhysReg,
from: PhysReg,
) {
let opcode = op(names, name);
func.build(block, opcode)
.operand(Operand::write(reg(into), GPR).with(Constraint::Reuse(1)))
.uses(reg(into), GPR)
.uses(reg(from), GPR)
.finish();
}
fn vector(
func: &mut Func,
names: &mut Interner,
block: Block,
name: &str,
into: PhysReg,
from: PhysReg,
) {
let opcode = op(names, name);
func.build(block, opcode)
.operand(Operand::write(reg(into), XMM).with(Constraint::Reuse(1)))
.uses(reg(into), XMM)
.uses(reg(from), XMM)
.finish();
}
fn mov(func: &mut Func, names: &mut Interner, block: Block, into: PhysReg, from: PhysReg) {
let opcode = op(names, "mov_rr_64");
func.build(block, opcode).def(reg(into), GPR).uses(reg(from), GPR).finish();
}
fn load(func: &mut Func, names: &mut Interner, block: Block, into: PhysReg, base: PhysReg) {
let opcode = op(names, "mov_rm_64");
func.build(block, opcode)
.def(reg(into), GPR)
.mem(Mem::at(Operand::read(reg(base), GPR)))
.finish();
}
fn bare(func: &mut Func, names: &mut Interner, block: Block, name: &str) {
let opcode = op(names, name);
func.build(block, opcode).finish();
}
fn shape(func: &Func, names: &Interner, block: Block) -> Vec<String> {
func.insts(block)
.map(|inst| TIMING.bare(names.resolve(func[inst].opcode.name())).to_owned())
.collect()
}
fn schedule(func: &mut Func, names: &Interner) -> Scheduled {
insts(func, &TIMING, &MACHINE, &FLAGS, names, false, &HashSet::new())
}
#[test]
fn a_block_already_in_the_only_order_it_has_comes_out_unchanged() {
let (mut names, mut func, block) = empty();
mov(&mut func, &mut names, block, RAX, RDX);
alu(&mut func, &mut names, block, "add_rr_64", RAX, RCX);
bare(&mut func, &mut names, block, "ret");
let done = schedule(&mut func, &names);
assert_eq!(done.moved, 0, "there was nothing else it could have written");
assert_eq!(shape(&func, &names, block), ["mov_rr_64", "add_rr_64", "ret"]);
}
#[test]
fn work_that_depends_on_nothing_moves_into_a_multiplys_latency() {
let (mut names, mut func, block) = empty();
alu(&mut func, &mut names, block, "imul_rr_64", RDI, RSI);
alu(&mut func, &mut names, block, "add_rr_64", RDI, RCX);
mov(&mut func, &mut names, block, RAX, RDX);
bare(&mut func, &mut names, block, "ret");
let done = schedule(&mut func, &names);
assert_eq!(done.runs, 1, "one run, since nothing in it is a barrier");
assert_eq!(
shape(&func, &names, block),
["imul_rr_64", "mov_rr_64", "add_rr_64", "ret"],
"the move is doing a cycle of the three the addition was going to spend waiting"
);
}
#[test]
fn nothing_crosses_a_call() {
let (mut names, mut func, block) = empty();
mov(&mut func, &mut names, block, RAX, RDX);
bare(&mut func, &mut names, block, "call");
alu(&mut func, &mut names, block, "imul_rr_64", RDI, RSI);
alu(&mut func, &mut names, block, "add_rr_64", RDI, RCX);
bare(&mut func, &mut names, block, "ret");
let done = schedule(&mut func, &names);
assert_eq!(done.moved, 0);
assert_eq!(
shape(&func, &names, block),
["mov_rr_64", "call", "imul_rr_64", "add_rr_64", "ret"]
);
}
#[test]
fn two_reads_of_memory_keep_the_order_they_arrived_in() {
let (mut names, mut func, block) = empty();
load(&mut func, &mut names, block, RAX, RDI);
load(&mut func, &mut names, block, RCX, RSI);
alu(&mut func, &mut names, block, "imul_rr_64", RCX, RDX);
bare(&mut func, &mut names, block, "ret");
let done = schedule(&mut func, &names);
assert_eq!(done.moved, 0);
assert_eq!(
shape(&func, &names, block),
["mov_rm_64", "mov_rm_64", "imul_rr_64", "ret"],
"the read whose value nothing here wants stayed in front of the one that matters"
);
}
#[test]
fn two_writes_of_one_register_keep_the_order_they_arrived_in() {
let (mut names, mut func, block) = empty();
mov(&mut func, &mut names, block, RAX, RDX);
mov(&mut func, &mut names, block, RAX, RCX);
alu(&mut func, &mut names, block, "imul_rr_64", RAX, RSI);
bare(&mut func, &mut names, block, "ret");
let done = schedule(&mut func, &names);
assert_eq!(done.moved, 0);
assert_eq!(shape(&func, &names, block), ["mov_rr_64", "mov_rr_64", "imul_rr_64", "ret"]);
}
#[test]
fn a_write_stays_behind_the_read_of_what_the_register_held() {
let (mut names, mut func, block) = empty();
alu(&mut func, &mut names, block, "add_rr_64", RCX, RAX);
mov(&mut func, &mut names, block, RAX, RDX);
alu(&mut func, &mut names, block, "imul_rr_64", RAX, RSI);
bare(&mut func, &mut names, block, "ret");
let done = schedule(&mut func, &names);
assert_eq!(done.moved, 0);
assert_eq!(
shape(&func, &names, block),
["add_rr_64", "mov_rr_64", "imul_rr_64", "ret"],
"the addition read what was in the register before the move put something else there"
);
}
#[test]
fn the_instruction_that_reads_the_condition_state_stays_behind_the_comparison() {
let (mut names, mut func, block) = empty();
mov(&mut func, &mut names, block, RCX, RDX);
let cmp = op(&mut names, "cmp_rr_64");
func.build(block, cmp).uses(reg(RDI), GPR).uses(reg(RSI), GPR).finish();
let set = op(&mut names, "set_e");
func.build(block, set).def(reg(RAX), GPR).finish();
alu(&mut func, &mut names, block, "add_rr_64", RAX, R8);
bare(&mut func, &mut names, block, "ret");
schedule(&mut func, &names);
assert_eq!(
shape(&func, &names, block),
["cmp_rr_64", "mov_rr_64", "set_e", "add_rr_64", "ret"],
"the move went into the cycle the set was waiting for the comparison in"
);
}
#[test]
fn the_last_instruction_of_a_block_never_moves() {
let (mut names, mut func, block) = empty();
mov(&mut func, &mut names, block, RAX, RDX);
mov(&mut func, &mut names, block, RCX, R8);
alu(&mut func, &mut names, block, "imul_rr_64", RSI, R9);
let done = schedule(&mut func, &names);
assert_eq!(done.moved, 0);
assert_eq!(
shape(&func, &names, block),
["mov_rr_64", "mov_rr_64", "imul_rr_64"],
"the multiply has the longest path and is last anyway"
);
}
#[test]
fn an_instruction_the_caller_pinned_never_moves() {
let build = |names: &mut Interner| {
let mut func = Func::new(names.intern("f"));
let block = func.create_block();
mov(&mut func, names, block, RAX, RDX);
mov(&mut func, names, block, RCX, R8);
alu(&mut func, names, block, "imul_rr_64", RSI, R9);
bare(&mut func, names, block, "ret");
(func, block)
};
let mut names = Interner::new();
let (mut loose, block) = build(&mut names);
schedule(&mut loose, &names);
assert_eq!(
shape(&loose, &names, block),
["imul_rr_64", "mov_rr_64", "mov_rr_64", "ret"],
"with nothing pinned the multiply goes first, since it has the longest path"
);
let (mut held, block) = build(&mut names);
let second = held.insts(block).nth(1).expect("the second move");
insts(&mut held, &TIMING, &MACHINE, &FLAGS, &names, false, &HashSet::from([second]));
assert_eq!(
shape(&held, &names, block),
["mov_rr_64", "mov_rr_64", "imul_rr_64", "ret"],
"pinning it splits the block into runs of one, and a run of one has one order"
);
}
#[test]
fn a_name_this_target_does_not_have_is_a_barrier() {
let (mut names, mut func, block) = empty();
mov(&mut func, &mut names, block, RAX, RDX);
bare(&mut func, &mut names, block, "not_an_instruction_this_machine_has");
alu(&mut func, &mut names, block, "imul_rr_64", RSI, R9);
bare(&mut func, &mut names, block, "ret");
let done = schedule(&mut func, &names);
assert_eq!(done.moved, 0);
assert_eq!(
shape(&func, &names, block),
["mov_rr_64", "not_an_instruction_this_machine_has", "imul_rr_64", "ret"]
);
}
#[test]
fn an_instruction_the_model_does_not_describe_is_a_barrier() {
let (mut names, mut func, block) = empty();
mov(&mut func, &mut names, block, RAX, RDX);
bare(&mut func, &mut names, block, "ud2");
alu(&mut func, &mut names, block, "imul_rr_64", RSI, R9);
bare(&mut func, &mut names, block, "ret");
assert_eq!(TIMING.of("x64.ud2").expect("described").unit, Unit::Fixed);
let done = schedule(&mut func, &names);
assert_eq!(done.moved, 0);
assert_eq!(shape(&func, &names, block), ["mov_rr_64", "ud2", "imul_rr_64", "ret"]);
}
#[test]
fn what_comes_out_is_the_instructions_that_went_in_and_no_others() {
let (mut names, mut func, block) = empty();
alu(&mut func, &mut names, block, "imul_rr_64", RDI, RSI);
mov(&mut func, &mut names, block, RAX, RDX);
load(&mut func, &mut names, block, RCX, R8);
alu(&mut func, &mut names, block, "add_rr_64", RAX, RCX);
alu(&mut func, &mut names, block, "sub_rr_64", RDX, R9);
mov(&mut func, &mut names, block, R10, RDI);
alu(&mut func, &mut names, block, "imul_rr_64", R10, RAX);
alu(&mut func, &mut names, block, "add_rr_64", R10, RDX);
bare(&mut func, &mut names, block, "ret");
let mut was: Vec<Inst> = func.insts(block).collect();
schedule(&mut func, &names);
let mut now: Vec<Inst> = func.insts(block).collect();
assert_eq!(now.len(), was.len(), "nothing was added or dropped");
was.sort_unstable();
now.sort_unstable();
assert_eq!(now, was, "the same instructions, in some order");
}
#[test]
fn the_same_block_twice_gives_the_same_order_twice() {
let build = |names: &mut Interner| {
let mut func = Func::new(names.intern("f"));
let block = func.create_block();
alu(&mut func, names, block, "imul_rr_64", RDI, RSI);
mov(&mut func, names, block, RAX, RDX);
load(&mut func, names, block, RCX, R8);
alu(&mut func, names, block, "add_rr_64", RAX, RCX);
alu(&mut func, names, block, "sub_rr_64", RDX, R9);
mov(&mut func, names, block, R10, RDI);
alu(&mut func, names, block, "imul_rr_64", R10, RAX);
bare(&mut func, names, block, "ret");
(func, block)
};
let mut names = Interner::new();
let (mut first, one) = build(&mut names);
let (mut second, two) = build(&mut names);
schedule(&mut first, &names);
schedule(&mut second, &names);
assert_eq!(shape(&first, &names, one), shape(&second, &names, two));
}
#[test]
fn a_constant_put_in_a_register_is_not_hoisted_over_work_that_hands_one_back() {
let (mut names, mut func, block) = empty();
let load_imm = op(&mut names, "mov_ri_64");
func.build(block, load_imm).def(reg(RCX), GPR).imm(5).finish();
alu(&mut func, &mut names, block, "add_rr_64", RAX, RDX);
alu(&mut func, &mut names, block, "add_rr_64", RAX, RCX);
bare(&mut func, &mut names, block, "ret");
schedule(&mut func, &names);
assert_eq!(
shape(&func, &names, block),
["add_rr_64", "mov_ri_64", "add_rr_64", "ret"],
"the constant is loaded as early as necessary and no earlier"
);
}
#[test]
fn a_model_worth_believing_about_its_units_fills_a_full_cycle_with_other_work() {
let build = |names: &mut Interner| {
let mut func = Func::new(names.intern("f"));
let block = func.create_block();
vector(&mut func, names, block, "addsd_rr", x86_64::xmm(0), x86_64::xmm(1));
vector(&mut func, names, block, "addsd_rr", x86_64::xmm(2), x86_64::xmm(3));
vector(&mut func, names, block, "addsd_rr", x86_64::xmm(4), x86_64::xmm(5));
mov(&mut func, names, block, RAX, RDX);
bare(&mut func, names, block, "ret");
(func, block)
};
assert_eq!(TIMING.slots(Unit::Float), 2, "the machine this model describes has two");
let mut names = Interner::new();
let (mut loose, block) = build(&mut names);
insts(&mut loose, &TIMING, &MACHINE, &FLAGS, &names, false, &HashSet::new());
assert_eq!(
shape(&loose, &names, block),
["addsd_rr", "addsd_rr", "addsd_rr", "mov_rr_64", "ret"],
"without the units the three additions are the same instruction three times over"
);
let (mut tight, block) = build(&mut names);
insts(&mut tight, &TIMING, &MACHINE, &FLAGS, &names, true, &HashSet::new());
assert_eq!(
shape(&tight, &names, block),
["addsd_rr", "addsd_rr", "mov_rr_64", "addsd_rr", "ret"],
"the third addition has nowhere to go this cycle and the move has"
);
}
#[test]
fn a_run_longer_than_the_bound_is_left_alone() {
let build = |names: &mut Interner, moves: usize| {
let mut func = Func::new(names.intern("f"));
let block = func.create_block();
alu(&mut func, names, block, "imul_rr_64", RSI, R9);
for _ in 0..moves {
mov(&mut func, names, block, RAX, RDX);
}
bare(&mut func, names, block, "ret");
(func, block)
};
let mut names = Interner::new();
let (mut short, block) = build(&mut names, 8);
let done = schedule(&mut short, &names);
assert!(done.moved > 0, "below the bound a run is looked at");
assert_eq!(
shape(&short, &names, block).first().map(String::as_str),
Some("mov_rr_64"),
"the chain of moves is the long way round and starts first"
);
let (mut long, block) = build(&mut names, LONGEST);
let done = schedule(&mut long, &names);
assert_eq!(done.moved, 0, "above it the run is written back exactly as it arrived");
assert_eq!(shape(&long, &names, block).first().map(String::as_str), Some("imul_rr_64"));
}
#[test]
fn a_block_of_two_instructions_is_not_looked_at() {
let (mut names, mut func, block) = empty();
alu(&mut func, &mut names, block, "imul_rr_64", RSI, R9);
bare(&mut func, &mut names, block, "ret");
let done = schedule(&mut func, &names);
assert_eq!(done, Scheduled::default());
assert_eq!(shape(&func, &names, block), ["imul_rr_64", "ret"]);
}
#[test]
fn a_shift_by_a_variable_amount_stays_behind_the_write_of_the_register_it_counts() {
let (mut names, mut func, block) = empty();
mov(&mut func, &mut names, block, RCX, R8);
let shift = op(&mut names, "shl_rcl_64");
func.build(block, shift)
.operand(Operand::write(reg(RAX), GPR).with(Constraint::Reuse(1)))
.uses(reg(RAX), GPR)
.uses(reg(RCX), GPR)
.finish();
alu(&mut func, &mut names, block, "imul_rr_64", RAX, RDX);
bare(&mut func, &mut names, block, "ret");
let done = schedule(&mut func, &names);
assert_eq!(done.moved, 0);
assert_eq!(shape(&func, &names, block), ["mov_rr_64", "shl_rcl_64", "imul_rr_64", "ret"]);
}
#[test]
fn a_divide_names_both_of_the_registers_the_machine_makes_it_use() {
let (mut names, mut func, block) = empty();
mov(&mut func, &mut names, block, RDX, R8);
let divide = op(&mut names, "idiv_quo_64");
func.build(block, divide)
.operand(Operand::write(reg(RAX), GPR).with(Constraint::Fixed(RAX)))
.operand(Operand::write_early(reg(RDX), GPR).with(Constraint::Fixed(RDX)))
.operand(Operand::read(reg(RAX), GPR).with(Constraint::Fixed(RAX)))
.uses(reg(RSI), GPR)
.finish();
bare(&mut func, &mut names, block, "ret");
assert!(TIMING.of("x64.idiv_quo_64").expect("described").latency > 1);
let done = schedule(&mut func, &names);
assert_eq!(done.moved, 0);
assert_eq!(shape(&func, &names, block), ["mov_rr_64", "idiv_quo_64", "ret"]);
}
#[test]
fn every_unit_a_run_can_ask_for_has_at_least_one_of_it() {
for &unit in Unit::ALL {
assert!(TIMING.slots(unit) >= 1, "{unit:?} has none of it");
}
}
#[test]
fn the_first_register_of_each_file_is_not_the_same_register() {
let (mut names, mut func, block) = empty();
mov(&mut func, &mut names, block, RAX, RDX);
vector(&mut func, &mut names, block, "addsd_rr", x86_64::xmm(0), x86_64::xmm(1));
bare(&mut func, &mut names, block, "ret");
assert_eq!(reg(RAX), reg(x86_64::xmm(0)), "and a register on its own does not say which");
schedule(&mut func, &names);
assert_eq!(
shape(&func, &names, block),
["addsd_rr", "mov_rr_64", "ret"],
"the addition is four cycles from the end and the move is one, and nothing joins them"
);
}
}