Skip to content

Conversation

@leynos
Copy link
Owner

@leynos leynos commented Sep 15, 2025

Summary

  • refactor CycleDetector to own traversal state and expose a detect API, avoiding borrowed keys
  • simplify canonicalize_cycle by rotating the prefix after removing the duplicate terminator
  • add targeted cycle-detection tests and document the revised helper responsibilities

closes #74

Testing

  • make fmt
  • make check-fmt
  • make lint
  • make test
  • make markdownlint
  • make nixie

https://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:

  • Extract cycle detection into a CycleDetector helper that owns traversal state and exposes a detect method
  • Simplify canonicalize_cycle by removing the duplicate terminator before rotating the cycle start

Documentation:

  • Update design documentation to describe the CycleDetector responsibilities and refined cycle-detection behavior

Tests:

  • Add tests for simple cycles, self-edges, acyclic graphs, and canonical ordering

@sourcery-ai
Copy link
Contributor

sourcery-ai bot commented Sep 15, 2025

Reviewer's Guide

This 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 helper

classDiagram
    class canonicalize_cycle {
        +cycle: Vec<PathBuf>
        +returns: Vec<PathBuf>
    }
    canonicalize_cycle --> PathBuf
Loading

Class diagram for new cycle-detection test helpers

classDiagram
    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
Loading

File-Level Changes

Change Details Files
Encapsulate traversal state and logic in CycleDetector
  • Introduce CycleDetector struct owning targets, stack, and states
  • Implement CycleDetector::new, detect, and visit methods
  • Remove free functions should_visit_node, visit, and visit_dependencies
src/ir.rs
Delegate find_cycle to CycleDetector
  • Replace inline traversal in find_cycle with CycleDetector::new/ detect
  • Simplify find_cycle signature to a one-liner
src/ir.rs
Simplify canonicalize_cycle behavior
  • Pop duplicate terminator before rotating
  • Push the first element at the end after rotation
src/ir.rs
Add targeted tests for cycle detection
  • Introduce edge_with_inputs helper
  • Add CycleDetector tests for simple cycles, self-edges, acyclic graphs
  • Add canonicalize_cycle tests for rotation behavior
src/ir.rs
Update documentation to reflect refactoring
  • Document self-edge handling and CycleDetector responsibilities
  • Clarify traversal algorithm description in netsuke-design.md
docs/netsuke-design.md

Assessment against linked issues

Issue Objective Addressed Explanation
#74 Reduce nested closure complexity in the find_cycle function by simplifying traversal logic.
#74 Refactor traversal logic by moving it into a separate type or breaking out smaller, focused functions.
#74 Update documentation to reflect the new traversal structure and improved maintainability.

Possibly linked issues


Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Sep 15, 2025

Summary by CodeRabbit

  • Bug Fixes
    • More robust cycle detection, including immediate rejection of self-dependencies.
    • Deterministic cycle error messages by canonicalising reported cycles.
  • Documentation
    • Updated design notes to clarify cycle detection semantics and traversal approach.
    • Minor formatting clean-up for readability.
  • Refactor
    • Centralised cycle detection logic into a dedicated helper to simplify traversal and maintenance.
  • Tests
    • Expanded coverage for simple cycles, self-dependencies, and acyclic graphs.
    • Added tests for deterministic cycle canonicalisation and aligned existing tests with the new logic.

Walkthrough

Replace 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

Cohort / File(s) Summary of Changes
Documentation: cycle detection design
docs/netsuke-design.md
Update cycle detection description: reject self‑edges, detect cycles via recursion stack, introduce CycleDetector as traversal owner; minor formatting fixes.
IR: CycleDetector refactor and tests
src/ir.rs
Replace ad‑hoc traversal with CycleDetector (stack + VisitState); make find_cycle delegate; revise canonicalisation to rotate to lexicographically smallest node; add/expand tests for simple cycles, self‑edges, acyclic graphs, and canonicalisation; add small helpers.

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
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~55 minutes

Possibly related PRs

Poem

Stacks climb high where the edges roam,
A self‑loop knocks, but finds no home.
The smallest name now leads the ring,
As CycleDetector tests its wing.
Closures quiet, the graph made plain—
A tidy path through node and chain.

✨ Finishing touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch codex/refactor-find_cycle-to-reduce-nesting-complexity-xa3wfu

Comment @coderabbitai help to get the list of available commands and usage tips.

Pre-merge checks

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 46.15% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (4 passed)
Check name Status Explanation
Title Check ✅ Passed State that the title "Refactor cycle detection traversal" succinctly and accurately summarises the primary change in this PR: the traversal logic was moved into a dedicated CycleDetector and find_cycle was refactored. Confirm the title is concise, focused, free of noise, and clear enough for a teammate scanning history to understand the main intent.
Linked Issues Check ✅ Passed State that the changes satisfy the objectives of linked issue #74 by extracting traversal into a dedicated CycleDetector type, reducing nesting and clarifying traversal responsibilities, and by adding targeted tests that validate cycles and self-edge handling. Confirm the implementation maps to the proposed solutions in issue #74 and addresses the coding-related requirements.
Out of Scope Changes Check ✅ Passed State that the diff is limited to an internal refactor (CycleDetector), test additions and documentation updates and that no exported API/signature changes or unrelated functional changes are present according to the provided summaries. Confirm no out-of-scope changes were detected.
Description Check ✅ Passed State that the PR description is directly related to the changeset: it documents the CycleDetector refactor, canonicalisation change to cycle handling, added targeted tests, and the listed test/formatting commands, which align with the raw_summary and PR objectives. Note that the description is sufficiently specific for this lenient check because it references the primary refactor, canonicalisation simplification, and test additions. Approve the description as passing this check.

Copy link

@codescene-delta-analysis codescene-delta-analysis bot left a 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.

Comment on lines +653 to +667
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);

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

Suppress

Copy link
Contributor

@sourcery-ai sourcery-ai bot left a 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>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment on lines +540 to +541
self.stack.pop();
self.states.insert(node, VisitState::Visited);
Copy link
Contributor

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();
Copy link
Contributor

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.

Copy link
Contributor

@coderabbitai coderabbitai bot left a 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

📥 Commits

Reviewing files that changed from the base of the PR and between f6439b0 and 24d8cd0.

📒 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 return in single-line functions.

  • Move conditionals with >2 branches into a predicate function.

  • Avoid unsafe unless 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 rstest fixtures for shared setup and to avoid repetition between tests.

  • Replace duplicated tests with #[rstest(...)] parameterised cases.

  • Prefer mockall for 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 mockable crate 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 stack and states in CycleDetector clarifies the DFS responsibilities.


546-549: Delegate via thin wrapper; OK.

find_cycle correctly 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 dependency

Replace 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 -C and stdio piping.

Explicitly call out that Netsuke resolves -C itself and forwards targets and -j to 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

Comment on lines +497 to 507
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
}
Copy link
Contributor

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.

Suggested change
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.

Comment on lines +509 to 525
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);
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

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.

Suggested change
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.

Comment on lines +509 to 543
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
}
Copy link
Contributor

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.

Suggested change
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.

Comment on lines +574 to +584
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);
}
}
Copy link
Contributor

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Refactor find_cycle function to reduce nested closure complexity

2 participants