Skip to content

Comments

Support compiling multi-module packages#161

Merged
warunalakshitha merged 8 commits intoballerina-platform:mainfrom
azinneera:prj_api-m3
Feb 24, 2026
Merged

Support compiling multi-module packages#161
warunalakshitha merged 8 commits intoballerina-platform:mainfrom
azinneera:prj_api-m3

Conversation

@azinneera
Copy link
Contributor

@azinneera azinneera commented Feb 18, 2026

Purpose

Support compiling multi-module packages

Approach

  1. Get topologically sorted module list
  2. Compile modules in 2 phases based on the parallelizability:

Phase 1 (Sequential): Parse documents (parallel) → AST building → Import Resolution → Symbol Resolution → Type Resolution

Phase 2 (Parallel): CFG creation → Semantic Analysis → CFG Analysis

Summary by CodeRabbit

  • New Features

    • Detects and reports cyclic module dependencies.
    • Adds multi-module and cyclic sample projects for testing and examples.
  • Improvements

    • More robust dependency resolution with graph-based ordering.
    • Faster, more reliable compilation via phased compilation and parallel parsing.
    • Aggregated package diagnostics after compilation.
  • Tests

    • New tests validating multi-module dependency ordering and cycle detection.

@coderabbitai
Copy link

coderabbitai bot commented Feb 18, 2026

📝 Walkthrough

Walkthrough

Replaces ad-hoc module ordering with a graph-based resolver, adds DependencyGraph and module resolver, splits compilation into two phases (sequential parse/type-resolve, concurrent CFG/analysis/desugaring), updates module import extraction and backend iteration source, and adds multi-module/cyclic test fixtures and tests.

Changes

