#[cfg(test)]
use crate::CallEdge;
#[cfg(test)]
use crate::ResolvedCallGraph;
#[cfg(test)]
use bonsai_common::{FuncId, Precision};
#[cfg(test)]
use std::cmp::Ordering;
#[derive(Clone, Debug)]
#[cfg(test)]
pub struct ResolvedPath {
pub funcs: Vec<FuncId>,
pub edges: Vec<CallEdge>,
pub precision: Precision,
}
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum PathTruncation {
None,
MaxDepth,
MaxPaths,
ProbeBudget,
}
impl PathTruncation {
#[must_use]
pub fn is_truncated(self) -> bool {
!matches!(self, PathTruncation::None)
}
#[must_use]
pub fn label(self) -> Option<&'static str> {
match self {
PathTruncation::None => None,
PathTruncation::MaxDepth => Some("max-depth cap"),
PathTruncation::MaxPaths => Some("max-paths cap"),
PathTruncation::ProbeBudget => Some("path-probe budget"),
}
}
}
#[must_use]
#[cfg(test)]
pub fn enumerate_paths_resolved(
cg: &ResolvedCallGraph,
from: FuncId,
to: FuncId,
max_paths: usize,
max_depth: usize,
max_probes: usize,
) -> (Vec<ResolvedPath>, PathTruncation) {
if max_paths == 0 {
return (Vec::new(), PathTruncation::MaxPaths);
}
let mut can_reach_target = ahash::AHashSet::new();
let mut reverse_work = vec![to];
can_reach_target.insert(to);
while let Some(callee) = reverse_work.pop() {
let mut callers: Vec<FuncId> = cg
.callers_of(callee)
.filter(|edge| edge.precision.is_semantic())
.map(|edge| edge.from)
.collect();
callers.sort_unstable_by_key(|func| func.raw());
callers.dedup();
for caller in callers {
if can_reach_target.insert(caller) {
reverse_work.push(caller);
}
}
}
if !can_reach_target.contains(&from) {
return (Vec::new(), PathTruncation::None);
}
let mut queue = std::collections::BinaryHeap::new();
let mut initial_seen = ahash::AHashSet::new();
initial_seen.insert(from);
queue.push(PathState {
funcs: vec![from],
edges: Vec::new(),
seen: initial_seen,
precision: Precision::Exact,
order: 0,
});
let mut next_order = 1usize;
let mut probes = 0usize;
let mut truncation = PathTruncation::None;
let mut results = Vec::new();
let mut emitted: ahash::AHashSet<Vec<(FuncId, u64, u64, u32)>> = ahash::AHashSet::default();
while let Some(state) = queue.pop() {
probes = probes.saturating_add(1);
if max_probes != 0 && probes > max_probes {
truncation = PathTruncation::ProbeBudget;
break;
}
let current = *state.funcs.last().expect("path state has at least one func");
if current == to {
let key = path_identity(&state.funcs, &state.edges);
if emitted.insert(key) {
if results.len() >= max_paths {
truncation = PathTruncation::MaxPaths;
break;
}
results.push(ResolvedPath {
funcs: state.funcs,
edges: state.edges,
precision: state.precision,
});
}
continue;
}
if max_depth != 0 && state.edges.len() >= max_depth {
if truncation == PathTruncation::None {
truncation = PathTruncation::MaxDepth;
}
continue;
}
let mut edges: Vec<&CallEdge> = cg
.callees_of(current)
.filter(|edge| edge.precision.is_semantic() && can_reach_target.contains(&edge.to))
.collect();
edges.sort_by_key(|edge| {
(
edge.to.raw(),
edge.span.file.raw(),
edge.span.start,
edge.span.end,
edge_kind_rank(edge.kind),
precision_rank(edge.precision),
)
});
for edge in edges {
if state.seen.contains(&edge.to) {
continue;
}
let mut next_funcs = state.funcs.clone();
next_funcs.push(edge.to);
let mut next_edges = state.edges.clone();
next_edges.push(edge.clone());
let mut next_seen = state.seen.clone();
next_seen.insert(edge.to);
let next_precision = state.precision.meet(edge.precision);
queue.push(PathState {
funcs: next_funcs,
edges: next_edges,
seen: next_seen,
precision: next_precision,
order: next_order,
});
next_order = next_order.saturating_add(1);
}
}
results.sort_by(|a, b| {
a.edges
.len()
.cmp(&b.edges.len())
.then_with(|| precision_rank(a.precision).cmp(&precision_rank(b.precision)))
.then_with(|| a.funcs.cmp(&b.funcs))
});
(results, truncation)
}
#[derive(Clone)]
#[cfg(test)]
struct PathState {
funcs: Vec<FuncId>,
edges: Vec<CallEdge>,
seen: ahash::AHashSet<FuncId>,
precision: Precision,
order: usize,
}
#[cfg(test)]
impl PathState {
fn hops(&self) -> usize {
self.edges.len()
}
}
#[cfg(test)]
impl PartialEq for PathState {
fn eq(&self, other: &Self) -> bool {
self.hops() == other.hops()
&& precision_rank(self.precision) == precision_rank(other.precision)
&& self.order == other.order
}
}
#[cfg(test)]
impl Eq for PathState {}
#[cfg(test)]
impl PartialOrd for PathState {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
#[cfg(test)]
impl Ord for PathState {
fn cmp(&self, other: &Self) -> Ordering {
other
.hops()
.cmp(&self.hops())
.then_with(|| precision_rank(other.precision).cmp(&precision_rank(self.precision)))
.then_with(|| other.order.cmp(&self.order))
}
}
#[cfg(test)]
fn path_identity(funcs: &[FuncId], edges: &[CallEdge]) -> Vec<(FuncId, u64, u64, u32)> {
funcs
.iter()
.copied()
.enumerate()
.map(|(idx, func)| {
let edge = edges.get(idx);
(
func,
edge.map_or(0, |edge| edge.span.start),
edge.map_or(0, |edge| edge.span.end),
edge.map_or(u32::MAX, |edge| edge.span.file.raw()),
)
})
.collect()
}
#[cfg(test)]
fn edge_kind_rank(kind: crate::EdgeKind) -> u8 {
match kind {
crate::EdgeKind::Direct => 0,
crate::EdgeKind::Virtual => 1,
crate::EdgeKind::Indirect => 2,
crate::EdgeKind::Unknown => 3,
}
}
#[cfg(test)]
fn precision_rank(precision: Precision) -> u8 {
match precision {
Precision::Exact => 0,
Precision::Narrowed => 1,
Precision::OverApproximate => 2,
Precision::Unknown => 3,
}
}