Skip to content

feat: add toTopologicallySortedLevels() to DependencyGraph#44564

Open
singhvishalkr wants to merge 5 commits into
ballerina-platform:masterfrom
singhvishalkr:feat/dependency-graph-levels
Open

feat: add toTopologicallySortedLevels() to DependencyGraph#44564
singhvishalkr wants to merge 5 commits into
ballerina-platform:masterfrom
singhvishalkr:feat/dependency-graph-levels

Conversation

@singhvishalkr
Copy link
Copy Markdown

@singhvishalkr singhvishalkr commented Apr 20, 2026

Summary

Adds a new public method toTopologicallySortedLevels() to DependencyGraph<T> that groups nodes by their dependency depth. This enables parallel compilation of independent modules within each level.

Resolves #43027

Changes

DependencyGraph.java

  • New method: List<Set<T>> toTopologicallySortedLevels() using BFS-based Kahn's algorithm
  • Level 0 = nodes with no dependencies (leaves), level 1 = nodes depending only on level-0, etc.
  • Includes cycle detection: throws IllegalStateException with unsorted node set if cycles exist
  • Returns fully unmodifiable structure (Collections.unmodifiableList of Collections.unmodifiableSet)

TopologicallySortedLevelsTest.java

8 unit tests covering:

  • Diamond graph (A→B→D, A→C→D) → [D] [B,C] [A]
  • Linear chain → one node per level
  • Disconnected components → independent roots share level 0
  • Single node / all-independent nodes
  • Cyclic graph → IllegalStateException
  • Partial cycle (acyclic root + cyclic subgraph) → IllegalStateException
  • Unmodifiable return guarantees (outer List + inner Sets)

Example

Graph: A → {B, C}, B → {D}, C → {D}

Level 0: {D}       ← no dependencies, compile first
Level 1: {B, C}    ← depend only on D, compile in parallel
Level 2: {A}       ← depends on B and C

Summary

This pull request adds a new toTopologicallySortedLevels() method to DependencyGraph that groups nodes into dependency levels to enable parallel processing of independent modules.

Changes

  • New public API: DependencyGraph.toTopologicallySortedLevels()

    • Produces levels where level 0 are nodes with no dependencies, level 1 depend only on level 0, etc.
    • Implements a BFS-style Kahn algorithm to build levels incrementally.
    • Returns fully unmodifiable results (Collections.unmodifiableList of Collections.unmodifiableSet).
    • Detects cyclic dependencies and throws IllegalStateException that includes the set of unsorted nodes.
  • Bug fix

    • Ensures nodes that appear only as dependency values (not as keys) are included in initial dependency counts so dependency-only leaves are handled correctly and do not trigger spurious cycle detection.
  • Javadoc

    • Documents the unmodifiable return guarantees and the IllegalStateException behavior for cycles.
  • Tests

    • Adds TopologicallySortedLevelsTest with 9 unit tests covering: diamond graph, linear chain, disconnected components, single node, multiple independent nodes, full and partial cycles, regression for dependency-only nodes (DependencyGraph.from(Map.of("A", Set.of("B")))), and immutability.
    • Strengthens immutability assertions: outer List is asserted to throw UnsupportedOperationException for add/remove/set; every inner Set is asserted to throw UnsupportedOperationException for add/remove/clear.

Business outcome

Provides a reliable way for the compiler driver (and other consumers) to obtain dependency levels so independent modules can be processed or compiled in parallel, improving compilation parallelism and correctness for graphs that include dependency-only nodes.