Cohort / File(s) Summary
Dependency graph & resolver
projects/dependency_graph.go, projects/module_resolver.go, projects/module_load_request.go
Adds a generic DependencyGraph[T] (lazy topo sort, cycle detection), module load/request types, and a moduleResolver with per-request caching and batch resolution.
Package resolution & compilation orchestration
projects/package_resolution.go, projects/package_compilation.go, projects/ballerina_backend.go
Wires module resolution through moduleResolver and DependencyGraph, builds internal topologicallySortedModuleList, replaces prior exported ordering API, implements two-phase compilation (Phase1 sequential; Phase2 concurrent), and updates backend iteration to use the new field.
Module context, document parsing & import extraction
projects/module_context.go, projects/document_context.go
Introduces Phase1/Phase2 split, parallel document parsing with concurrency-safe syntax tree collection, new importedSymbols field and helpers to populate module load requests from source/test documents.
Test helpers & test-only exports
projects/export_test.go, projects/build_project_test.go
Adds test-only TopologicallySortedModuleNames() and TestMultiModuleDependencyOrder validating topological module order.
Fixtures & tests updates
projects/testdata/multi-module-project/*, projects/testdata/cyclicproject/*, projects/testdata/myproject/main.bal, projects/single_file_project_test.go
Adds multi-module and cyclic project fixtures and Bal sources; updates single-file test path references and inserts license header in a source file.
Misc
.gitignore
Add IDE ignore rule for .idea/.

Sequence Diagram(s)

sequenceDiagram
    participant PkgRes as PackageResolution
    participant Resolver as ModuleResolver
    participant Graph as DependencyGraph
    participant PkgComp as PackageCompilation
    participant ModCtx as ModuleContext
    PkgRes->>PkgRes: collectModuleDescriptors()
    PkgRes->>Resolver: newModuleResolver(descriptors)
    PkgRes->>Resolver: resolveModuleLoadRequests()
    Resolver-->>PkgRes: importModuleResponse (resolved/unresolved)
    PkgRes->>Graph: add nodes & dependencies
    PkgRes->>Graph: ToTopologicallySortedList()
    Graph-->>PkgRes: sorted ModuleDescriptor list
    PkgRes->>PkgComp: set topologicallySortedModuleList
Loading
sequenceDiagram
    participant Backend as BallerinaBackend
    participant PkgComp as PackageCompilation
    participant ModCtx as ModuleContext
    participant Worker as Phase2Worker
    Backend->>PkgComp: iterate topologicallySortedModuleList
    loop Phase 1 (sequential per module)
        PkgComp->>ModCtx: resolveTypesAndSymbols() — parse, AST, symbol/type resolution
        ModCtx->>ModCtx: parseDocumentsParallel()
    end
    PkgComp->>Worker: spawn goroutines for analyzeAndDesugar()
    loop Phase 2 (concurrent)
        Worker->>ModCtx: create CFG
        Worker->>ModCtx: semantic analysis & CFG analysis
        Worker->>ModCtx: desugar
    end
    Worker-->>PkgComp: signal completion
    PkgComp->>PkgComp: collect diagnostics
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested reviewers

  • warunalakshitha
  • gimantha

Poem

🐰
I hopped through nodes and edges so bright,
Built sorted paths from morning to night,
Phase One parsed, Phase Two took wing,
Goroutines hummed — what joy they bring!
A rabbit cheers: modules aligned in flight.

🚥 Pre-merge checks | ✅ 1 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Description check ⚠️ Warning The PR description is incomplete. It only covers Purpose and Approach sections. Missing: Goals, User stories, Release note, Documentation, Training, Certification, Marketing, Automation tests, Security checks, Samples, Related PRs, Migrations, Test environment, and Learning sections from the template. Complete the PR description by adding the missing template sections, particularly Goals, Release note, Documentation, Automation tests, and Security checks as these are typically critical for code review.
Docstring Coverage ⚠️ Warning Docstring coverage is 64.71% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (1 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main change: adding support for compiling multi-module packages, which is the core objective of the PR.

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

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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.

Copy link

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

🧹 Nitpick comments (6)
projects/dependency_graph.go (2)

83-120: Nondeterministic node iteration order affects topological sort stability.

The computeTopologicalSort method iterates over g.nodes() which uses maps.Keys - this returns keys in an unspecified order. While the topological ordering will still be valid (dependencies before dependents), the specific order of nodes at the same dependency level will vary between runs.

This could cause:

  • Non-reproducible builds
  • Flaky tests that depend on module order
  • Harder debugging when order-sensitive issues occur
🔧 Suggested fix: sort nodes for deterministic traversal
 func (g *DependencyGraph[T]) computeTopologicalSort() ([]T, [][]T) {
 	nodes := g.nodes()
+	// Sort nodes for deterministic traversal order
+	slices.SortFunc(nodes, func(a, b T) int {
+		// Requires T to have a String() method or comparable ordering
+		// Alternatively, pass a comparator during graph construction
+		return strings.Compare(fmt.Sprint(a), fmt.Sprint(b))
+	})
 	visited := make(map[T]bool, len(nodes))

Alternatively, track insertion order in the builder and use that for iteration.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@projects/dependency_graph.go` around lines 83 - 120, computeTopologicalSort
iterates over g.nodes() (which calls maps.Keys) producing a nondeterministic
traversal; sort the node list before DFS so the visit order is stable: call a
deterministic sort on the slice returned by g.nodes() (or use a deterministic
ordering source) prior to the for _, node := range nodes loop so visit, inStack
and cycles are processed reproducibly by computeTopologicalSort/visit; ensure
any dependency iteration (for dep := range g.dependencies[vertex]) is also
deterministic (sort the dependency keys) to fully stabilize the topological
output.

29-35: Consider documenting the rootNode field usage.

The DependencyGraph struct has a rootNode field that is copied in build() but never used in the current implementation. If this is intended for future use, a brief comment would help. If unused, consider removing it to reduce confusion.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@projects/dependency_graph.go` around lines 29 - 35, The rootNode field on
struct DependencyGraph[T comparable] is currently copied in build() but never
used, which is confusing; either remove the rootNode field and any assignments
to it in build() (to eliminate dead state), or add a short doc comment above
rootNode explaining its intended future purpose (e.g., "stored copy of the
entry/root node for potential future traversal or reporting; currently unused")
so callers/maintainers understand why it exists—update build() accordingly to
stop assigning if removed or keep the assignment if you choose to retain the
documented field.
projects/module_context.go (3)

307-322: Test documents are parsed but results are discarded.

The test document parsing loop spawns goroutines that parse documents but don't collect the results. If test documents don't need syntax trees for the current workflow, consider whether parsing them here is necessary at all, or document why the side effects of parsing are needed.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@projects/module_context.go` around lines 307 - 322, The loop that iterates
testDocIDs and calls documentContext.parse() in goroutines (using
testDocContextMap, wg, sem) runs parses but never uses or stores any results;
either remove the loop if parsing test documents is unnecessary, or modify it to
collect and persist parse outputs (e.g., have documentContext.parse return or
populate a field with the syntax tree that you then store back into
testDocContextMap or a results slice) and ensure synchronization around writes;
if parse is only kept for side effects, add a clear comment above the loop
explaining those required side effects so future readers know why results are
discarded.

228-233: Phase 2 silently skips modules without logging.

When bLangPkg or compilerCtx is nil, Phase 2 returns silently. This could mask issues where Phase 1 failed unexpectedly. Consider adding a debug log or ensuring the compilation state reflects the skip.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@projects/module_context.go` around lines 228 - 233, compilePhase2 currently
returns silently when moduleCtx.bLangPkg or moduleCtx.compilerCtx is nil; add a
diagnostic to avoid masking Phase 1 failures by logging the skip and marking the
module state. Update compilePhase2 to check moduleCtx.bLangPkg and
moduleCtx.compilerCtx and when either is nil emit a debug/error log that
includes the module identifier (e.g., moduleCtx.name or moduleCtx.package path)
and reason, and also set or update a module-level flag/state (e.g.,
phase2Skipped, errored, or similar on moduleContext) so the rest of the pipeline
can detect the skip.

279-304: Nondeterministic syntax tree ordering may cause subtle issues.

The parallel parsing collects syntax trees into a shared slice under a mutex, but the order depends on goroutine scheduling. While buildBLangPackage merges declarations, the order of TopLevelNodes, Functions, etc. in the resulting BLangPackage will be nondeterministic across runs.

If downstream processing (symbol resolution, error reporting, code generation) is sensitive to declaration order, this could cause flaky behavior or non-reproducible builds.

🔧 Suggested fix: preserve deterministic ordering
-	var (
-		mu          sync.Mutex
-		wg          sync.WaitGroup
-		syntaxTrees []*tree.SyntaxTree
-	)
+	var wg sync.WaitGroup
+	// Pre-allocate slice to preserve document order
+	syntaxTrees := make([]*tree.SyntaxTree, len(srcDocIDs))

 	// Parse source documents - collect syntax trees
-	for _, docID := range srcDocIDs {
+	for i, docID := range srcDocIDs {
 		docCtx := srcDocContextMap[docID]
 		if docCtx == nil {
 			continue
 		}

 		wg.Add(1)
-		go func(dc *documentContext) {
+		go func(idx int, dc *documentContext) {
 			defer wg.Done()
 			sem <- struct{}{}        // acquire semaphore
 			defer func() { <-sem }() // release semaphore

 			st := dc.parse()
-			if st != nil {
-				mu.Lock()
-				syntaxTrees = append(syntaxTrees, st)
-				mu.Unlock()
-			}
-		}(docCtx)
+			syntaxTrees[idx] = st // nil is fine, filtered below
+		}(i, docCtx)
 	}

Then filter nils after wg.Wait():

wg.Wait()
// Filter nil entries while preserving order
result := syntaxTrees[:0]
for _, st := range syntaxTrees {
    if st != nil {
        result = append(result, st)
    }
}
return result
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@projects/module_context.go` around lines 279 - 304, The parallel parse loop
appends SyntaxTree results in goroutines which yields nondeterministic ordering;
fix by making collection deterministic: preallocate syntaxTrees with length
len(srcDocIDs) and in the for loop capture the loop index (e.g., i) and write
the parsed result into syntaxTrees[i] inside the goroutine after calling
dc.parse(); after wg.Wait(), build the final ordered slice by iterating
syntaxTrees and appending non-nil entries (preserving original srcDocIDs order)
before passing them to buildBLangPackage or other downstream code that reads
TopLevelNodes/Functions.
projects/module_resolver.go (1)

88-104: Redundant check in resolveRequest.

The method checks isRootPackageModule which already verifies the module exists in moduleNames, then immediately checks moduleNames[request.moduleName] again. This is safe but slightly redundant.

♻️ Minor simplification
 func (r *moduleResolver) resolveRequest(request *moduleLoadRequest) *importModuleResponse {
-	// Check if this is a root package module
-	if r.isRootPackageModule(request.orgName, request.moduleName) {
-		modDesc, ok := r.moduleNames[request.moduleName]
-		if ok {
-			return &importModuleResponse{
-				moduleDesc: modDesc,
-				resolved:   true,
-			}
-		}
+	// Check org matches (or is nil) before looking up module
+	if request.orgName != nil && !request.orgName.Equals(r.rootPkgDesc.Org()) {
+		return &importModuleResponse{resolved: false}
+	}
+	
+	if modDesc, ok := r.moduleNames[request.moduleName]; ok {
+		return &importModuleResponse{
+			moduleDesc: modDesc,
+			resolved:   true,
+		}
 	}

 	// Module not found in root package
 	return &importModuleResponse{
 		resolved: false,
 	}
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@projects/module_resolver.go` around lines 88 - 104, In resolveRequest, avoid
the redundant double lookup of r.moduleNames by first fetching modDesc with
"modDesc, ok := r.moduleNames[request.moduleName]" and then checking ok &&
r.isRootPackageModule(request.orgName, request.moduleName); if true return an
importModuleResponse using the already-fetched modDesc; otherwise return the
existing unresolved response. This keeps the same behavior but eliminates the
repeated map access while still using the isRootPackageModule, moduleNames map,
resolveRequest method, and importModuleResponse struct.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@projects/build_project_test.go`:
- Around line 616-618: The test fixture path is incorrect: update the
occurrences where projectPath is set (currently filepath.Join("testdata",
"multimoduleproject")) and any subsequent filepath.Abs calls that derive absPath
in projects/build_project_test.go to use the correct fixture directory name
"testdata/multi-module-project" (matching the usage around the later block at
lines ~728-730); ensure both places (the block setting projectPath/absPath near
the 616–618 range and the similar block near 667–669) are changed so the
projectPath variable and resulting absPath reference "multi-module-project".

In `@projects/document_context.go`:
- Around line 178-185: The org name path creates a PackageOrg with the raw text
from orgNameNode.OrgName().Text(), but quoted identifiers are not normalized
like module parts are; update the code in the branch that constructs org (where
NewPackageOrg is called) to strip a leading '\'' from
orgNameNode.OrgName().Text() (e.g., use strings.TrimPrefix(text, "'") or
equivalent) before passing it to NewPackageOrg so quoted org names match the
module-name normalization used elsewhere.

In `@projects/package_resolution.go`:
- Around line 125-131: Replace the blank assignment for
r.moduleDependencyGraph.FindCycles() with a real variable (e.g., cycles :=
r.moduleDependencyGraph.FindCycles()), check if len(cycles) > 0, build
Diagnostic entries describing each detected cycle (use your project's Diagnostic
type and a suitable diagnostic code such as a cycle/DependencyCycle code), set
r.diagnosticResult = NewDiagnosticResult(diags) with those diagnostics, and
prevent continuing with an invalid topological order (e.g., do not set
r.topologicallySortedModuleList or return/propagate an error so compilation
stops). Ensure you reference the existing symbols:
r.moduleDependencyGraph.FindCycles(), r.topologicallySortedModuleList, and
r.diagnosticResult = NewDiagnosticResult(nil) when replacing the nil with the
actual diagnostics.
- Around line 109-123: The map currently keys moduleContext entries by
modCtx.getDescriptor().String(), which is fragile and inefficient; change
descToCtx to type map[ModuleDescriptor]*moduleContext and use the descriptor
value itself as the key (assign descToCtx[modCtx.getDescriptor()] = modCtx in
the loop), then when building sorted use if modCtx, ok := descToCtx[desc]; ok {
sorted = append(sorted, modCtx) } so lookups use ModuleDescriptor directly
instead of string conversions; update the descToCtx make(...) call and both
places where you index/assign to descToCtx accordingly.

In `@projects/single_file_project_test.go`:
- Line 102: Update the incorrect testdata path references from
"testdata/single_file/main.bal" to "testdata/single-file/main.bal" inside the
tests; specifically edit the occurrences used by TestLoadSingleFile and
TestSingleFileTargetDirectory (where the projectPath or similar path variable is
assigned) so they match the hyphenated directory used elsewhere (e.g., where
projectPath is set to filepath.Join("testdata", "single-file", "main.bal")).

---

Nitpick comments:
In `@projects/dependency_graph.go`:
- Around line 83-120: computeTopologicalSort iterates over g.nodes() (which
calls maps.Keys) producing a nondeterministic traversal; sort the node list
before DFS so the visit order is stable: call a deterministic sort on the slice
returned by g.nodes() (or use a deterministic ordering source) prior to the for
_, node := range nodes loop so visit, inStack and cycles are processed
reproducibly by computeTopologicalSort/visit; ensure any dependency iteration
(for dep := range g.dependencies[vertex]) is also deterministic (sort the
dependency keys) to fully stabilize the topological output.
- Around line 29-35: The rootNode field on struct DependencyGraph[T comparable]
is currently copied in build() but never used, which is confusing; either remove
the rootNode field and any assignments to it in build() (to eliminate dead
state), or add a short doc comment above rootNode explaining its intended future
purpose (e.g., "stored copy of the entry/root node for potential future
traversal or reporting; currently unused") so callers/maintainers understand why
it exists—update build() accordingly to stop assigning if removed or keep the
assignment if you choose to retain the documented field.

In `@projects/module_context.go`:
- Around line 307-322: The loop that iterates testDocIDs and calls
documentContext.parse() in goroutines (using testDocContextMap, wg, sem) runs
parses but never uses or stores any results; either remove the loop if parsing
test documents is unnecessary, or modify it to collect and persist parse outputs
(e.g., have documentContext.parse return or populate a field with the syntax
tree that you then store back into testDocContextMap or a results slice) and
ensure synchronization around writes; if parse is only kept for side effects,
add a clear comment above the loop explaining those required side effects so
future readers know why results are discarded.
- Around line 228-233: compilePhase2 currently returns silently when
moduleCtx.bLangPkg or moduleCtx.compilerCtx is nil; add a diagnostic to avoid
masking Phase 1 failures by logging the skip and marking the module state.
Update compilePhase2 to check moduleCtx.bLangPkg and moduleCtx.compilerCtx and
when either is nil emit a debug/error log that includes the module identifier
(e.g., moduleCtx.name or moduleCtx.package path) and reason, and also set or
update a module-level flag/state (e.g., phase2Skipped, errored, or similar on
moduleContext) so the rest of the pipeline can detect the skip.
- Around line 279-304: The parallel parse loop appends SyntaxTree results in
goroutines which yields nondeterministic ordering; fix by making collection
deterministic: preallocate syntaxTrees with length len(srcDocIDs) and in the for
loop capture the loop index (e.g., i) and write the parsed result into
syntaxTrees[i] inside the goroutine after calling dc.parse(); after wg.Wait(),
build the final ordered slice by iterating syntaxTrees and appending non-nil
entries (preserving original srcDocIDs order) before passing them to
buildBLangPackage or other downstream code that reads TopLevelNodes/Functions.

In `@projects/module_resolver.go`:
- Around line 88-104: In resolveRequest, avoid the redundant double lookup of
r.moduleNames by first fetching modDesc with "modDesc, ok :=
r.moduleNames[request.moduleName]" and then checking ok &&
r.isRootPackageModule(request.orgName, request.moduleName); if true return an
importModuleResponse using the already-fetched modDesc; otherwise return the
existing unresolved response. This keeps the same behavior but eliminates the
repeated map access while still using the isRootPackageModule, moduleNames map,
resolveRequest method, and importModuleResponse struct.

Copy link

@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.

🧹 Nitpick comments (3)
projects/testdata/multi-module-project/main.bal (1)

17-18: Import style inconsistency.

The imports use two different styles: multimoduleproject.services (without org) and testorg/multimoduleproject.storage (with org). If this is intentional to test both import formats, consider adding a comment. Otherwise, consider using a consistent format.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@projects/testdata/multi-module-project/main.bal` around lines 17 - 18, The
two import statements use inconsistent styles: multimoduleproject.services (no
org) and testorg/multimoduleproject.storage (with org); either make them
consistent by using the same import format for both modules (e.g., change
multimoduleproject.services to testorg/multimoduleproject.services or change
testorg/multimoduleproject.storage to multimoduleproject.storage) or add a brief
inline comment explaining that the mixed styles are intentional for testing;
update the imports referencing multimoduleproject.services and
testorg/multimoduleproject.storage accordingly.
projects/dependency_graph.go (1)

83-120: Topological sort iteration order is non-deterministic.

The nodes() method returns map keys in non-deterministic order (line 84, 113). While the resulting topological sort is still valid, different runs may produce different orderings for nodes at the same dependency level. This is typically acceptable but worth noting if deterministic output is needed for debugging or testing.

If deterministic ordering is desired, consider sorting nodes before iteration.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@projects/dependency_graph.go` around lines 83 - 120, The topological
traversal uses g.nodes() which iterates map keys non-deterministically causing
varying order across runs; to make results deterministic, sort the slice
returned by nodes() before using it in computeTopologicalSort (so sort the local
variable nodes prior to the for _, node := range nodes loop), ensuring
visit/visit-related logic (in computeTopologicalSort and its inner visit
closure) operates over the sorted order; pick a stable comparator for T (or
require/convert to a comparable string/int key) and apply that sort step to
guarantee consistent sorted and cycles output.
projects/module_context.go (1)

266-326: Non-deterministic syntax tree ordering may cause inconsistent compilation results.

The parallel parsing collects syntax trees in completion order, which is non-deterministic. While buildBLangPackage merges all compilation units, the order could affect:

  1. The order of top-level nodes in the resulting BLangPackage
  2. Potentially, which PackageID is used (first syntax tree's ID is taken)

Consider sorting the syntax trees by document ID after collection to ensure deterministic ordering.

♻️ Proposed fix to ensure deterministic ordering
 	wg.Wait()
+	
+	// Sort by document path/name to ensure deterministic ordering
+	slices.SortFunc(syntaxTrees, func(a, b *tree.SyntaxTree) int {
+		// Assuming SyntaxTree has a method to get its source path/name
+		return strings.Compare(a.FilePath(), b.FilePath())
+	})
+	
 	return syntaxTrees

Note: Adjust the comparison function based on the actual SyntaxTree API for obtaining the file path or document identifier.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@projects/module_context.go` around lines 266 - 326, The parsing goroutines
append syntax trees in completion order causing non-deterministic ordering; to
fix, change parseDocumentsParallel so you store results into a map keyed by
DocumentID (e.g., map[DocumentID]*tree.SyntaxTree) instead of appending directly
to the shared slice, and after wg.Wait() build the returned slice in a
deterministic order by iterating srcDocIDs (or another stable ordering) and
appending map[docID] when present; update references to the local variable
syntaxTrees accordingly and ensure the goroutines set the map entry (protected
by mu) using the input docID or the dc identity rather than relying on parse
completion order.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@projects/package_resolution.go`:
- Around line 125-131: The call to r.moduleDependencyGraph.FindCycles()
currently discards the result, so when cycles exist the compiler continues;
update the code to check the returned cycles slice and, if non-empty, populate
r.diagnosticResult with a Diagnostic (or Diagnostics) describing each cycle (use
DiagnosticCode for cycle errors), and prevent or flag further processing (do not
silently set r.topologicallySortedModuleList to sorted without handling cycles).
Specifically, use the result of r.moduleDependencyGraph.FindCycles(), convert
each cycle into a diagnostic entry (with clear message and location
information), add them to the value returned by NewDiagnosticResult(...) or
create the result from those diagnostics, and ensure subsequent logic respects
r.diagnosticResult when cycles are present.
- Around line 109-123: Replace the string-key map with a ModuleDescriptor-key
map to avoid using String(): change descToCtx to type
map[ModuleDescriptor]*moduleContext (e.g., descToCtx :=
make(map[ModuleDescriptor]*moduleContext, len(pkgCtx.moduleIDs))), use
modCtx.getDescriptor() (the ModuleDescriptor) as the key when populating
descToCtx, and when building sorted iterate over sortedDescs (which are
ModuleDescriptor values) and look up descToCtx[desc] instead of
descToCtx[desc.String()]. Ensure all key lookups and assignments use the
ModuleDescriptor type consistently (symbols: descToCtx, moduleContext,
pkgCtx.moduleIDs, pkgCtx.moduleContextMap, sortedDescs, modCtx.getDescriptor()).

---

Nitpick comments:
In `@projects/dependency_graph.go`:
- Around line 83-120: The topological traversal uses g.nodes() which iterates
map keys non-deterministically causing varying order across runs; to make
results deterministic, sort the slice returned by nodes() before using it in
computeTopologicalSort (so sort the local variable nodes prior to the for _,
node := range nodes loop), ensuring visit/visit-related logic (in
computeTopologicalSort and its inner visit closure) operates over the sorted
order; pick a stable comparator for T (or require/convert to a comparable
string/int key) and apply that sort step to guarantee consistent sorted and
cycles output.

In `@projects/module_context.go`:
- Around line 266-326: The parsing goroutines append syntax trees in completion
order causing non-deterministic ordering; to fix, change parseDocumentsParallel
so you store results into a map keyed by DocumentID (e.g.,
map[DocumentID]*tree.SyntaxTree) instead of appending directly to the shared
slice, and after wg.Wait() build the returned slice in a deterministic order by
iterating srcDocIDs (or another stable ordering) and appending map[docID] when
present; update references to the local variable syntaxTrees accordingly and
ensure the goroutines set the map entry (protected by mu) using the input docID
or the dc identity rather than relying on parse completion order.

In `@projects/testdata/multi-module-project/main.bal`:
- Around line 17-18: The two import statements use inconsistent styles:
multimoduleproject.services (no org) and testorg/multimoduleproject.storage
(with org); either make them consistent by using the same import format for both
modules (e.g., change multimoduleproject.services to
testorg/multimoduleproject.services or change testorg/multimoduleproject.storage
to multimoduleproject.storage) or add a brief inline comment explaining that the
mixed styles are intentional for testing; update the imports referencing
multimoduleproject.services and testorg/multimoduleproject.storage accordingly.

@azinneera azinneera marked this pull request as draft February 18, 2026 17:47
@azinneera azinneera force-pushed the prj_api-m3 branch 2 times, most recently from 2625cb2 to 27f8045 Compare February 19, 2026 06:56
@codecov
Copy link

codecov bot commented Feb 19, 2026

Codecov Report

❌ Patch coverage is 84.28571% with 44 lines in your changes missing coverage. Please review.
✅ Project coverage is 29.48%. Comparing base (f19bee3) to head (eba75f5).
⚠️ Report is 9 commits behind head on main.

Files with missing lines Patch % Lines
projects/module_context.go 71.66% 13 Missing and 4 partials ⚠️
projects/dependency_graph.go 78.87% 12 Missing and 3 partials ⚠️
projects/document_context.go 72.97% 5 Missing and 5 partials ⚠️
projects/package_resolution.go 95.83% 1 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #161      +/-   ##
==========================================
+ Coverage   29.13%   29.48%   +0.34%     
==========================================
  Files         254      257       +3     
  Lines       54309    54514     +205     
==========================================
+ Hits        15825    16074     +249     
+ Misses      37508    37451      -57     
- Partials      976      989      +13     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@azinneera azinneera marked this pull request as ready for review February 19, 2026 06:57
Copy link
Member

@heshanpadmasiri heshanpadmasiri left a comment

Choose a reason for hiding this comment

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

Minor nit other than that LGTM

@warunalakshitha
Copy link
Contributor

Please sync with latest changes in main branch

Copy link
Member

@heshanpadmasiri heshanpadmasiri left a comment

Choose a reason for hiding this comment

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

LGTM

Copy link

@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

♻️ Duplicate comments (1)
projects/package_resolution.go (1)

125-131: Cycle detection result is discarded — already tracked for a later phase.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@projects/package_resolution.go` around lines 125 - 131, The code calls
r.moduleDependencyGraph.FindCycles() and stores the result in cycles but
discards it (assigned to _), losing cycle diagnostics; update the block in
projects/package_resolution.go to capture and process the cycles instead of
ignoring them: use the returned value from FindCycles() (cycles) and convert it
into diagnostics to include in r.diagnosticResult (or pass into
NewDiagnosticResult) so cycle information is preserved for later phases,
referencing r.moduleDependencyGraph.FindCycles, the cycles variable,
r.diagnosticResult, and NewDiagnosticResult when implementing the fix.
🧹 Nitpick comments (3)
projects/module_context.go (1)

272-321: Preserve source-document order for deterministic outputs.
parseDocumentsParallel currently appends in completion order, which can shuffle compilation unit order and diagnostic ordering. Consider capturing by index and compacting after join.

♻️ Suggested deterministic collection
 func parseDocumentsParallel(
 	srcDocIDs []DocumentID,
 	srcDocContextMap map[DocumentID]*documentContext,
 	testDocIDs []DocumentID,
 	testDocContextMap map[DocumentID]*documentContext,
 ) []*tree.SyntaxTree {
 	var (
 		mu          sync.Mutex
 		wg          sync.WaitGroup
-		syntaxTrees []*tree.SyntaxTree
+		syntaxTrees = make([]*tree.SyntaxTree, len(srcDocIDs))
 	)
 
 	// Parse source documents - collect syntax trees
-	for _, docID := range srcDocIDs {
+	for i, docID := range srcDocIDs {
 		docCtx := srcDocContextMap[docID]
 		if docCtx == nil {
 			continue
 		}
 
 		wg.Add(1)
-		go func(dc *documentContext) {
+		go func(idx int, dc *documentContext) {
 			defer wg.Done()
 			st := dc.parse()
 			if st != nil {
-				mu.Lock()
-				syntaxTrees = append(syntaxTrees, st)
-				mu.Unlock()
+				mu.Lock()
+				syntaxTrees[idx] = st
+				mu.Unlock()
 			}
-		}(docCtx)
+		}(i, docCtx)
 	}
@@
 	wg.Wait()
-	return syntaxTrees
+	ordered := make([]*tree.SyntaxTree, 0, len(syntaxTrees))
+	for _, st := range syntaxTrees {
+		if st != nil {
+			ordered = append(ordered, st)
+		}
+	}
+	return ordered
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@projects/module_context.go` around lines 272 - 321, parseDocumentsParallel
appends syntax trees in goroutine completion order causing non-deterministic
ordering; instead allocate a temp slice with length len(srcDocIDs), spawn each
source parse goroutine passing its index (use go func(i int, dc
*documentContext) { ... }(i, docCtx)) and write the returned *tree.SyntaxTree
into tempTrees[i], then after wg.Wait() compact tempTrees into syntaxTrees by
iterating in index order and appending non-nil entries; reference
parseDocumentsParallel, documentContext.parse, and the syntaxTrees variable when
making these changes.
projects/package_resolution.go (2)

37-44: Remove block-level comments — extract the described blocks to named functions instead.

The file has repeated block-level comments (// Add all modules as nodes first, // Get all module load requests for this module, // Map descriptors to contexts for lookup, // Build sorted module list from sorted descriptors, and the three in newPackageResolution) that all violate the project guideline. When you need a comment to describe a block, the block should become a function with that name instead.

In newPackageResolution (Lines 37–44) the comments are doubly redundant — the three blocks are already extracted to self-describing named functions (collectModuleDescriptors, buildModuleDependencyGraph, resolveDependencies).

Inside buildModuleDependencyGraph, the inner loops can be extracted:

♻️ Suggested extraction for buildModuleDependencyGraph
 func (r *PackageResolution) buildModuleDependencyGraph() {
 	pkgCtx := r.rootPackageContext
 	builder := newDependencyGraphBuilder[ModuleDescriptor]()
-
-	// Add all modules as nodes first
-	for _, modID := range pkgCtx.moduleIDs {
-		modCtx := pkgCtx.moduleContextMap[modID]
-		if modCtx != nil {
-			builder.addNode(modCtx.getDescriptor())
-		}
-	}
-
-	// Process each module's imports and add edges
-	for _, modID := range pkgCtx.moduleIDs {
-		modCtx := pkgCtx.moduleContextMap[modID]
-		if modCtx == nil {
-			continue
-		}
-		fromDesc := modCtx.getDescriptor()
-		// Get all module load requests for this module
-		requests := modCtx.populateModuleLoadRequests()
-		requests = append(requests, modCtx.populateTestModuleLoadRequests()...)
-		// Resolve requests and add edges
-		responses := r.moduleResolver.resolveModuleLoadRequests(requests)
-		for _, resp := range responses {
-			if resp.resolved {
-				toDesc := resp.moduleDesc
-				// Only add edge if the dependency is a different module
-				if !fromDesc.Equals(toDesc) {
-					builder.addDependency(fromDesc, toDesc)
-				}
-			}
-		}
-	}
+	addModuleNodes(builder, pkgCtx)
+	r.addModuleEdges(builder, pkgCtx)
 	r.moduleDependencyGraph = builder.build()
 }
+
+func addModuleNodes(builder *dependencyGraphBuilder[ModuleDescriptor], pkgCtx *packageContext) {
+	for _, modID := range pkgCtx.moduleIDs {
+		if modCtx := pkgCtx.moduleContextMap[modID]; modCtx != nil {
+			builder.addNode(modCtx.getDescriptor())
+		}
+	}
+}
+
+func (r *PackageResolution) addModuleEdges(builder *dependencyGraphBuilder[ModuleDescriptor], pkgCtx *packageContext) {
+	for _, modID := range pkgCtx.moduleIDs {
+		modCtx := pkgCtx.moduleContextMap[modID]
+		if modCtx == nil {
+			continue
+		}
+		fromDesc := modCtx.getDescriptor()
+		requests := modCtx.populateModuleLoadRequests()
+		requests = append(requests, modCtx.populateTestModuleLoadRequests()...)
+		for _, resp := range r.moduleResolver.resolveModuleLoadRequests(requests) {
+			if resp.resolved && !fromDesc.Equals(resp.moduleDesc) {
+				builder.addDependency(fromDesc, resp.moduleDesc)
+			}
+		}
+	}
+}

As per coding guidelines: "Don't add comments explaining each line of code. If you need to add comments to describe a block of statements then extract them to a function with meaningful name."

Also applies to: 61-96, 102-131

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@projects/package_resolution.go` around lines 37 - 44, Remove the block-level
comments by extracting each commented block into a clearly named function and
calling it from the existing places; specifically, in newPackageResolution
replace the three commented blocks with calls to collectModuleDescriptors,
buildModuleDependencyGraph, and resolveDependencies (they already exist) and
delete the redundant comments, and inside buildModuleDependencyGraph break the
inner loops into small helper functions such as addModuleNodes (for "Add all
modules as nodes first"), getModuleLoadRequests (for "Get all module load
requests for this module"), mapDescriptorsToContexts, and addDependencyEdges (or
similar names) so each block has a self-describing function name rather than a
comment; update callers to use these new helpers and remove the original block
comments in the regions around functions referenced (collectModuleDescriptors,
buildModuleDependencyGraph, resolveDependencies and the blocks around lines
61-96 and 102-131).

26-27: moduleResolver should be a local variable, not a struct field.

r.moduleResolver is written once at line 39 and read once at line 87 (inside buildModuleDependencyGraph, which is called only from the constructor). The field unnecessarily extends the resolver's lifetime for the entire duration of PackageResolution. Pass it as a parameter to buildModuleDependencyGraph instead.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@projects/package_resolution.go` around lines 26 - 27, The field
r.moduleResolver on PackageResolution should be removed and made a local
variable in the constructor: stop storing moduleResolver as a struct field,
create it locally where it is currently assigned, and pass it into
buildModuleDependencyGraph as an explicit parameter instead of reading
r.moduleResolver; update buildModuleDependencyGraph's signature to accept the
*moduleResolver argument and update the call site in the constructor (where
r.moduleResolver is currently set and buildModuleDependencyGraph is invoked).
Remove any references to r.moduleResolver elsewhere (e.g., in PackageResolution)
so the resolver's lifetime is limited to the constructor-only usage.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@projects/dependency_graph.go`:
- Around line 27-72: Rename the exported type DependencyGraph to an unexported
dependencyGraph and update all code references accordingly; specifically change
the type declaration "type DependencyGraph[T comparable]" to "type
dependencyGraph[T comparable]" and update any constructors/usages, plus adjust
method receivers on nodes(), DirectDependencies(), ToTopologicallySortedList(),
and FindCycles() to use the new receiver type name; also make these methods
unexported if they are only package-internal (e.g., DirectDependencies ->
directDependencies, ToTopologicallySortedList -> toTopologicallySortedList,
FindCycles -> findCycles) and update any callers/tests in the package to use the
new names.

In `@projects/module_context.go`:
- Around line 56-61: The struct field importedSymbols is left nil and should be
initialized to an empty map in all constructors that create this module/context
structs; update the constructor(s) that build the object containing fields
bLangPkg, bPackageSymbol, compilerCtx, importedSymbols, birPkg to set
importedSymbols = make(map[string]model.ExportedSymbolSpace) (and likewise
initialize the same-named map fields in the other related constructors
referenced around lines 87-98 and 122-133) so callers never encounter nil-map
panics.

---

Duplicate comments:
In `@projects/package_resolution.go`:
- Around line 125-131: The code calls r.moduleDependencyGraph.FindCycles() and
stores the result in cycles but discards it (assigned to _), losing cycle
diagnostics; update the block in projects/package_resolution.go to capture and
process the cycles instead of ignoring them: use the returned value from
FindCycles() (cycles) and convert it into diagnostics to include in
r.diagnosticResult (or pass into NewDiagnosticResult) so cycle information is
preserved for later phases, referencing r.moduleDependencyGraph.FindCycles, the
cycles variable, r.diagnosticResult, and NewDiagnosticResult when implementing
the fix.

---

Nitpick comments:
In `@projects/module_context.go`:
- Around line 272-321: parseDocumentsParallel appends syntax trees in goroutine
completion order causing non-deterministic ordering; instead allocate a temp
slice with length len(srcDocIDs), spawn each source parse goroutine passing its
index (use go func(i int, dc *documentContext) { ... }(i, docCtx)) and write the
returned *tree.SyntaxTree into tempTrees[i], then after wg.Wait() compact
tempTrees into syntaxTrees by iterating in index order and appending non-nil
entries; reference parseDocumentsParallel, documentContext.parse, and the
syntaxTrees variable when making these changes.

In `@projects/package_resolution.go`:
- Around line 37-44: Remove the block-level comments by extracting each
commented block into a clearly named function and calling it from the existing
places; specifically, in newPackageResolution replace the three commented blocks
with calls to collectModuleDescriptors, buildModuleDependencyGraph, and
resolveDependencies (they already exist) and delete the redundant comments, and
inside buildModuleDependencyGraph break the inner loops into small helper
functions such as addModuleNodes (for "Add all modules as nodes first"),
getModuleLoadRequests (for "Get all module load requests for this module"),
mapDescriptorsToContexts, and addDependencyEdges (or similar names) so each
block has a self-describing function name rather than a comment; update callers
to use these new helpers and remove the original block comments in the regions
around functions referenced (collectModuleDescriptors,
buildModuleDependencyGraph, resolveDependencies and the blocks around lines
61-96 and 102-131).
- Around line 26-27: The field r.moduleResolver on PackageResolution should be
removed and made a local variable in the constructor: stop storing
moduleResolver as a struct field, create it locally where it is currently
assigned, and pass it into buildModuleDependencyGraph as an explicit parameter
instead of reading r.moduleResolver; update buildModuleDependencyGraph's
signature to accept the *moduleResolver argument and update the call site in the
constructor (where r.moduleResolver is currently set and
buildModuleDependencyGraph is invoked). Remove any references to
r.moduleResolver elsewhere (e.g., in PackageResolution) so the resolver's
lifetime is limited to the constructor-only usage.

ℹ️ Review info

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 23fc644 and eba75f5.

📒 Files selected for processing (25)
  • .gitignore
  • projects/ballerina_backend.go
  • projects/build_project_test.go
  • projects/dependency_graph.go
  • projects/document_context.go
  • projects/export_test.go
  • projects/module_context.go
  • projects/module_load_request.go
  • projects/module_resolver.go
  • projects/package_compilation.go
  • projects/package_resolution.go
  • projects/single_file_project_test.go
  • projects/testdata/cyclicproject/Ballerina.toml
  • projects/testdata/cyclicproject/main.bal
  • projects/testdata/cyclicproject/modules/moduleA/a.bal
  • projects/testdata/cyclicproject/modules/moduleB/b.bal
  • projects/testdata/multi-module-project/Ballerina.toml
  • projects/testdata/multi-module-project/main.bal
  • projects/testdata/multi-module-project/modules/services/svc.bal
  • projects/testdata/multi-module-project/modules/storage/db.bal
  • projects/testdata/multi-module-project/util/file-util.bal
  • projects/testdata/multi-module-project/utils.bal
  • projects/testdata/myproject/main.bal
  • projects/testdata/single-file/main-with-error.bal
  • projects/testdata/single-file/main.bal
🚧 Files skipped from review as they are similar to previous changes (12)
  • projects/testdata/multi-module-project/modules/services/svc.bal
  • projects/build_project_test.go
  • projects/testdata/cyclicproject/modules/moduleB/b.bal
  • projects/testdata/multi-module-project/main.bal
  • projects/document_context.go
  • projects/testdata/multi-module-project/util/file-util.bal
  • projects/export_test.go
  • projects/testdata/multi-module-project/utils.bal
  • .gitignore
  • projects/package_compilation.go
  • projects/single_file_project_test.go
  • projects/module_load_request.go

@warunalakshitha warunalakshitha merged commit 51e86a5 into ballerina-platform:main Feb 24, 2026
6 checks passed
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.

3 participants