use camino::{Utf8Path, Utf8PathBuf};
use super::graph::{BuildEdge, IrHashMap};
#[cfg(test)]
#[path = "cycle_property_tests.rs"]
mod cycle_property_tests;
#[path = "cycle_support.rs"]
mod support;
#[cfg(any(test, kani))]
use support::canonicalize_cycle_by;
#[cfg(not(kani))]
use support::path_cmp;
use support::{canonicalize_cycle, path_eq, state_for_path, target_entry_for_path};
#[cfg(test)]
#[path = "cycle_tests.rs"]
mod tests;
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum VisitState {
Visiting,
Visited,
}
#[derive(Clone, Copy, Debug)]
enum CycleSearch {
#[cfg(kani)]
Presence,
Path,
}
#[derive(Debug, Eq, PartialEq)]
enum CycleVisitResult {
None,
#[cfg(kani)]
Present,
Path(Vec<Utf8PathBuf>),
}
impl CycleVisitResult {
const fn is_cycle(&self) -> bool {
!matches!(self, Self::None)
}
fn into_path(self) -> Option<Vec<Utf8PathBuf>> {
match self {
Self::Path(cycle) => Some(cycle),
#[cfg(kani)]
Self::Present => None,
Self::None => None,
}
}
}
pub(crate) struct CycleDetectionReport {
pub(crate) cycle: Option<Vec<Utf8PathBuf>>,
pub(crate) missing_dependencies: Vec<(Utf8PathBuf, Utf8PathBuf)>,
}
pub(crate) fn analyse(targets: &IrHashMap<Utf8PathBuf, BuildEdge>) -> CycleDetectionReport {
let mut detector = CycleDetector::new(targets);
let cycle = detector.detect();
CycleDetectionReport {
cycle,
missing_dependencies: detector.missing_dependencies,
}
}
#[cfg(kani)]
pub(crate) fn contains_cycle(targets: &IrHashMap<Utf8PathBuf, BuildEdge>) -> bool {
CycleDetector::new(targets).detect_presence()
}
struct CycleDetector<'targets> {
targets: &'targets IrHashMap<Utf8PathBuf, BuildEdge>,
stack: Vec<&'targets Utf8Path>,
states: IrHashMap<&'targets Utf8Path, VisitState>,
missing_dependencies: Vec<(Utf8PathBuf, Utf8PathBuf)>,
}
impl<'targets> CycleDetector<'targets> {
fn new(targets: &IrHashMap<Utf8PathBuf, BuildEdge>) -> CycleDetector<'_> {
CycleDetector {
targets,
stack: Vec::new(),
states: IrHashMap::default(),
missing_dependencies: Vec::new(),
}
}
fn detect(&mut self) -> Option<Vec<Utf8PathBuf>> {
self.detect_with(CycleSearch::Path).into_path()
}
#[cfg(kani)]
fn detect_presence(&mut self) -> bool {
self.detect_with(CycleSearch::Presence).is_cycle()
}
fn detect_with(&mut self, search: CycleSearch) -> CycleVisitResult {
self.states.clear();
self.stack.clear();
self.missing_dependencies.clear();
self.detect_targets(search)
}
#[cfg(not(kani))]
fn detect_targets(&mut self, search: CycleSearch) -> CycleVisitResult {
let mut nodes: Vec<&'targets Utf8Path> =
self.targets.keys().map(Utf8PathBuf::as_path).collect();
nodes.sort_by(|left, right| path_cmp(left, right));
for node in nodes {
let Some((target, _)) = target_entry_for_path(self.targets, node) else {
continue;
};
if self.is_visited(target) {
continue;
}
let result = self.visit(target, search);
if result.is_cycle() {
return result;
}
}
CycleVisitResult::None
}
#[cfg(kani)]
fn detect_targets(&mut self, search: CycleSearch) -> CycleVisitResult {
for index in 0..self.targets.len() {
let Some((node, _)) = self.targets.entry_at(index) else {
continue;
};
if self.is_visited(node.as_path()) {
continue;
}
let result = self.visit(node.as_path(), search);
if result.is_cycle() {
return result;
}
}
CycleVisitResult::None
}
fn is_visited(&self, node: &Utf8Path) -> bool {
matches!(
state_for_path(&self.states, node),
Some(VisitState::Visited)
)
}
fn back_edge_result(&self, node: &'targets Utf8Path, search: CycleSearch) -> CycleVisitResult {
match search {
#[cfg(kani)]
CycleSearch::Presence => CycleVisitResult::Present,
CycleSearch::Path => CycleVisitResult::Path(canonicalize_cycle(
self.cycle_from_stack(self.stack_index(node), node),
)),
}
}
fn visit_known_edge(
&mut self,
node: &'targets Utf8Path,
edge: &'targets BuildEdge,
search: CycleSearch,
) -> CycleVisitResult {
let cycle = self.visit_dependencies(node, &edge.inputs, search);
if cycle.is_cycle() {
return cycle;
}
self.visit_dependencies(node, &edge.implicit_deps, search)
}
fn visit(&mut self, node: &'targets Utf8Path, search: CycleSearch) -> CycleVisitResult {
match state_for_path(&self.states, node) {
Some(VisitState::Visited) => return CycleVisitResult::None,
Some(VisitState::Visiting) => return self.back_edge_result(node, search),
None => {
self.states.insert(node, VisitState::Visiting);
}
}
if matches!(search, CycleSearch::Path) {
self.stack.push(node);
}
let cycle = match target_entry_for_path(self.targets, node) {
Some((_, edge)) => self.visit_known_edge(node, edge, search),
None => CycleVisitResult::None,
};
if matches!(search, CycleSearch::Path) {
self.stack.pop();
}
if !cycle.is_cycle() {
self.states.insert(node, VisitState::Visited);
}
cycle
}
fn stack_index(&self, node: &Utf8Path) -> usize {
let mut index = 0;
while index < self.stack.len() {
if let Some(candidate) = self.stack.get(index)
&& path_eq(candidate, node)
{
return index;
}
index += 1;
}
debug_assert!(false, "visiting node must be on the stack");
0
}
fn cycle_from_stack(&self, start: usize, node: &Utf8Path) -> Vec<Utf8PathBuf> {
let mut cycle = Vec::new();
let mut index = start;
while index < self.stack.len() {
if let Some(path) = self.stack.get(index) {
cycle.push(path.to_path_buf());
}
index += 1;
}
cycle.push(node.to_path_buf());
cycle
}
fn visit_dependencies(
&mut self,
node: &'targets Utf8Path,
dependencies: &[Utf8PathBuf],
search: CycleSearch,
) -> CycleVisitResult {
let mut index = 0;
while index < dependencies.len() {
let Some(dependency) = dependencies.get(index) else {
index += 1;
continue;
};
let result = self.visit_dependency(node, dependency.as_path(), search);
if result.is_cycle() {
return result;
}
index += 1;
}
CycleVisitResult::None
}
#[cfg(test)]
fn find_cycle(targets: &IrHashMap<Utf8PathBuf, BuildEdge>) -> Option<Vec<Utf8PathBuf>> {
analyse(targets).cycle
}
fn record_missing_dependency(&mut self, node: &Utf8Path, dep: &Utf8Path) {
tracing::debug!(
missing = %dep,
dependent = %node,
"skipping dependency missing from targets during cycle detection",
);
self.missing_dependencies
.push((node.to_path_buf(), dep.to_path_buf()));
}
fn visit_dependency(
&mut self,
node: &'targets Utf8Path,
dep: &Utf8Path,
search: CycleSearch,
) -> CycleVisitResult {
let Some((target, _)) = target_entry_for_path(self.targets, dep) else {
if matches!(search, CycleSearch::Path) {
self.record_missing_dependency(node, dep);
}
return CycleVisitResult::None;
};
self.visit(target, search)
}
}
#[cfg(kani)]
#[path = "cycle_verification.rs"]
mod verification;