-
Notifications
You must be signed in to change notification settings - Fork 0
Refactor cycle detection traversal #163
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Refactor cycle detection traversal #163
Conversation
Reviewer's GuideThis PR refactors the cycle detection logic by encapsulating traversal state in a dedicated CycleDetector struct with new detect and visit methods, streamlines the canonicalize_cycle helper, enriches targeted cycle-detection tests, and updates design documentation accordingly. Class diagram for updated cycle canonicalization helperclassDiagram
class canonicalize_cycle {
+cycle: Vec<PathBuf>
+returns: Vec<PathBuf>
}
canonicalize_cycle --> PathBuf
Class diagram for new cycle-detection test helpersclassDiagram
class edge_with_inputs {
+inputs: &[&str]
+output: &str
+returns: BuildEdge
}
class cyclic_targets {
+returns: HashMap<PathBuf, BuildEdge>
}
edge_with_inputs --> BuildEdge
cyclic_targets --> BuildEdge
cyclic_targets --> PathBuf
File-Level Changes
Assessment against linked issues
Possibly linked issues
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
Summary by CodeRabbit
WalkthroughReplace ad‑hoc DFS cycle detection in src/ir.rs with a CycleDetector struct that manages traversal state and exposes detect(). Update find_cycle to delegate to CycleDetector. Adjust cycle canonicalisation. Add tests for cycles, self‑edges, and acyclic graphs. Update design docs to reflect new semantics and helper. Changes
Sequence Diagram(s)sequenceDiagram
autonumber
participant C as Caller
participant IR as find_cycle(...)
participant CD as CycleDetector
participant G as Targets (graph)
C->>IR: find_cycle(targets)
IR->>CD: CycleDetector::new(G)
IR->>CD: detect()
rect rgba(200,230,255,0.3)
note right of CD: DFS with visitation map and stack
CD->>CD: visit(node)
alt node is on stack
CD-->>IR: Return cycle (stack slice)
else node unvisited
CD->>G: iterate input deps
CD->>CD: visit(dep) (recurse if in G)
end
end
IR->>IR: canonicalise(cycle)<br/>(rotate to smallest)
IR-->>C: Some(cycle) or None
Estimated code review effort🎯 4 (Complex) | ⏱️ ~55 minutes Possibly related PRs
Poem
✨ Finishing touches
🧪 Generate unit tests
Comment Pre-merge checks❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Gates Failed
Enforce advisory code health rules
(1 file with Code Duplication)
Gates Passed
5 Quality Gates Passed
See analysis details in CodeScene
Reason for failure
| Enforce advisory code health rules | Violations | Code Health Impact | |
|---|---|---|---|
| ir.rs | 1 advisory rule | 10.00 → 9.39 | Suppress |
Quality Gate Profile: Pay Down Tech Debt
Want more control? Customize Code Health rules or catch issues early with our IDE extension and CLI tool.
| fn canonicalize_cycle_handles_reverse_direction() { | ||
| let cycle = vec![ | ||
| PathBuf::from("c"), | ||
| PathBuf::from("b"), | ||
| PathBuf::from("a"), | ||
| PathBuf::from("c"), | ||
| ]; | ||
| let canonical = canonicalize_cycle(cycle); | ||
| let expected = vec![ | ||
| PathBuf::from("a"), | ||
| PathBuf::from("c"), | ||
| PathBuf::from("b"), | ||
| PathBuf::from("a"), | ||
| ]; | ||
| assert_eq!(canonical, expected); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
❌ New issue: Code Duplication
The module contains 2 functions with similar structure: tests.canonicalize_cycle_handles_reverse_direction,tests.canonicalize_cycle_rotates_smallest_node
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hey there - I've reviewed your changes and they look great!
Prompt for AI Agents
Please address the comments from this code review:
## Individual Comments
### Comment 1
<location> `src/ir.rs:540-541` </location>
<code_context>
+ }
}
+
+ self.stack.pop();
+ self.states.insert(node, VisitState::Visited);
+ None
}
</code_context>
<issue_to_address>
**issue (bug_risk):** Potential issue if visit returns early: stack may not be properly unwound.
If visit exits early due to a cycle, self.stack.pop() is skipped, leaving the stack inconsistent. Ensure stack is always unwound, even on early returns.
</issue_to_address>
### Comment 2
<location> `src/ir.rs:562` </location>
<code_context>
.enumerate()
.min_by(|(_, a), (_, b)| a.cmp(b))
.map_or(0, |(idx, _)| idx);
+ cycle.pop();
cycle.rotate_left(start);
- if let (Some(first), Some(slot)) = (cycle.first().cloned(), cycle.get_mut(len)) {
</code_context>
<issue_to_address>
**issue:** cycle.pop() may remove necessary information for single-node cycles.
This logic may fail for cycles of length one or two, such as self-loops. Please verify that all cycle sizes are handled correctly.
</issue_to_address>
### Comment 3
<location> `docs/netsuke-design.md:1086` </location>
<code_context>
+ deterministic error messages.
+
+ Traversal state is managed by a small `CycleDetector` helper struct. This
+ type owns the recursion stack and visitation map, allowing the traversal
+ functions to remain focused and easily testable.
</code_context>
<issue_to_address>
**suggestion (review_instructions):** This bullet paragraph exceeds 80 columns and should be wrapped for readability.
Please wrap this paragraph so that no line exceeds 80 columns, in accordance with the documentation formatting guidelines.
<details>
<summary>Review instructions:</summary>
**Path patterns:** `**/*.md`
**Instructions:**
Paragraphs and bullets must be wrapped to 80 columns
</details>
</issue_to_address>Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
| self.stack.pop(); | ||
| self.states.insert(node, VisitState::Visited); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
issue (bug_risk): Potential issue if visit returns early: stack may not be properly unwound.
If visit exits early due to a cycle, self.stack.pop() is skipped, leaving the stack inconsistent. Ensure stack is always unwound, even on early returns.
| .enumerate() | ||
| .min_by(|(_, a), (_, b)| a.cmp(b)) | ||
| .map_or(0, |(idx, _)| idx); | ||
| cycle.pop(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
issue: cycle.pop() may remove necessary information for single-node cycles.
This logic may fail for cycles of length one or two, such as self-loops. Please verify that all cycle sizes are handled correctly.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 4
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
docs/netsuke-design.md (1)
1-1715: Use en‑GB Oxford “-ize” spelling consistently in prose.The style guide specifies en‑GB‑oxendict with “-ize/-our”. Replace instances such as “serialise/deserialise/serialisation” with “serialize/deserialize/serialization” across the docs.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: ASSERTIVE
Plan: Pro
📒 Files selected for processing (2)
docs/netsuke-design.md(8 hunks)src/ir.rs(3 hunks)
🧰 Additional context used
📓 Path-based instructions (4)
**/*.rs
📄 CodeRabbit inference engine (AGENTS.md)
**/*.rs: Each Rust module file must begin with a module-level //! comment explaining its purpose
Document public APIs using Rustdoc comments (///) so cargo doc can generate docs
Place function attributes after doc comments
Prefer immutable data; avoid unnecessary mut bindings
Handle errors with Result instead of panicking where feasible
Avoid unsafe code unless absolutely necessary and clearly document any usage
Do not use return in single-line functions
Prefer single-line function bodies where appropriate (e.g., pub fn new(id: u64) -> Self { Self(id) })
Prefer .expect() over .unwrap()
Use concat!() to combine long string literals rather than escaping newlines with a backslash
Use predicate functions for conditional criteria with more than two branches
Where a function is too long, extract meaningfully named helper functions (separation of concerns, CQRS)
Where a function has too many parameters, group related parameters into meaningfully named structs
If a function is unused with specific features selected, use conditional compilation with #[cfg] or #[cfg_attr]
Where a function returns a large error, consider using Arc to reduce returned data size
Use semantic error enums deriving std::error::Error via thiserror for inspectable conditions
Clippy warnings must be disallowed; fix issues in code rather than silencing
Do not silence lints except as a last resort; any suppression must be tightly scoped and include a clear reason
Prefer #[expect] over #[allow] when suppressing lints
Use cap-std for all filesystem operations (capability-based, sandboxed)
Use camino for path handling; avoid std::path::PathBuf directly
Avoid std::fs directly; wrap with cap-std
Comments must use en-GB-oxendict spelling and grammar (except external API names)
No single Rust source file may exceed 400 lines; break up large switches/dispatch tables by feature
Name booleans with is/has/should prefixes; use clear, descriptive names for variables and functions
Function documentation must include...
Files:
src/ir.rs
⚙️ CodeRabbit configuration file
**/*.rs: * Seek to keep the cyclomatic complexity of functions no more than 12.
Adhere to single responsibility and CQRS
Place function attributes after doc comments.
Do not use
returnin single-line functions.Move conditionals with >2 branches into a predicate function.
Avoid
unsafeunless absolutely necessary.Every module must begin with a
//!doc comment that explains the module's purpose and utility.Comments and docs must follow en-GB-oxendict (-ize / -our) spelling and grammar
Lints must not be silenced except as a last resort.
#[allow]is forbidden.- Only narrowly scoped
#[expect(lint, reason = "...")]is allowed.- No lint groups, no blanket or file-wide suppression.
- Include
FIXME:with link if a fix is expected.Where code is only used by specific features, it must be conditionally compiled or a conditional expectation for unused_code applied.
Use
rstestfixtures for shared setup and to avoid repetition between tests.Replace duplicated tests with
#[rstest(...)]parameterised cases.Prefer
mockallfor mocks/stubs.Prefer
.expect()over.unwrap()Ensure that any API or behavioural changes are reflected in the documentation in
docs/Ensure that any completed roadmap steps are recorded in the appropriate roadmap in
docs/Files must not exceed 400 lines in length
- Large modules must be decomposed
- Long match statements or dispatch tables should be decomposed by domain and collocated with targets
- Large blocks of inline data (e.g., test fixtures, constants or templates) must be moved to external files and inlined at compile-time or loaded at run-time.
Environment access (env::set_var and env::remove_var) are always unsafe in Rust 2024 and MUST be marked as such
- For testing of functionality depending upon environment variables, dependency injection and the
mockablecrate are the preferred option.- If mockable cannot be used, env mutations in tests ...
Files:
src/ir.rs
docs/**/*.{rs,md}
📄 CodeRabbit inference engine (docs/rust-doctest-dry-guide.md)
In fenced code blocks for docs, explicitly mark code fences with rust (```rust) for clarity
Files:
docs/netsuke-design.md
docs/**/*.md
📄 CodeRabbit inference engine (AGENTS.md)
Use the docs/ markdown files as the reference knowledge base and update them proactively when decisions or requirements change
Files:
docs/netsuke-design.md
**/*.md
📄 CodeRabbit inference engine (AGENTS.md)
**/*.md: Documentation must use en-GB-oxendict spelling and grammar (LICENSE name excluded)
Validate Markdown with make markdownlint and run make fmt to format Markdown (including fixing table markup)
Validate Mermaid diagrams in Markdown by running make nixie
Wrap Markdown paragraphs and bullet points at 80 columns
Do not wrap tables and headings in Markdown
Wrap code blocks in Markdown at 120 columns
Use dashes (-) for list bullets in Markdown
Use GitHub-flavoured Markdown footnotes ([^1])
Files:
docs/netsuke-design.md
⚙️ CodeRabbit configuration file
**/*.md: * Avoid 2nd person or 1st person pronouns ("I", "you", "we")
- Use en-GB-oxendict (-ize / -our) spelling and grammar
- Headings must not be wrapped.
- Documents must start with a level 1 heading
- Headings must correctly increase or decrease by no more than one level at a time
- Use GitHub-flavoured Markdown style for footnotes and endnotes.
- Numbered footnotes must be numbered by order of appearance in the document.
Files:
docs/netsuke-design.md
🔍 Remote MCP Deepwiki, Ref
Summary of additional context found (concise, review‑focused)
-
The repository’s developer documentation includes a “Developer Guide” with pages for Testing Framework, Code Quality Standards, and Documentation Testing — these directly relate to the PR’s test/format/lint checklist (make fmt, make check-fmt, make lint, make test, make markdownlint, make nixie).
-
Architecture/implementation docs (Architecture → Data Structures / Implementation Details) are present and likely document the IR / BuildEdge / error types (e.g., IrGenError::CircularDependency) referenced by the PR; consult them to validate behavioral claims.
-
Attempts to fetch the PR page and the modified source files (PR #163, files / src/ir.rs) directly failed (HTTP 402), so I could not retrieve the diffs or file contents via the ref tool — reviewers should inspect the PR diffs on GitHub and run the test suite locally to verify behavior.
Actionable review checklist (minimal)
- Confirm unit tests added for cycles/self-edges run and cover expected cases (self-edge rejection, simple cycles, acyclic).
- Run make check-fmt, lint, test, and markdownlint per repo standards.
- Verify canonicalize_cycle rotation is deterministic and messages match expectations (lexicographic rotation to smallest node).
- Confirm no public API/signature changes were introduced.
Citations
- Repo documentation structure (pages found):
- Failed direct fetches of PR/files (HTTP 402):,
⏰ Context from checks skipped due to timeout of 120000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: Sourcery review
🔇 Additional comments (7)
src/ir.rs (4)
481-486: Encapsulate traversal state; good separation of concerns.Ownership of
stackandstatesinCycleDetectorclarifies the DFS responsibilities.
546-549: Delegate via thin wrapper; OK.
find_cyclecorrectly wraps the detector without exposing internals.
551-568: Canonical cycle normalisation looks correct.Dropping the duplicate terminator, rotating to the minimal node, then re‑closing the cycle yields stable, readable diagnostics.
Ensure ordering invariants are documented in the error message format used by callers so snapshot tests remain stable across platforms.
481-486: Replace std::path::PathBuf with camino::Utf8PathBuf in IR types; verify MSRV and dependencyReplace PathBuf with Utf8PathBuf in IR structs and tests in a follow‑up to avoid widening this PR. Confirm the project’s MSRV supports the camino version already used, or add camino in a separate change to avoid MSRV churn.
Location: src/ir.rs — lines 481–486, 574–584
docs/netsuke-design.md (3)
1078-1088: Document algorithm precisely; matches implementation.State that order‑only deps are ignored and that cycles are reported from the first re‑visited node with the cycle rotated to the lexicographically smallest element. This aligns with
CycleDetector.
1189-1189: Tighten CLI description around-Cand stdio piping.Explicitly call out that Netsuke resolves
-Citself and forwards targets and-jto Ninja; stdio is piped and mirrored. This is clear and actionable.Also applies to: 1194-1194, 1196-1198, 1200-1204
1538-1538: Keep roadmap tasks in imperative mood and consistent style.Entries read cleanly and map to deliverables; good.
Also applies to: 1541-1541, 1544-1544, 1546-1546
| fn detect(&mut self) -> Option<Vec<PathBuf>> { | ||
| for node in self.targets.keys() { | ||
| if self.states.contains_key(node.as_path()) { | ||
| continue; | ||
| } | ||
|
|
||
| if let Some(cycle) = visit(targets, dep, stack, states) { | ||
| if let Some(cycle) = self.visit(node.clone()) { | ||
| return Some(cycle); | ||
| } | ||
| } | ||
| None | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧹 Nitpick (assertive)
Make traversal order deterministic.
HashMap iteration order is non‑deterministic; the first reported cycle may vary run‑to‑run. Sort keys before traversal to stabilise error reporting.
Apply this diff:
- fn detect(&mut self) -> Option<Vec<PathBuf>> {
- for node in self.targets.keys() {
+ fn detect(&mut self) -> Option<Vec<PathBuf>> {
+ let mut nodes: Vec<_> = self.targets.keys().cloned().collect();
+ nodes.sort();
+ for node in nodes {
if self.states.contains_key(node.as_path()) {
continue;
}
- if let Some(cycle) = self.visit(node.clone()) {
+ if let Some(cycle) = self.visit(node.clone()) {
return Some(cycle);
}
}
None
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| fn detect(&mut self) -> Option<Vec<PathBuf>> { | |
| for node in self.targets.keys() { | |
| if self.states.contains_key(node.as_path()) { | |
| continue; | |
| } | |
| if let Some(cycle) = visit(targets, dep, stack, states) { | |
| if let Some(cycle) = self.visit(node.clone()) { | |
| return Some(cycle); | |
| } | |
| } | |
| None | |
| } | |
| fn detect(&mut self) -> Option<Vec<PathBuf>> { | |
| let mut nodes: Vec<_> = self.targets.keys().cloned().collect(); | |
| nodes.sort(); | |
| for node in nodes { | |
| if self.states.contains_key(node.as_path()) { | |
| continue; | |
| } | |
| if let Some(cycle) = self.visit(node.clone()) { | |
| return Some(cycle); | |
| } | |
| } | |
| None | |
| } |
🤖 Prompt for AI Agents
In src/ir.rs around lines 497 to 507, the detect() traversal uses
HashMap::keys() which yields a non-deterministic order; collect the keys into a
Vec<PathBuf>, sort that Vec (e.g. let mut keys: Vec<_> =
self.targets.keys().cloned().collect(); keys.sort();), then iterate over the
sorted keys so cycle detection is deterministic and error reports are stable
across runs.
| fn visit(&mut self, node: PathBuf) -> Option<Vec<PathBuf>> { | ||
| match self.states.get(node.as_path()) { | ||
| Some(VisitState::Visited) => return None, | ||
| Some(VisitState::Visiting) => { | ||
| let idx = self | ||
| .stack | ||
| .iter() | ||
| .position(|n| n == &node) | ||
| .expect("node should be present in the traversal stack"); | ||
| let mut cycle: Vec<PathBuf> = self.stack.iter().skip(idx).cloned().collect(); | ||
| cycle.push(node.clone()); | ||
| return Some(canonicalize_cycle(cycle)); | ||
| } | ||
| None => { | ||
| self.states.insert(node.clone(), VisitState::Visiting); | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fix match on Option<&VisitState>; current pattern does not compile.
HashMap::get returns Option<&VisitState>. Match arms must pattern‑match references.
Apply this diff:
- match self.states.get(node.as_path()) {
- Some(VisitState::Visited) => return None,
- Some(VisitState::Visiting) => {
+ match self.states.get(node.as_path()) {
+ Some(&VisitState::Visited) => return None,
+ Some(&VisitState::Visiting) => {📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| fn visit(&mut self, node: PathBuf) -> Option<Vec<PathBuf>> { | |
| match self.states.get(node.as_path()) { | |
| Some(VisitState::Visited) => return None, | |
| Some(VisitState::Visiting) => { | |
| let idx = self | |
| .stack | |
| .iter() | |
| .position(|n| n == &node) | |
| .expect("node should be present in the traversal stack"); | |
| let mut cycle: Vec<PathBuf> = self.stack.iter().skip(idx).cloned().collect(); | |
| cycle.push(node.clone()); | |
| return Some(canonicalize_cycle(cycle)); | |
| } | |
| None => { | |
| self.states.insert(node.clone(), VisitState::Visiting); | |
| } | |
| } | |
| fn visit(&mut self, node: PathBuf) -> Option<Vec<PathBuf>> { | |
| match self.states.get(node.as_path()) { | |
| Some(&VisitState::Visited) => return None, | |
| Some(&VisitState::Visiting) => { | |
| let idx = self | |
| .stack | |
| .iter() | |
| .position(|n| n == &node) | |
| .expect("node should be present in the traversal stack"); | |
| let mut cycle: Vec<PathBuf> = self.stack.iter().skip(idx).cloned().collect(); | |
| cycle.push(node.clone()); | |
| return Some(canonicalize_cycle(cycle)); | |
| } | |
| None => { | |
| self.states.insert(node.clone(), VisitState::Visiting); | |
| } | |
| } |
🤖 Prompt for AI Agents
In src/ir.rs around lines 509 to 525, the match is using patterns for owned
VisitState but HashMap::get returns Option<&VisitState>; update the patterns to
match references (e.g. change Some(VisitState::Visited) to
Some(&VisitState::Visited) and Some(VisitState::Visiting) to
Some(&VisitState::Visiting)), leaving the rest of the arm bodies unchanged so
you still clone the node/stack as before.
| fn visit(&mut self, node: PathBuf) -> Option<Vec<PathBuf>> { | ||
| match self.states.get(node.as_path()) { | ||
| Some(VisitState::Visited) => return None, | ||
| Some(VisitState::Visiting) => { | ||
| let idx = self | ||
| .stack | ||
| .iter() | ||
| .position(|n| n == &node) | ||
| .expect("node should be present in the traversal stack"); | ||
| let mut cycle: Vec<PathBuf> = self.stack.iter().skip(idx).cloned().collect(); | ||
| cycle.push(node.clone()); | ||
| return Some(canonicalize_cycle(cycle)); | ||
| } | ||
| None => { | ||
| self.states.insert(node.clone(), VisitState::Visiting); | ||
| } | ||
| } | ||
| if let Some(cycle) = visit(targets, node, &mut stack, &mut states) { | ||
| return Some(cycle); | ||
|
|
||
| self.stack.push(node.clone()); | ||
|
|
||
| if let Some(edge) = self.targets.get(&node) { | ||
| for dep in &edge.inputs { | ||
| if !self.targets.contains_key(dep) { | ||
| continue; | ||
| } | ||
| if let Some(cycle) = self.visit(dep.clone()) { | ||
| return Some(cycle); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| self.stack.pop(); | ||
| self.states.insert(node, VisitState::Visited); | ||
| None | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧹 Nitpick (assertive)
Avoid needless cloning by visiting by reference.
Passing PathBuf by value forces clones on every edge. Visit by &Path to cut allocations on deep graphs.
Apply this diff (and adjust call sites accordingly):
- fn visit(&mut self, node: PathBuf) -> Option<Vec<PathBuf>> {
- match self.states.get(node.as_path()) {
+ fn visit(&mut self, node: &std::path::Path) -> Option<Vec<PathBuf>> {
+ match self.states.get(node) {
Some(&VisitState::Visited) => return None,
Some(&VisitState::Visiting) => {
- let idx = self
- .stack
- .iter()
- .position(|n| n == &node)
+ let idx = self.stack.iter().position(|n| n.as_path() == node)
.expect("node should be present in the traversal stack");
let mut cycle: Vec<PathBuf> = self.stack.iter().skip(idx).cloned().collect();
- cycle.push(node.clone());
+ cycle.push(node.to_path_buf());
return Some(canonicalize_cycle(cycle));
}
None => {
- self.states.insert(node.clone(), VisitState::Visiting);
+ self.states.insert(node.to_path_buf(), VisitState::Visiting);
}
}
- self.stack.push(node.clone());
+ self.stack.push(node.to_path_buf());
- if let Some(edge) = self.targets.get(&node) {
+ if let Some(edge) = self.targets.get(node) {
for dep in &edge.inputs {
if !self.targets.contains_key(dep) {
continue;
}
- if let Some(cycle) = self.visit(dep.clone()) {
+ if let Some(cycle) = self.visit(dep) {
return Some(cycle);
}
}
}
self.stack.pop();
- self.states.insert(node, VisitState::Visited);
+ self.states.insert(node.to_path_buf(), VisitState::Visited);
None
}And in detect:
- if let Some(cycle) = self.visit(node.clone()) {
+ if let Some(cycle) = self.visit(node.as_path()) {
return Some(cycle);
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| fn visit(&mut self, node: PathBuf) -> Option<Vec<PathBuf>> { | |
| match self.states.get(node.as_path()) { | |
| Some(VisitState::Visited) => return None, | |
| Some(VisitState::Visiting) => { | |
| let idx = self | |
| .stack | |
| .iter() | |
| .position(|n| n == &node) | |
| .expect("node should be present in the traversal stack"); | |
| let mut cycle: Vec<PathBuf> = self.stack.iter().skip(idx).cloned().collect(); | |
| cycle.push(node.clone()); | |
| return Some(canonicalize_cycle(cycle)); | |
| } | |
| None => { | |
| self.states.insert(node.clone(), VisitState::Visiting); | |
| } | |
| } | |
| if let Some(cycle) = visit(targets, node, &mut stack, &mut states) { | |
| return Some(cycle); | |
| self.stack.push(node.clone()); | |
| if let Some(edge) = self.targets.get(&node) { | |
| for dep in &edge.inputs { | |
| if !self.targets.contains_key(dep) { | |
| continue; | |
| } | |
| if let Some(cycle) = self.visit(dep.clone()) { | |
| return Some(cycle); | |
| } | |
| } | |
| } | |
| self.stack.pop(); | |
| self.states.insert(node, VisitState::Visited); | |
| None | |
| } | |
| fn visit(&mut self, node: &std::path::Path) -> Option<Vec<PathBuf>> { | |
| match self.states.get(node) { | |
| Some(&VisitState::Visited) => return None, | |
| Some(&VisitState::Visiting) => { | |
| let idx = self | |
| .stack | |
| .iter() | |
| .position(|n| n.as_path() == node) | |
| .expect("node should be present in the traversal stack"); | |
| let mut cycle: Vec<PathBuf> = self.stack.iter().skip(idx).cloned().collect(); | |
| cycle.push(node.to_path_buf()); | |
| return Some(canonicalize_cycle(cycle)); | |
| } | |
| None => { | |
| self.states.insert(node.to_path_buf(), VisitState::Visiting); | |
| } | |
| } | |
| self.stack.push(node.to_path_buf()); | |
| if let Some(edge) = self.targets.get(node) { | |
| for dep in &edge.inputs { | |
| if !self.targets.contains_key(dep) { | |
| continue; | |
| } | |
| if let Some(cycle) = self.visit(dep) { | |
| return Some(cycle); | |
| } | |
| } | |
| } | |
| self.stack.pop(); | |
| self.states.insert(node.to_path_buf(), VisitState::Visited); | |
| None | |
| } |
🤖 Prompt for AI Agents
In src/ir.rs around lines 509 to 543, change visit to take a reference (fn
visit(&mut self, node: &Path)) and update all uses inside: lookup states with
self.states.get(node), insert with self.states.insert(node.to_path_buf(),
VisitState::Visiting), push onto stack with self.stack.push(node.to_path_buf()),
build cycle by comparing stack entries to node (use node.as_ref() or &** as
needed), and when recursing call self.visit(dep.as_path()) instead of cloning;
after finishing, pop the stack and mark visited with
self.states.insert(self.stack_last_or_node.to_path_buf(), VisitState::Visited)
(or insert node.to_path_buf() if easier). Also update detect and any other call
sites to pass &Path (e.g., node.as_path()) so you avoid unnecessary PathBuf
clones on every edge traversal.
| fn edge_with_inputs(inputs: &[&str], output: &str) -> BuildEdge { | ||
| BuildEdge { | ||
| action_id: "id".into(), | ||
| inputs: vec![PathBuf::from("b")], | ||
| explicit_outputs: vec![PathBuf::from("a")], | ||
| inputs: inputs.iter().map(PathBuf::from).collect(), | ||
| explicit_outputs: vec![PathBuf::from(output)], | ||
| implicit_outputs: Vec::new(), | ||
| order_only_deps: Vec::new(), | ||
| phony: false, | ||
| always: false, | ||
| }; | ||
| let edge_b = BuildEdge { | ||
| action_id: "id".into(), | ||
| inputs: vec![PathBuf::from("a")], | ||
| explicit_outputs: vec![PathBuf::from("b")], | ||
| implicit_outputs: Vec::new(), | ||
| order_only_deps: Vec::new(), | ||
| phony: false, | ||
| always: false, | ||
| }; | ||
| targets.insert(PathBuf::from("a"), edge_a); | ||
| targets.insert(PathBuf::from("b"), edge_b); | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧹 Nitpick (assertive)
Tests cover core scenarios; expand for determinism and multi‑cycle graphs.
Add a test that asserts determinism of the first reported cycle across runs, and a case where multiple disjoint cycles exist.
I can draft #[rstest] parametrised cases for multi‑cycle graphs and a seed‑stability check once traversal order is sorted.
Also applies to: 586-591, 593-602, 604-613, 615-623, 624-633, 634-651, 652-668
Summary
CycleDetectorto own traversal state and expose adetectAPI, avoiding borrowed keyscanonicalize_cycleby rotating the prefix after removing the duplicate terminatorcloses #74
Testing
make fmtmake check-fmtmake lintmake testmake markdownlintmake nixiehttps://chatgpt.com/codex/tasks/task_e_68c615de9a808322b7eca4a379fe4f9b
Summary by Sourcery
Refactor the cycle detection logic into a dedicated CycleDetector struct with a clear detect API, simplify cycle canonicalization, and reinforce correctness with targeted tests and documentation updates.
Enhancements:
Documentation:
Tests: