use vyre::ir::Program;
use vyre_primitives::graph::csr_backward_traverse::csr_backward_traverse;
use vyre_primitives::graph::program_graph::ProgramGraphShape;
use vyre_primitives::predicate::edge_kind;
use crate::region::{reparent_program_children, wrap_anonymous};
const OP_ID: &str = "vyre-libs::security::dominator_tree";
#[must_use]
pub fn dominator_tree(shape: ProgramGraphShape, frontier_in: &str, frontier_out: &str) -> Program {
crate::security::assert_security_inputs(
OP_ID,
shape.node_count,
&[("frontier_in", frontier_in), ("frontier_out", frontier_out)],
);
let primitive = csr_backward_traverse(shape, frontier_in, frontier_out, edge_kind::DOMINANCE);
Program::wrapped(
primitive.buffers().to_vec(),
primitive.workgroup_size(),
vec![wrap_anonymous(
OP_ID,
reparent_program_children(&primitive, OP_ID),
)],
)
}
#[must_use]
#[cfg(test)]
pub(crate) fn cpu_dominator_sets(
num_nodes: u32,
entry: u32,
edges: &[(u32, u32)],
) -> Vec<Vec<u32>> {
let idoms = vyre_primitives::graph::dominator_tree::cpu_ref(num_nodes, entry, edges);
vyre_primitives::graph::dominator_tree::idoms_to_dominator_sets(&idoms, num_nodes)
}
inventory::submit! {
crate::harness::OpEntry {
id: OP_ID,
build: || dominator_tree(ProgramGraphShape::new(4, 4), "fin", "fout"),
test_inputs: Some(|| {
let to_bytes = vyre_primitives::wire::pack_u32_slice;
vec![vec![
to_bytes(&[0, 0, 0, 0]), to_bytes(&[0, 2, 3, 4, 4]), to_bytes(&[1, 2, 3, 3]), to_bytes(&[
edge_kind::DOMINANCE,
edge_kind::DOMINANCE,
edge_kind::DOMINANCE,
edge_kind::DOMINANCE,
]), to_bytes(&[0, 0, 0, 0]), to_bytes(&[0b1000]), to_bytes(&[0b1000]), ]]
}),
expected_output: Some(|| {
let to_bytes = vyre_primitives::wire::pack_u32_slice;
vec![vec![to_bytes(&[0b1110])]]
}),
category: Some("security"),
}
}
inventory::submit! {
crate::harness::ConvergenceContract {
op_id: OP_ID,
max_iterations: 4096,
}
}
#[cfg(test)]
mod tests {
use super::*;
use vyre_primitives::graph::csr_backward_traverse::cpu_ref;
fn diamond_dominance_tree() -> (u32, Vec<u32>, Vec<u32>, Vec<u32>) {
let node_count = 4;
let edge_offsets = vec![0, 2, 3, 4, 4];
let edge_targets = vec![1, 2, 3, 3];
let edge_kind_mask = vec![edge_kind::DOMINANCE; 4];
(node_count, edge_offsets, edge_targets, edge_kind_mask)
}
#[test]
fn cpu_dominator_sets_linear_chain() {
let edges = &[(0, 1), (1, 2), (2, 3)];
let dom = cpu_dominator_sets(4, 0, edges);
assert_eq!(dom[0], vec![0]);
assert_eq!(dom[1], vec![0, 1]);
assert_eq!(dom[2], vec![0, 1, 2]);
assert_eq!(dom[3], vec![0, 1, 2, 3]);
}
#[test]
fn cpu_dominator_sets_diamond() {
let edges = &[(0, 1), (0, 2), (1, 3), (2, 3)];
let dom = cpu_dominator_sets(4, 0, edges);
assert_eq!(dom[0], vec![0]);
assert_eq!(dom[1], vec![0, 1]);
assert_eq!(dom[2], vec![0, 2]);
assert_eq!(dom[3], vec![0, 3]);
}
#[test]
fn cpu_dominator_sets_while_loop() {
let edges = &[(0, 1), (1, 2), (2, 1), (1, 3)];
let dom = cpu_dominator_sets(4, 0, edges);
assert_eq!(dom[0], vec![0]);
assert_eq!(dom[1], vec![0, 1]);
assert_eq!(dom[2], vec![0, 1, 2]);
assert_eq!(dom[3], vec![0, 1, 3]);
}
#[test]
fn dominator_tree_backward_step_reaches_ancestors() {
let (node_count, offsets, targets, masks) = diamond_dominance_tree();
let frontier_in = vec![0b1000]; let out = cpu_ref(
node_count,
&offsets,
&targets,
&masks,
&frontier_in,
edge_kind::DOMINANCE,
);
assert_eq!(out[0], 0b0110, "backward from 3 must reach 1 and 2");
}
#[test]
fn dominator_tree_program_emits_frontier_buffers() {
let p = dominator_tree(ProgramGraphShape::new(4, 4), "fin", "fout");
let names: Vec<&str> = p.buffers().iter().map(|b| b.name()).collect();
assert!(names.contains(&"fin"));
assert!(names.contains(&"fout"));
}
#[test]
fn dominator_tree_soundness_is_mayover() {
use vyre::ir::Node;
let p = dominator_tree(ProgramGraphShape::new(2, 1), "fin", "fout");
let [Node::Region { generator, .. }] = p.entry() else {
panic!("dominator_tree must emit one wrapped region");
};
assert_eq!(generator.as_str(), OP_ID);
}
#[test]
fn dominator_tree_gpu_over_approximates_strict_dominators_on_diamond() {
let p = dominator_tree(ProgramGraphShape::new(4, 4), "fin", "fout");
let to_bytes = vyre_primitives::wire::pack_u32_slice;
let inputs = vec![
to_bytes(&[0, 0, 0, 0]), to_bytes(&[0, 2, 3, 4, 4]), to_bytes(&[1, 2, 3, 3]), to_bytes(&[
edge_kind::DOMINANCE,
edge_kind::DOMINANCE,
edge_kind::DOMINANCE,
edge_kind::DOMINANCE,
]),
to_bytes(&[0, 0, 0, 0]), to_bytes(&[0b1000]), to_bytes(&[0b1000]), ];
let values: Vec<vyre_reference::value::Value> = inputs
.into_iter()
.map(vyre_reference::value::Value::from)
.collect();
let outputs = vyre_reference::reference_eval(&p, &values).unwrap();
let gpu_out = u32::from_le_bytes(outputs[0].to_bytes()[0..4].try_into().unwrap());
let dom = cpu_dominator_sets(4, 0, &[(0, 1), (0, 2), (1, 3), (2, 3)]);
let true_dom_bitset: u32 = dom[3].iter().map(|&n| 1u32 << n).sum();
let one_hop_predecessors_of_3: u32 = 0b1110; assert_eq!(
gpu_out, one_hop_predecessors_of_3,
"dominator_tree single-step shim returned {gpu_out:b}; expected \
{one_hop_predecessors_of_3:b} (immediate DOMINANCE predecessors of node 3). \
True strict dominators are {true_dom_bitset:b} - the shim does NOT \
compute these and rules requiring strict dominators must use \
cpu_dominator_sets instead."
);
assert_ne!(
gpu_out, true_dom_bitset,
"dominator_tree shim must visibly differ from strict dominators \
on the diamond - equality here would mean the substrate \
silently became a closure and the doc/tests need updating."
);
}
#[test]
#[should_panic(expected = "node_count must be positive")]
fn dominator_tree_zero_node_count_should_panic() {
let _ = dominator_tree(ProgramGraphShape::new(0, 0), "fin", "fout");
}
#[test]
#[should_panic(expected = "empty buffer name")]
fn dominator_tree_empty_buffer_name_should_panic() {
let _ = dominator_tree(ProgramGraphShape::new(4, 4), "", "fout");
}
}