Implement BFS-based (Kahn's algorithm) level computation for
the dependency graph. Nodes are grouped into levels where level 0
contains nodes with no dependencies, level 1 contains nodes
that only depend on level-0 nodes, etc.

Includes cycle detection that throws IllegalStateException
with the set of unsorted nodes when cyclic dependencies exist.
Covers diamond graph, linear chain, disconnected components,
single node, all-independent, cyclic (IllegalStateException),
partial cycle, and unmodifiable return guarantees.
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 20, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 977cbd76-9917-4467-83b1-8a59e4123364

📥 Commits

Reviewing files that changed from the base of the PR and between daa5278 and 3faceb5.

📒 Files selected for processing (2)
  • compiler/ballerina-lang/src/main/java/io/ballerina/projects/DependencyGraph.java
  • compiler/ballerina-lang/src/test/java/io/ballerina/projects/TopologicallySortedLevelsTest.java
🚧 Files skipped from review as they are similar to previous changes (2)
  • compiler/ballerina-lang/src/main/java/io/ballerina/projects/DependencyGraph.java
  • compiler/ballerina-lang/src/test/java/io/ballerina/projects/TopologicallySortedLevelsTest.java

📝 Walkthrough

Walkthrough

Added toTopologicallySortedLevels() to DependencyGraph, producing immutable level-wise topological ordering via dependency counts and breadth-first queue traversal; detects cycles and throws IllegalStateException. New tests validate multiple graph shapes, cycle detection, and immutability.

Changes

Cohort / File(s) Summary
Implementation
compiler/ballerina-lang/src/main/java/io/ballerina/projects/DependencyGraph.java
Added public List<Set<T>> toTopologicallySortedLevels() implementing level-by-level topological traversal using a dependency-count map, reverse-dependency adjacency, and a queue. Returns immutable list/sets and throws IllegalStateException for cycles. Added LinkedList/Queue imports.
Test Suite
compiler/ballerina-lang/src/test/java/io/ballerina/projects/TopologicallySortedLevelsTest.java
Added TopologicallySortedLevelsTest with tests for diamond graph, linear chain, disconnected components, single node, multiple independent nodes, full and partial cycles (asserting exceptions), from(Map...) case, and immutability checks (outer list and inner sets).

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐰
I hop through nodes in neat little rows,
Grouping each level where the dependency flows,
I sniff out cycles and guard what I made,
Immutable baskets for every cascade,
Compile in parallel — hooray! — as I braid.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 8.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely summarizes the main change: adding a new method toTopologicallySortedLevels() to DependencyGraph.
Description check ✅ Passed The description provides clear purpose, approach, changes, and examples, though some template sections (Samples, Remarks, full Checklist) are not completed.
Linked Issues check ✅ Passed The PR fully implements the feature requested in #43027: a method returning dependency levels to enable parallel compilation of independent modules within each level.
Out of Scope Changes check ✅ Passed All changes directly address the linked issue; the bug fix for dependency-only nodes and documentation updates are necessary for correct operation of the requested feature.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

Assert all mutation ops (add, remove, set) on the outer List and
(add, remove, clear) on every inner Set throw UnsupportedOperationException.
@singhvishalkr
Copy link
Copy Markdown
Author

Addressed CodeRabbit's review — strengthened testLevelsAreUnmodifiable() in daa5278:

  • Outer List: now asserts add, remove, and set all throw UnsupportedOperationException
  • Every inner Set: iterates all levels and asserts add, remove, and clear each throw UnsupportedOperationException

Copy link
Copy Markdown

@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: 2

🧹 Nitpick comments (1)
compiler/ballerina-lang/src/main/java/io/ballerina/projects/DependencyGraph.java (1)

175-183: Document the public API guarantees.

The implementation intentionally returns deeply unmodifiable levels and throws on cycles, but the Javadoc only describes the happy path. Please add the immutability guarantee and @throws IllegalStateException so callers can rely on the contract.

📝 Proposed documentation update
      * `@return` an ordered list of sets, where the index represents the level
-     *         and each set contains nodes at that level
+     *         and each set contains nodes at that level; both the returned list
+     *         and each contained set are unmodifiable
+     * `@throws` IllegalStateException if the graph contains cyclic dependencies
      */
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@compiler/ballerina-lang/src/main/java/io/ballerina/projects/DependencyGraph.java`
around lines 175 - 183, Update the Javadoc for the public method in
DependencyGraph that returns nodes grouped by dependency levels to state the API
guarantees: document that each returned level (each Set and the enclosing List)
is deeply unmodifiable (immutable) and that the method will throw
IllegalStateException when a cycle is detected. Add an explicit `@throws`
IllegalStateException clause and a brief sentence describing the immutability
guarantee so callers (of the DependencyGraph method that returns dependency
levels) can rely on the contract.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@compiler/ballerina-lang/src/main/java/io/ballerina/projects/DependencyGraph.java`:
- Around line 185-205: The depCount map is only populated for keys of
dependencies, so nodes that appear only as dependency values never get an entry
and are missing from the initial zero-in-degree queue; update the population
logic so every node that appears either as a key or as a dependency value gets
an entry in depCount (e.g., before or while iterating dependencies.entrySet(),
ensure depCount.putIfAbsent(dep, 0) for each dep), keep the existing
depCount.put(dependent, entry.getValue().size()) for dependents, and continue
building reverseDeps as before so nodes like "B" in
DependencyGraph.from(Map.of("A", Set.of("B"))) will be queued correctly.

In
`@compiler/ballerina-lang/src/test/java/io/ballerina/projects/TopologicallySortedLevelsTest.java`:
- Around line 34-158: Add a new test in TopologicallySortedLevelsTest that
builds a graph via DependencyGraph.from(Map.of("A", Set.of("B"))) (import
java.util.Map) and asserts that toTopologicallySortedLevels() does not throw and
returns two levels with level 0 == Set.of("B") and level 1 == Set.of("A"); name
it something like testFromMapHandlesDependencyOnlyLeaves to mirror existing
tests and ensure dependency-only leaves are treated as level 0 rather than
causing a cyclic-detection error.

---

Nitpick comments:
In
`@compiler/ballerina-lang/src/main/java/io/ballerina/projects/DependencyGraph.java`:
- Around line 175-183: Update the Javadoc for the public method in
DependencyGraph that returns nodes grouped by dependency levels to state the API
guarantees: document that each returned level (each Set and the enclosing List)
is deeply unmodifiable (immutable) and that the method will throw
IllegalStateException when a cycle is detected. Add an explicit `@throws`
IllegalStateException clause and a brief sentence describing the immutability
guarantee so callers (of the DependencyGraph method that returns dependency
levels) can rely on the contract.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 74decea4-d8fc-422b-a3d3-43b44f37cbf7

📥 Commits

Reviewing files that changed from the base of the PR and between f59068a and daa5278.

📒 Files selected for processing (2)
  • compiler/ballerina-lang/src/main/java/io/ballerina/projects/DependencyGraph.java
  • compiler/ballerina-lang/src/test/java/io/ballerina/projects/TopologicallySortedLevelsTest.java

Nodes appearing only as dependency values (not map keys) were
missing from depCount, causing them to be excluded from the
initial zero-in-degree queue. This made DependencyGraph.from()
graphs incorrectly throw IllegalStateException for acyclic graphs.

Also documents @throws and unmodifiable return guarantees.
@singhvishalkr
Copy link
Copy Markdown
Author

All three points addressed:

  1. Bug fix (245a028): Added depCount.putIfAbsent(dep, 0) for dependency-only nodes inside the inner loop, and restored depCount.putIfAbsent(node, 0) in the key-set loop. Nodes that appear only as dependency values (e.g. B in Map.of("A", Set.of("B"))) are now correctly queued at level 0 instead of being falsely reported as cyclic.

  2. Javadoc (245a028): Updated @return to document unmodifiable guarantees and added @throws IllegalStateException for cycle detection.

  3. Regression test (3faceb5): Added testFromMapWithDependencyOnlyNode() using DependencyGraph.from(Map.of("A", Set.of("B"))) to assert level 0 = {B}, level 1 = {A}. Also added java.util.Map import.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Improvement]: Add a method to return the dependency levels to DepenendencyGraph.java class

1 participant