use std::collections::{HashMap, HashSet};
use crate::vm::instruction::{CompiledFunction, Op, Reg};
#[derive(Clone, Copy, PartialEq, Eq)]
enum Role {
Def,
Use,
DefUse,
}
trait RegVisitor {
fn scalar(&mut self, r: &mut Reg, role: Role);
fn range(&mut self, start: &mut Reg, count: u16, role: Role);
fn members(&mut self, start: &mut Reg, count: u16, role: Role) {
if count == 1 {
self.scalar(start, role);
} else {
self.range(start, count, role);
}
}
}
fn visit_regs(op: &mut Op, functions: &[CompiledFunction], v: &mut dyn RegVisitor) {
use Role::{Def, DefUse, Use};
match op {
Op::LoadConst { dst, .. } => v.scalar(dst, Def),
Op::Move { dst, src } => {
v.scalar(src, Use);
v.scalar(dst, Def);
}
Op::EnsureOwned { reg } => v.scalar(reg, Role::DefUse),
Op::Add { dst, lhs, rhs }
| Op::Sub { dst, lhs, rhs }
| Op::Mul { dst, lhs, rhs }
| Op::Pow { dst, lhs, rhs }
| Op::Div { dst, lhs, rhs }
| Op::ExactDiv { dst, lhs, rhs }
| Op::FloorDiv { dst, lhs, rhs }
| Op::Mod { dst, lhs, rhs }
| Op::Lt { dst, lhs, rhs }
| Op::Gt { dst, lhs, rhs }
| Op::LtEq { dst, lhs, rhs }
| Op::GtEq { dst, lhs, rhs }
| Op::Eq { dst, lhs, rhs }
| Op::NotEq { dst, lhs, rhs }
| Op::ApproxEq { dst, lhs, rhs }
| Op::Concat { dst, lhs, rhs }
| Op::SeqConcat { dst, lhs, rhs }
| Op::BitXor { dst, lhs, rhs }
| Op::BitAnd { dst, lhs, rhs }
| Op::BitOr { dst, lhs, rhs }
| Op::Shl { dst, lhs, rhs }
| Op::Shr { dst, lhs, rhs }
| Op::UnionOp { dst, lhs, rhs }
| Op::IntersectOp { dst, lhs, rhs } => {
v.scalar(lhs, Use);
v.scalar(rhs, Use);
v.scalar(dst, Def);
}
Op::AddAssign { dst, src } => {
v.scalar(src, Use);
v.scalar(dst, DefUse);
}
Op::DivPow2 { dst, lhs, .. } | Op::MagicDivU { dst, lhs, .. } => {
v.scalar(lhs, Use);
v.scalar(dst, Def);
}
Op::Not { dst, src } | Op::DeepClone { dst, src } => {
v.scalar(src, Use);
v.scalar(dst, Def);
}
Op::Jump { .. } => {}
Op::JumpIfFalse { cond, .. } | Op::JumpIfTrue { cond, .. } => {
v.scalar(cond, Use)
}
Op::Call { dst, args_start, arg_count, .. } => {
v.members(args_start, *arg_count, Use);
v.scalar(dst, Def);
}
Op::CallBuiltin { dst, args_start, arg_count, .. } => {
v.members(args_start, *arg_count, Use);
v.scalar(dst, Def);
}
Op::CallValue { dst, callee, args_start, arg_count, .. } => {
v.scalar(callee, Use);
v.members(args_start, *arg_count, Use);
v.scalar(dst, Def);
}
Op::MakeClosure { dst, func, locals_start } => {
let cap_n = functions.get(*func as usize).map_or(0, |f| f.captures.len() as u16);
v.members(locals_start, cap_n, Use);
v.scalar(dst, Def);
}
Op::CheckPolicy { subject, object, .. } => {
v.scalar(subject, Use);
if *object != Reg::MAX {
v.scalar(object, Use);
}
}
Op::ListPushField { obj, src, .. } => {
v.scalar(obj, Use);
v.scalar(src, Use);
}
Op::GlobalGet { dst, .. } => v.scalar(dst, Def),
Op::GlobalSet { src, .. } => v.scalar(src, Use),
Op::Return { src } => v.scalar(src, Use),
Op::ReturnNothing => {}
Op::NewList { dst, start, count } | Op::NewTuple { dst, start, count } => {
v.members(start, *count, Use);
v.scalar(dst, Def);
}
Op::NewEmptyList { dst }
| Op::NewEmptyListI32 { dst }
| Op::NewEmptySet { dst }
| Op::NewEmptyMap { dst }
| Op::LoadToday { dst }
| Op::LoadNow { dst }
| Op::NewStruct { dst, .. }
| Op::NewCrdt { dst, .. }
| Op::Args { dst }
| Op::ChanNew { dst, .. }
| Op::SelectWait { dst_arm: dst } => v.scalar(dst, Def),
Op::NewRange { dst, start, end } => {
v.scalar(start, Use);
v.scalar(end, Use);
v.scalar(dst, Def);
}
Op::ListPush { list, value } => {
v.scalar(list, Use);
v.scalar(value, Use);
}
Op::SetAdd { set, value } => {
v.scalar(set, Use);
v.scalar(value, Use);
}
Op::RemoveFrom { collection, value } => {
v.scalar(collection, Use);
v.scalar(value, Use);
}
Op::Contains { dst, collection, value } => {
v.scalar(collection, Use);
v.scalar(value, Use);
v.scalar(dst, Def);
}
Op::SetIndex { collection, index, value } | Op::SetIndexUnchecked { collection, index, value } => {
v.scalar(collection, Use);
v.scalar(index, Use);
v.scalar(value, Use);
}
Op::Index { dst, collection, index } | Op::IndexUnchecked { dst, collection, index } => {
v.scalar(collection, Use);
v.scalar(index, Use);
v.scalar(dst, Def);
}
Op::RegionBoundsGuard { array, bound, iv, .. } => {
v.scalar(array, Use);
v.scalar(bound, Use);
v.scalar(iv, Use);
}
Op::Length { dst, collection } => {
v.scalar(collection, Use);
v.scalar(dst, Def);
}
Op::FormatValue { dst, src, .. } => {
v.scalar(src, Use);
v.scalar(dst, Def);
}
Op::SliceOp { dst, collection, start, end } => {
v.scalar(collection, Use);
v.scalar(start, Use);
v.scalar(end, Use);
v.scalar(dst, Def);
}
Op::StructInsert { obj, value, .. } => {
v.scalar(obj, Use);
v.scalar(value, Use);
}
Op::GetField { dst, obj, .. } => {
v.scalar(obj, Use);
v.scalar(dst, Def);
}
Op::NewInductive { dst, args_start, count, .. } => {
v.members(args_start, *count, Use);
v.scalar(dst, Def);
}
Op::TestArm { dst, target, .. } | Op::BindArm { dst, target, .. } => {
v.scalar(target, Use);
v.scalar(dst, Def);
}
Op::CrdtBump { obj, amount, .. } => {
v.scalar(obj, Use);
v.scalar(amount, Use);
}
Op::CrdtMerge { target, source } => {
v.scalar(target, Use);
v.scalar(source, Use);
}
Op::CrdtAppend { seq, value } => {
v.scalar(seq, Use);
v.scalar(value, Use);
}
Op::CrdtResolve { obj, value, .. } => {
v.scalar(obj, Use);
v.scalar(value, Use);
}
Op::IterPrepare { iterable } => v.scalar(iterable, Use),
Op::IterNext { dst, .. } => v.scalar(dst, Def),
Op::IterPop => {}
Op::ListPop { list, dst } => {
v.scalar(list, Use);
v.scalar(dst, Def);
}
Op::Sleep { duration } => v.scalar(duration, Use),
Op::DestructureTuple { src, start, count } => {
v.scalar(src, Use);
v.members(start, *count, Def);
}
Op::Show { src } => v.scalar(src, Use),
Op::ChanSend { chan, val } => {
v.scalar(chan, Use);
v.scalar(val, Use);
}
Op::ChanRecv { dst, chan } | Op::ChanTryRecv { dst, chan } => {
v.scalar(chan, Use);
v.scalar(dst, Def);
}
Op::ChanTrySend { dst, chan, val } => {
v.scalar(chan, Use);
v.scalar(val, Use);
v.scalar(dst, Def);
}
Op::ChanClose { chan } => v.scalar(chan, Use),
Op::Spawn { args_start, arg_count, .. } => v.members(args_start, *arg_count, Use),
Op::SpawnHandle { dst, args_start, arg_count, .. } => {
v.members(args_start, *arg_count, Use);
v.scalar(dst, Def);
}
Op::TaskAwait { dst, handle } => {
v.scalar(handle, Use);
v.scalar(dst, Def);
}
Op::TaskAbort { handle } => v.scalar(handle, Use),
Op::SelectArmRecv { chan, var } => {
v.scalar(chan, Use);
v.scalar(var, Def);
}
Op::SelectArmTimeout { ticks } => v.scalar(ticks, Use),
Op::NetConnect { url } => v.scalar(url, Use),
Op::NetListen { topic } => v.scalar(topic, Use),
Op::NetSend { to, msg } => {
v.scalar(to, Use);
v.scalar(msg, Use);
}
Op::NetStream { to, values } => {
v.scalar(to, Use);
v.scalar(values, Use);
}
Op::NetAwait { dst, from, .. } => {
v.scalar(from, Use);
v.scalar(dst, Def);
}
Op::NetMakePeer { dst, addr } => {
v.scalar(addr, Use);
v.scalar(dst, Def);
}
Op::NetSync { dst, topic } => {
v.scalar(topic, Use);
v.scalar(dst, DefUse);
}
Op::FailWith { .. } | Op::Halt => {}
}
}
pub(crate) fn op_def_uses(op: &Op, functions: &[CompiledFunction]) -> (Vec<Reg>, Vec<Reg>) {
struct Collect {
defs: Vec<Reg>,
uses: Vec<Reg>,
}
impl RegVisitor for Collect {
fn scalar(&mut self, r: &mut Reg, role: Role) {
match role {
Role::Def => self.defs.push(*r),
Role::Use => self.uses.push(*r),
Role::DefUse => {
self.defs.push(*r);
self.uses.push(*r);
}
}
}
fn range(&mut self, start: &mut Reg, count: u16, role: Role) {
for i in 0..count {
self.scalar(&mut start.wrapping_add(i), role);
}
}
}
let mut tmp = op.clone();
let mut c = Collect { defs: Vec::new(), uses: Vec::new() };
visit_regs(&mut tmp, functions, &mut c);
(c.defs, c.uses)
}
fn successors(ops: &[Op], pc: usize) -> Vec<usize> {
let n = ops.len();
let fallthrough = |out: &mut Vec<usize>| {
if pc + 1 < n {
out.push(pc + 1);
}
};
let mut out = Vec::new();
match ops[pc] {
Op::Jump { target } => out.push(target),
Op::JumpIfFalse { target, .. } | Op::JumpIfTrue { target, .. } => {
out.push(target);
fallthrough(&mut out);
}
Op::IterNext { exit, .. } => {
out.push(exit);
fallthrough(&mut out);
}
Op::Return { .. } | Op::ReturnNothing | Op::Halt | Op::FailWith { .. } => {}
_ => fallthrough(&mut out),
}
out
}
struct UnionFind {
parent: Vec<u32>,
}
impl UnionFind {
fn new(n: usize) -> Self {
UnionFind { parent: (0..n as u32).collect() }
}
fn find(&mut self, x: u32) -> u32 {
let mut r = x;
while self.parent[r as usize] != r {
r = self.parent[r as usize];
}
let mut c = x;
while self.parent[c as usize] != r {
let next = self.parent[c as usize];
self.parent[c as usize] = r;
c = next;
}
r
}
fn union(&mut self, a: u32, b: u32) {
let (ra, rb) = (self.find(a), self.find(b));
if ra != rb {
let (lo, hi) = (ra.min(rb), ra.max(rb));
self.parent[hi as usize] = lo;
}
}
}
fn op_reg_roles(op: &Op, functions: &[CompiledFunction]) -> (Vec<(Reg, Role)>, Vec<(u32, Role)>) {
struct Collect {
roles: Vec<(Reg, Role)>,
range_idx: Vec<(u32, Role)>,
}
impl RegVisitor for Collect {
fn scalar(&mut self, r: &mut Reg, role: Role) {
self.roles.push((*r, role));
}
fn range(&mut self, start: &mut Reg, count: u16, role: Role) {
for i in 0..count {
let idx = self.roles.len() as u32;
self.roles.push((start.wrapping_add(i), role));
self.range_idx.push((idx, role));
}
}
}
let mut tmp = op.clone();
let mut c = Collect { roles: Vec::new(), range_idx: Vec::new() };
visit_regs(&mut tmp, functions, &mut c);
(c.roles, c.range_idx)
}
fn op_use_ranges(op: &Op, functions: &[CompiledFunction]) -> Vec<(Reg, u16)> {
struct Collect {
ranges: Vec<(Reg, u16)>,
}
impl RegVisitor for Collect {
fn scalar(&mut self, _r: &mut Reg, _role: Role) {}
fn range(&mut self, start: &mut Reg, count: u16, role: Role) {
if matches!(role, Role::Use | Role::DefUse) {
self.ranges.push((*start, count));
}
}
}
let mut tmp = op.clone();
let mut c = Collect { ranges: Vec::new() };
visit_regs(&mut tmp, functions, &mut c);
c.ranges
}
fn use_range_start(op: &Op, functions: &[CompiledFunction]) -> Option<Reg> {
op_use_ranges(op, functions).first().map(|&(s, _)| s)
}
fn set_use_range_start(op: &mut Op, functions: &[CompiledFunction], base: Reg) {
struct SetStart {
base: Reg,
done: bool,
}
impl RegVisitor for SetStart {
fn scalar(&mut self, _r: &mut Reg, _role: Role) {}
fn range(&mut self, start: &mut Reg, _count: u16, role: Role) {
if !self.done && matches!(role, Role::Use | Role::DefUse) {
*start = self.base;
self.done = true;
}
}
}
let mut s = SetStart { base, done: false };
visit_regs(op, functions, &mut s);
}
fn remap_target(op: &mut Op, new_index: &[usize]) {
match op {
Op::Jump { target } | Op::JumpIfFalse { target, .. } | Op::JumpIfTrue { target, .. } => {
*target = new_index[*target];
}
Op::IterNext { exit, .. } => *exit = new_index[*exit],
_ => {}
}
}
fn materialize_ranges(
ops: &[Op],
num_regs: u32,
targets: &[(usize, Reg, u16)],
functions: &[CompiledFunction],
) -> (Vec<Op>, u32) {
let mut sorted: Vec<(usize, Reg, u16)> = targets.to_vec();
sorted.sort_unstable();
let mut block_of: HashMap<usize, (Reg, u16)> = HashMap::new();
let mut next = num_regs;
for &(pc, _start, count) in &sorted {
block_of.entry(pc).or_insert_with(|| {
let base = next as Reg;
next += u32::from(count);
(base, count)
});
}
let mut new_ops: Vec<Op> = Vec::with_capacity(ops.len() + block_of.len());
let mut new_index: Vec<usize> = vec![0; ops.len()];
for (pc, op) in ops.iter().enumerate() {
new_index[pc] = new_ops.len();
if let Some(&(base, count)) = block_of.get(&pc) {
let old_start = use_range_start(op, functions).expect("materialized op has a use range");
for i in 0..count {
new_ops.push(Op::Move { dst: base.wrapping_add(i), src: old_start.wrapping_add(i) });
}
let mut relocated = op.clone();
set_use_range_start(&mut relocated, functions, base);
new_ops.push(relocated);
} else {
new_ops.push(op.clone());
}
}
for op in &mut new_ops {
remap_target(op, &new_index);
}
(new_ops, next)
}
pub(crate) fn split_registers(
ops: &[Op],
num_regs: u32,
num_params: u32,
functions: &[CompiledFunction],
) -> (Vec<Op>, u32) {
let n = ops.len();
if n == 0 {
return (ops.to_vec(), num_regs);
}
let pinned: HashSet<Reg> = (0..num_params as u16).collect();
let mut range_idx_at: Vec<Vec<(u32, Role)>> = Vec::with_capacity(n);
let roles: Vec<Vec<(Reg, Role)>> = ops
.iter()
.map(|op| {
let (r, ri) = op_reg_roles(op, functions);
range_idx_at.push(ri);
r
})
.collect();
let mut def_sites: Vec<(usize, Reg)> = Vec::new();
let mut sites_at: Vec<Vec<u32>> = vec![Vec::new(); n]; let mut defs_at: Vec<Vec<Reg>> = vec![Vec::new(); n];
for (pc, rs) in roles.iter().enumerate() {
for &(r, role) in rs {
if matches!(role, Role::Def | Role::DefUse) {
sites_at[pc].push(def_sites.len() as u32);
def_sites.push((pc, r));
defs_at[pc].push(r);
}
}
}
let num_sites = def_sites.len();
if num_sites == 0 {
return (ops.to_vec(), num_regs);
}
let mut sites_of_reg: HashMap<Reg, Vec<u32>> = HashMap::new();
for (i, &(_, r)) in def_sites.iter().enumerate() {
sites_of_reg.entry(r).or_default().push(i as u32);
}
let mut preds: Vec<Vec<usize>> = vec![Vec::new(); n];
for pc in 0..n {
for s in successors(ops, pc) {
if s < n {
preds[s].push(pc);
}
}
}
let mut reach_in: Vec<HashSet<u32>> = vec![HashSet::new(); n];
let mut reach_out: Vec<HashSet<u32>> = vec![HashSet::new(); n];
let mut changed = true;
while changed {
changed = false;
for pc in 0..n {
let mut new_in: HashSet<u32> = HashSet::new();
for &p in &preds[pc] {
new_in.extend(&reach_out[p]);
}
let mut new_out = new_in.clone();
for &r in &defs_at[pc] {
if let Some(killed) = sites_of_reg.get(&r) {
for &k in killed {
new_out.remove(&k);
}
}
}
new_out.extend(&sites_at[pc]);
if new_in != reach_in[pc] || new_out != reach_out[pc] {
reach_in[pc] = new_in;
reach_out[pc] = new_out;
changed = true;
}
}
}
let mut uf = UnionFind::new(num_sites);
let mut unsplittable: HashSet<Reg> = HashSet::new();
let mut use_web: HashMap<(usize, u32), u32> = HashMap::new();
for (pc, rs) in roles.iter().enumerate() {
let mut self_site: HashMap<Reg, u32> = HashMap::new();
for &s in &sites_at[pc] {
self_site.insert(def_sites[s as usize].1, s);
}
for (idx, &(r, role)) in rs.iter().enumerate() {
if matches!(role, Role::Use | Role::DefUse) {
let reaching: Vec<u32> =
reach_in[pc].iter().copied().filter(|&s| def_sites[s as usize].1 == r).collect();
if reaching.is_empty() {
if !pinned.contains(&r) {
unsplittable.insert(r);
}
continue;
}
let first = reaching[0];
for &s in &reaching[1..] {
uf.union(first, s);
}
if let Role::DefUse = role {
if let Some(&own) = self_site.get(&r) {
uf.union(first, own);
}
}
use_web.insert((pc, idx as u32), first);
}
}
}
for w in use_web.values_mut() {
*w = uf.find(*w);
}
let mut webs_of_reg: HashMap<Reg, Vec<u32>> = HashMap::new();
for i in 0..num_sites {
let (_, r) = def_sites[i];
let w = uf.find(i as u32);
let entry = webs_of_reg.entry(r).or_default();
if !entry.contains(&w) {
entry.push(w);
}
}
let recycled = |r: Reg| webs_of_reg.get(&r).map_or(false, |w| w.len() >= 2);
let mut to_materialize: Vec<(usize, Reg, u16)> = Vec::new();
for (pc, op) in ops.iter().enumerate() {
for (start, count) in op_use_ranges(op, functions) {
if (0..count).any(|i| recycled(start.wrapping_add(i))) {
to_materialize.push((pc, start, count));
}
}
}
let extra: u32 = to_materialize.iter().map(|&(_, _, c)| u32::from(c)).sum();
if !to_materialize.is_empty() && num_regs + extra <= u32::from(u16::MAX) + 1 {
let (new_ops, new_regs) = materialize_ranges(ops, num_regs, &to_materialize, functions);
return split_registers(&new_ops, new_regs, num_params, functions);
}
let mut anchored_webs: HashSet<u32> = HashSet::new();
for (pc, ris) in range_idx_at.iter().enumerate() {
for &(idx, role) in ris {
match role {
Role::Use | Role::DefUse => {
if let Some(&w) = use_web.get(&(pc, idx)) {
anchored_webs.insert(w); }
}
Role::Def => {
let r = roles[pc][idx as usize].0;
if let Some(&s) = sites_at[pc].iter().find(|&&s| def_sites[s as usize].1 == r) {
anchored_webs.insert(uf.find(s));
}
}
}
}
}
let fresh_count = |r: Reg| -> u32 {
let webs = &webs_of_reg[&r];
let anchored = webs.iter().filter(|w| anchored_webs.contains(w)).count();
let free = webs.len() - anchored;
if anchored >= 1 { free as u32 } else { free.saturating_sub(1) as u32 }
};
let splittable = |r: Reg| {
!pinned.contains(&r) && !unsplittable.contains(&r) && webs_of_reg[&r].len() >= 2 && fresh_count(r) >= 1
};
let mut regs: Vec<Reg> = webs_of_reg.keys().copied().filter(|&r| splittable(r)).collect();
if regs.is_empty() {
return (ops.to_vec(), num_regs);
}
regs.sort_unstable();
let extra: u32 = regs.iter().map(|&r| fresh_count(r)).sum();
if num_regs + extra > u16::MAX as u32 + 1 {
return (ops.to_vec(), num_regs); }
let mut local_of_web: HashMap<u32, Reg> = HashMap::new();
let mut next_local = num_regs;
for &r in ®s {
let mut claimed = false;
for &w in &webs_of_reg[&r] {
if anchored_webs.contains(&w) {
local_of_web.insert(w, r);
claimed = true;
}
}
for &w in &webs_of_reg[&r] {
if !anchored_webs.contains(&w) {
if claimed {
local_of_web.insert(w, next_local as Reg);
next_local += 1;
} else {
local_of_web.insert(w, r);
claimed = true;
}
}
}
}
let new_ops = rename_ops(ops, &local_of_web, &use_web, &mut uf, &def_sites, functions);
(new_ops, next_local)
}
fn rename_ops(
ops: &[Op],
local_of_web: &HashMap<u32, Reg>,
use_web: &HashMap<(usize, u32), u32>,
uf: &mut UnionFind,
def_sites: &[(usize, Reg)],
functions: &[CompiledFunction],
) -> Vec<Op> {
let mut def_local_at: HashMap<(usize, Reg), Reg> = HashMap::new();
for (i, &(pc, r)) in def_sites.iter().enumerate() {
let w = uf.find(i as u32);
if let Some(&l) = local_of_web.get(&w) {
def_local_at.insert((pc, r), l);
}
}
struct Rename<'a> {
pc: usize,
op_idx: u32,
local_of_web: &'a HashMap<u32, Reg>,
use_web: &'a HashMap<(usize, u32), u32>,
def_local_at: &'a HashMap<(usize, Reg), Reg>,
}
impl RegVisitor for Rename<'_> {
fn scalar(&mut self, r: &mut Reg, role: Role) {
match role {
Role::Use => {
if let Some(&w) = self.use_web.get(&(self.pc, self.op_idx)) {
if let Some(&l) = self.local_of_web.get(&w) {
*r = l;
}
}
}
Role::Def | Role::DefUse => {
if let Some(&l) = self.def_local_at.get(&(self.pc, *r)) {
*r = l;
}
}
}
self.op_idx += 1;
}
fn range(&mut self, _start: &mut Reg, count: u16, _role: Role) {
self.op_idx += u32::from(count);
}
}
let mut out = Vec::with_capacity(ops.len());
for (pc, op) in ops.iter().enumerate() {
let mut op = op.clone();
let mut rn = Rename { pc, op_idx: 0, local_of_web, use_web, def_local_at: &def_local_at };
visit_regs(&mut op, functions, &mut rn);
out.push(op);
}
out
}
#[cfg(test)]
mod tests {
use super::*;
fn fmt(ops: &[Op]) -> Vec<String> {
ops.iter().map(|o| format!("{o:?}")).collect()
}
fn split(ops: &[Op], num_regs: u32, num_params: u32) -> (Vec<Op>, u32) {
split_registers(ops, num_regs, num_params, &[])
}
#[test]
fn identity_when_every_register_is_single_web() {
let ops = vec![
Op::LoadConst { dst: 0, idx: 0 },
Op::LoadConst { dst: 1, idx: 1 },
Op::Add { dst: 2, lhs: 0, rhs: 1 },
Op::Show { src: 2 },
Op::Halt,
];
let (out, n) = split(&ops, 3, 0);
assert_eq!(fmt(&out), fmt(&ops), "single-web program must be untouched");
assert_eq!(n, 3, "no locals added");
}
#[test]
fn disjoint_ranges_of_one_register_split_into_separate_locals() {
let ops = vec![
Op::LoadConst { dst: 1, idx: 0 },
Op::Show { src: 1 },
Op::LoadConst { dst: 1, idx: 1 },
Op::Show { src: 1 },
Op::Halt,
];
let (out, n) = split(&ops, 2, 0);
assert_eq!(n, 3, "one extra local for the second web");
let got = fmt(&out);
assert_eq!(got[0], format!("{:?}", Op::LoadConst { dst: 1, idx: 0 }), "first web keeps reg 1");
assert_eq!(got[1], format!("{:?}", Op::Show { src: 1 }));
assert_eq!(got[2], format!("{:?}", Op::LoadConst { dst: 2, idx: 1 }), "second web → local 2");
assert_eq!(got[3], format!("{:?}", Op::Show { src: 2 }), "its use follows");
}
#[test]
fn loop_carried_accumulator_is_one_web_not_split() {
let ops = vec![
Op::LoadConst { dst: 0, idx: 0 }, Op::LoadConst { dst: 1, idx: 1 }, Op::Lt { dst: 2, lhs: 0, rhs: 1 }, Op::JumpIfFalse { cond: 2, target: 6 },
Op::AddAssign { dst: 0, src: 1 }, Op::Jump { target: 2 }, Op::Show { src: 0 }, Op::Halt,
];
let (out, n) = split(&ops, 3, 0);
assert_eq!(fmt(&out), fmt(&ops), "loop-carried accumulator must not split");
assert_eq!(n, 3);
}
#[test]
fn parameters_are_pinned_and_never_split() {
let ops = vec![
Op::Show { src: 0 },
Op::LoadConst { dst: 0, idx: 0 },
Op::Show { src: 0 },
Op::Return { src: 0 },
];
let (out, n) = split(&ops, 1, 1); assert_eq!(fmt(&out), fmt(&ops), "a parameter register is never renamed");
assert_eq!(n, 1);
}
#[test]
fn split_is_deterministic_across_runs() {
let ops = vec![
Op::LoadConst { dst: 0, idx: 0 }, Op::JumpIfFalse { cond: 0, target: 4 },
Op::LoadConst { dst: 1, idx: 1 }, Op::Jump { target: 5 },
Op::LoadConst { dst: 1, idx: 2 }, Op::Move { dst: 2, src: 1 }, Op::LoadConst { dst: 1, idx: 3 }, Op::Return { src: 1 }, ];
let (first, first_n) = split(&ops, 3, 0);
assert_eq!(first_n, 4, "the second web of reg1 splits to one new local");
let got = fmt(&first);
assert_eq!(got[6], format!("{:?}", Op::LoadConst { dst: 3, idx: 3 }), "W2 def → local 3");
assert_eq!(got[7], format!("{:?}", Op::Return { src: 3 }), "W2 use → local 3");
assert_eq!(got[2], format!("{:?}", Op::LoadConst { dst: 1, idx: 1 }), "W def keeps reg 1");
assert_eq!(got[5], format!("{:?}", Op::Move { dst: 2, src: 1 }), "W use keeps reg 1");
for _ in 0..64 {
let (again, again_n) = split(&ops, 3, 0);
assert_eq!(fmt(&again), got, "splitting must be deterministic run-to-run");
assert_eq!(again_n, first_n);
}
}
#[test]
fn recycled_range_member_across_two_calls_is_materialized_and_split() {
let ops = vec![
Op::LoadConst { dst: 2, idx: 0 }, Op::LoadConst { dst: 3, idx: 1 },
Op::Call { dst: 0, func: 0, args_start: 2, arg_count: 2 }, Op::LoadConst { dst: 1, idx: 2 },
Op::LoadConst { dst: 2, idx: 3 }, Op::Call { dst: 0, func: 0, args_start: 1, arg_count: 2 }, Op::Halt,
];
let (out, n) = split(&ops, 4, 0);
let calls: Vec<usize> =
out.iter().enumerate().filter(|(_, o)| matches!(o, Op::Call { .. })).map(|(i, _)| i).collect();
assert_eq!(calls.len(), 2, "both calls survive");
for &ci in &calls {
let Op::Call { args_start, arg_count, .. } = out[ci] else { unreachable!() };
assert!(args_start >= 4, "call args relocated to a fresh block, got {args_start}");
for j in 0..arg_count {
let mv = &out[ci - arg_count as usize + j as usize];
assert!(
matches!(mv, Op::Move { dst, .. } if *dst == args_start + j),
"arg slot {} populated by a preceding Move, got {mv:?}", args_start + j
);
}
}
let dsts: Vec<Reg> = out
.iter()
.filter_map(|o| match o {
Op::LoadConst { dst, idx } if *idx == 0 || *idx == 3 => Some(*dst),
_ => None,
})
.collect();
assert_eq!(dsts.len(), 2);
assert_ne!(dsts[0], dsts[1], "the reused register was split into two locals");
assert!(n > 4, "register file grew for the fresh blocks and the split web");
}
#[test]
fn single_web_range_members_are_not_materialized() {
let ops = vec![
Op::LoadConst { dst: 1, idx: 0 },
Op::LoadConst { dst: 2, idx: 1 },
Op::Call { dst: 0, func: 0, args_start: 1, arg_count: 2 },
Op::Halt,
];
let (out, n) = split(&ops, 3, 0);
assert_eq!(fmt(&out), fmt(&ops), "no reused range member ⇒ no materialization");
assert_eq!(n, 3);
}
#[test]
fn materialization_remaps_jump_targets() {
let ops = vec![
Op::LoadConst { dst: 2, idx: 0 }, Op::LoadConst { dst: 3, idx: 1 },
Op::Call { dst: 0, func: 0, args_start: 2, arg_count: 2 }, Op::LoadConst { dst: 2, idx: 2 }, Op::Jump { target: 6 }, Op::LoadConst { dst: 9, idx: 3 }, Op::Call { dst: 0, func: 0, args_start: 2, arg_count: 2 }, Op::Halt,
];
let (out, _n) = split(&ops, 4, 0);
let Some(Op::Jump { target }) =
out.iter().find(|o| matches!(o, Op::Jump { .. })).copied()
else {
panic!("jump survived materialization");
};
assert!(matches!(out[target], Op::Move { .. }), "jump lands on the call's arg setup, got {:?}", out[target]);
let next_call = out[target..].iter().find(|o| matches!(o, Op::Call { .. }));
assert!(
matches!(next_call, Some(Op::Call { args_start, .. }) if *args_start >= 4),
"the targeted call reads a materialized block, got {next_call:?}"
);
}
#[test]
fn materialization_is_deterministic() {
let ops = vec![
Op::LoadConst { dst: 2, idx: 0 },
Op::LoadConst { dst: 3, idx: 1 },
Op::Call { dst: 0, func: 0, args_start: 2, arg_count: 2 },
Op::LoadConst { dst: 1, idx: 2 },
Op::LoadConst { dst: 2, idx: 3 },
Op::Call { dst: 0, func: 0, args_start: 1, arg_count: 2 },
Op::Halt,
];
let (first, first_n) = split(&ops, 4, 0);
let got = fmt(&first);
for _ in 0..64 {
let (again, again_n) = split(&ops, 4, 0);
assert_eq!(fmt(&again), got, "materialization must be deterministic run-to-run");
assert_eq!(again_n, first_n);
}
}
}