use crate::{rpo_dfs::RpoResult, vbyte};
const UNDEFINED: u32 = u32::MAX;
pub fn compute_dominators(
n: usize,
rpo: RpoResult,
gc_root_indices: &[u32],
inb_block_off: &[u64], inb_data: &[u8],
) -> std::io::Result<Vec<u32>> {
let vroot = n as u32;
let count = rpo.parent_pre.len();
debug_assert_eq!(
rpo.vertex.len(),
count,
"vertex must be rebuilt (rebuild_vertex) before compute_dominators"
);
let mut semi = vec![0u32; count];
let mut ancestor = vec![UNDEFINED; count]; let mut label = vec![0u32; count];
crate::trace::probe("dominator: after semi/ancestor/label alloc");
for i in 0..count {
semi[i] = i as u32;
label[i] = i as u32;
ancestor[i] = UNDEFINED;
}
crate::trace::probe("dominator: after init loop (semi/ancestor/label resident)");
let mut vr_adjacent = crate::bitset::Bitset::with_len(n + 1);
for &r in gc_root_indices {
vr_adjacent.set(r as usize);
}
let mut recovered_index: Option<Vec<u64>> = None;
let mut attempt = 0u32;
loop {
for i in 0..count {
semi[i] = i as u32;
label[i] = i as u32;
ancestor[i] = UNDEFINED;
}
let res = phase1(
count,
&rpo,
&vr_adjacent,
inb_block_off,
inb_data,
recovered_index.as_deref(),
&mut semi,
&mut ancestor,
&mut label,
);
match res {
Ok(()) => break,
Err(desync) => {
attempt += 1;
if attempt == 1 {
eprintln!(
"[dominator] inbound CSR decode desync at pre-order {} ({}); \
retrying (tier 1)",
desync.at, desync.why
);
continue;
} else if attempt == 2 {
eprintln!(
"[dominator] desync persisted at pre-order {} ({}); rebuilding \
exact offset index and retrying (tier 2)",
desync.at, desync.why
);
recovered_index = Some(build_exact_offsets(n, inb_data)?);
continue;
} else {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidData,
format!(
"inbound CSR decode desync at pre-order {} ({}); recovery \
exhausted, cannot compute a valid dominator tree",
desync.at, desync.why
),
));
}
}
}
}
drop(recovered_index);
let mut idom_pre = ancestor;
crate::trace::drop_vec(label);
idom_pre[0] = 0;
for i in 1..count {
let mut d = rpo.parent_pre[i];
while d > semi[i] {
d = idom_pre[d as usize];
}
idom_pre[i] = d;
}
crate::trace::drop_vec(semi);
crate::trace::probe("dominator: after drop(semi), before idom alloc");
let mut idom = vec![UNDEFINED; n + 1];
crate::trace::probe("dominator: after idom alloc");
idom[n] = vroot; for i in 1..count {
let node = rpo.vertex[i] as usize;
let dom_pre = idom_pre[i];
let dom_node = rpo.vertex[dom_pre as usize];
idom[node] = if dom_pre == 0 { vroot } else { dom_node };
}
Ok(idom)
}
struct DesyncErr {
at: usize, why: &'static str,
}
#[inline]
fn decode_checked(buf: &[u8], pos: usize) -> Option<(u32, usize)> {
if pos > buf.len() {
return None;
}
let (v, c) = vbyte::decode_one(&buf[pos..]);
if pos + c > buf.len() {
return None;
}
Some((v, c))
}
#[allow(clippy::too_many_arguments)]
fn phase1(
count: usize,
rpo: &RpoResult,
vr_adjacent: &crate::bitset::Bitset,
inb_block_off: &[u64],
inb_data: &[u8],
exact_offsets: Option<&[u64]>,
semi: &mut [u32],
ancestor: &mut [u32],
label: &mut [u32],
) -> Result<(), DesyncErr> {
let cnt_u32 = count as u32;
let mut chain: Vec<u32> = Vec::new();
for i in (1..count).rev() {
let w_node = rpo.vertex[i] as usize;
if vr_adjacent.get(w_node) {
let u = eval(0, ancestor, label, semi, &mut chain);
if semi[u as usize] < semi[i] {
semi[i] = semi[u as usize];
}
}
let mut pos = if let Some(off) = exact_offsets {
off[w_node] as usize
} else {
let block = w_node / crate::pass2::INB_BLOCK;
let mut p = inb_block_off[block] as usize;
for _ in (block * crate::pass2::INB_BLOCK)..w_node {
let (cnt, c0) = decode_checked(inb_data, p).ok_or(DesyncErr {
at: i,
why: "skip count ran off end",
})?;
p += c0;
for _ in 0..cnt {
let (_, c1) = decode_checked(inb_data, p).ok_or(DesyncErr {
at: i,
why: "skip delta ran off end",
})?;
p += c1;
}
}
p
};
let (cnt_w, c0) = decode_checked(inb_data, pos).ok_or(DesyncErr {
at: i,
why: "count ran off end",
})?;
pos += c0;
let mut prev: u32 = 0;
for _ in 0..cnt_w {
let (delta, consumed) = decode_checked(inb_data, pos).ok_or(DesyncErr {
at: i,
why: "delta ran off end",
})?;
pos += consumed;
let pv = prev.wrapping_add(delta);
prev = pv;
if pv >= cnt_u32 {
return Err(DesyncErr {
at: i,
why: "predecessor pre-order out of range",
});
}
let u = eval(pv, ancestor, label, semi, &mut chain);
if semi[u as usize] < semi[i] {
semi[i] = semi[u as usize];
}
}
link(i as u32, rpo.parent_pre[i], ancestor);
}
Ok(())
}
fn build_exact_offsets(n: usize, inb_data: &[u8]) -> std::io::Result<Vec<u64>> {
let mut offsets = Vec::with_capacity(n + 1);
let mut pos = 0usize;
for node in 0..n {
offsets.push(pos as u64);
let (cnt, c0) = decode_checked(inb_data, pos).ok_or_else(|| {
std::io::Error::new(
std::io::ErrorKind::InvalidData,
format!("CSR rebuild: count ran off end at node {node}"),
)
})?;
pos += c0;
for _ in 0..cnt {
let (_, c1) = decode_checked(inb_data, pos).ok_or_else(|| {
std::io::Error::new(
std::io::ErrorKind::InvalidData,
format!("CSR rebuild: delta ran off end at node {node}"),
)
})?;
pos += c1;
}
}
offsets.push(pos as u64); if pos != inb_data.len() {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidData,
format!(
"CSR rebuild consumed {} of {} bytes; framing is corrupt",
pos,
inb_data.len()
),
));
}
Ok(offsets)
}
fn eval(
v: u32,
ancestor: &mut [u32],
label: &mut [u32],
semi: &[u32],
chain: &mut Vec<u32>,
) -> u32 {
if ancestor[v as usize] == UNDEFINED {
return label[v as usize];
}
compress(v, ancestor, label, semi, chain);
label[v as usize]
}
fn compress(v: u32, ancestor: &mut [u32], label: &mut [u32], semi: &[u32], chain: &mut Vec<u32>) {
chain.clear();
let mut x = v;
while ancestor[ancestor[x as usize] as usize] != UNDEFINED {
chain.push(x);
x = ancestor[x as usize];
}
for &node in chain.iter().rev() {
let anc = ancestor[node as usize];
if semi[label[anc as usize] as usize] < semi[label[node as usize] as usize] {
label[node as usize] = label[anc as usize];
}
ancestor[node as usize] = ancestor[anc as usize];
}
}
fn link(w: u32, parent: u32, ancestor: &mut [u32]) {
ancestor[w as usize] = parent;
}
#[cfg(test)]
mod tests {
use super::*;
use crate::chunkvec::ChunkU32;
use crate::rpo_dfs::{rebuild_vertex, rpo_dfs};
fn make_fwd_tgt(v: Vec<u32>) -> ChunkU32 {
ChunkU32::from_vec(v)
}
fn build_inb(n: usize, preds: &[Vec<u32>], dfn: &[u32]) -> (Vec<u64>, Vec<u8>) {
use crate::pass2::INB_BLOCK;
let mut block_off = Vec::with_capacity(n / INB_BLOCK + 2);
let mut data = Vec::new();
for i in 0..n {
let mut pre: Vec<u32> = preds
.get(i)
.cloned()
.unwrap_or_default()
.into_iter()
.map(|node| dfn[node as usize])
.filter(|&p| p != u32::MAX)
.collect();
pre.sort_unstable();
pre.dedup();
if i % INB_BLOCK == 0 {
block_off.push(data.len() as u64);
}
vbyte::encode(pre.len() as u32, &mut data);
let mut prev = 0u32;
for &v in &pre {
vbyte::encode(v - prev, &mut data);
prev = v;
}
}
block_off.push(data.len() as u64);
(block_off, data)
}
#[test]
fn diamond_idom() {
let fwd_off = vec![0u32, 2, 3, 4, 4];
let fwd_tgt = make_fwd_tgt(vec![1u32, 2, 3, 3]);
let roots = vec![0u32];
let mut rpo = rpo_dfs(4, &roots, &fwd_off, &fwd_tgt);
rpo.vertex = rebuild_vertex(&rpo.dfn, rpo.parent_pre.len());
let preds = vec![vec![], vec![0u32], vec![0u32], vec![1u32, 2u32]];
let (inb_offsets, inb_data) = build_inb(4, &preds, &rpo.dfn);
let idom = compute_dominators(4, rpo, &roots, &inb_offsets, &inb_data).unwrap();
assert_eq!(idom[0], 4, "idom[0]=vroot");
assert_eq!(idom[1], 0, "idom[1]=0");
assert_eq!(idom[2], 0, "idom[2]=0");
assert_eq!(idom[3], 0, "idom[3]=0 (both paths through 0)");
}
#[test]
fn chain_idom() {
let fwd_off = vec![0u32, 1, 2, 2];
let fwd_tgt = make_fwd_tgt(vec![1u32, 2u32]);
let roots = vec![0u32];
let mut rpo = rpo_dfs(3, &roots, &fwd_off, &fwd_tgt);
rpo.vertex = rebuild_vertex(&rpo.dfn, rpo.parent_pre.len());
let preds = vec![vec![], vec![0u32], vec![1u32]];
let (inb_offsets, inb_data) = build_inb(3, &preds, &rpo.dfn);
let idom = compute_dominators(3, rpo, &roots, &inb_offsets, &inb_data).unwrap();
assert_eq!(idom[0], 3, "idom[0]=vroot");
assert_eq!(idom[1], 0);
assert_eq!(idom[2], 1);
}
#[test]
fn two_roots_no_shared_path() {
let fwd_off = vec![0u32, 0, 0];
let fwd_tgt = make_fwd_tgt(vec![]);
let roots = vec![0u32, 1u32];
let mut rpo = rpo_dfs(2, &roots, &fwd_off, &fwd_tgt);
rpo.vertex = rebuild_vertex(&rpo.dfn, rpo.parent_pre.len());
let preds = vec![vec![], vec![]];
let (inb_offsets, inb_data) = build_inb(2, &preds, &rpo.dfn);
let idom = compute_dominators(2, rpo, &roots, &inb_offsets, &inb_data).unwrap();
let vroot = 2u32;
assert_eq!(idom[0], vroot);
assert_eq!(idom[1], vroot);
}
#[test]
fn reconvergent_diamond_with_bypass() {
let fwd_off = vec![0u32, 2, 4, 5, 5];
let fwd_tgt = make_fwd_tgt(vec![1u32, 2, 2, 3, 3]);
let roots = vec![0u32];
let mut rpo = rpo_dfs(4, &roots, &fwd_off, &fwd_tgt);
rpo.vertex = rebuild_vertex(&rpo.dfn, rpo.parent_pre.len());
let preds = vec![vec![], vec![0u32], vec![0u32, 1u32], vec![1u32, 2u32]];
let (inb_offsets, inb_data) = build_inb(4, &preds, &rpo.dfn);
let idom = compute_dominators(4, rpo, &roots, &inb_offsets, &inb_data).unwrap();
assert_eq!(idom[0], 4);
assert_eq!(idom[1], 0);
assert_eq!(idom[2], 0, "2 dominated by 0 (direct edge bypasses 1)");
assert_eq!(idom[3], 0, "3 dominated by 0 (paths via 1 and via 2)");
}
#[test]
fn recovers_from_corrupt_block_offset() {
use crate::pass2::INB_BLOCK;
let n = INB_BLOCK * 3 + 5; let mut fwd_off = vec![0u32; n + 1];
let mut fwd_tgt_raw = Vec::new();
for i in 0..n {
fwd_off[i] = fwd_tgt_raw.len() as u32;
if i + 1 < n {
fwd_tgt_raw.push((i + 1) as u32);
}
}
fwd_off[n] = fwd_tgt_raw.len() as u32;
let fwd_tgt = make_fwd_tgt(fwd_tgt_raw.clone());
let roots = vec![0u32];
let mut rpo = rpo_dfs(n, &roots, &fwd_off, &fwd_tgt);
rpo.vertex = rebuild_vertex(&rpo.dfn, rpo.parent_pre.len());
let mut preds = vec![Vec::new(); n];
for i in 1..n {
preds[i] = vec![(i - 1) as u32];
}
let (good_off, inb_data) = build_inb(n, &preds, &rpo.dfn);
let fwd_tgt2 = make_fwd_tgt(fwd_tgt_raw.clone());
let mut rpo2 = rpo_dfs(n, &roots, &fwd_off, &fwd_tgt2);
rpo2.vertex = rebuild_vertex(&rpo2.dfn, rpo2.parent_pre.len());
let idom_good = compute_dominators(n, rpo2, &roots, &good_off, &inb_data).unwrap();
for i in 1..n {
assert_eq!(idom_good[i], (i - 1) as u32, "good chain idom[{i}]");
}
let mut bad_off = good_off.clone();
bad_off[1] = bad_off[1].wrapping_add(1); let fwd_tgt3 = make_fwd_tgt(fwd_tgt_raw);
let mut rpo3 = rpo_dfs(n, &roots, &fwd_off, &fwd_tgt3);
rpo3.vertex = rebuild_vertex(&rpo3.dfn, rpo3.parent_pre.len());
let idom_rec = compute_dominators(n, rpo3, &roots, &bad_off, &inb_data).unwrap();
for i in 1..n {
assert_eq!(idom_rec[i], (i - 1) as u32, "recovered chain idom[{i}]");
}
assert_eq!(idom_rec, idom_good, "recovery reproduces the correct tree");
}
#[test]
fn reconverge_below_gcroot_child_gets_vroot_idom() {
let fwd_off = vec![0u32, 1, 2, 2, 3];
let fwd_tgt = make_fwd_tgt(vec![2u32, 3u32, 2u32]); let roots = vec![0u32, 1u32]; let mut rpo = rpo_dfs(4, &roots, &fwd_off, &fwd_tgt);
rpo.vertex = rebuild_vertex(&rpo.dfn, rpo.parent_pre.len());
let preds = vec![vec![], vec![], vec![0u32, 3u32], vec![1u32]];
let (inb_offsets, inb_data) = build_inb(4, &preds, &rpo.dfn);
let idom = compute_dominators(4, rpo, &roots, &inb_offsets, &inb_data).unwrap();
let vroot = 4u32;
assert_eq!(idom[0], vroot, "idom(A)=vroot");
assert_eq!(idom[1], vroot, "idom(C)=vroot");
assert_eq!(idom[3], 1, "idom(D)=C");
assert_eq!(
idom[2], vroot,
"idom(S)=vroot (reconverges below two gc roots)"
);
}
}