Cross-Module Resolution
trace_flow uses a breadth-first search (BFS) to trace execution flow from an entry point through the call graph. The detail parameter must be one of VALID_DETAILS ("trace", "source", "compact"); invalid values raise ValueError. When cross_module=True, it resolves imported symbols across package boundaries — the most complex subsystem in axm-ast.
Algorithm Overview
flowchart TD
A["Entry point symbol"] --> B["Resolve to (module, line)"]
B --> C["Enqueue at depth 0"]
C --> D{"Queue empty?"}
D -- No --> E["Dequeue (symbol, depth, chain, pkg, module)"]
E --> F{"depth >= max_depth?"}
F -- Yes --> D
F -- No --> G["find_callees(pkg, symbol)"]
G --> H["For each callee"]
H --> I{"Already visited?"}
I -- Yes --> H
I -- No --> J["Append FlowStep + enqueue"]
J --> H
H -- Done --> K{"cross_module enabled?"}
K -- No --> D
K -- Yes --> L["_resolve_cross_module_callees"]
L --> M["For each imported callee"]
M --> N{"_try_resolve_callee → skip?"}
N -- Yes --> M
N -- No --> N2["_find_source_module"]
N2 --> O["_resolve_import → (path, dotted)"]
O --> P["_locate_symbol(path, name)"]
P --> Q{"Found?"}
Q -- Yes --> R["Append FlowStep"]
Q -- No --> S["_follow_reexport"]
S --> T{"Re-export found?"}
T -- Yes --> R
T -- No --> M
R --> M
M -- Done --> D
D -- Yes --> U["Optional: enrich with source"]
U --> V["Return (steps, truncated)"]
Key Data Structures
_ResolutionScope
A dataclass grouping per-iteration parameters for cross-module callee resolution. Created fresh in each BFS iteration and passed through _resolve_cross_module_callees → _resolve_single_cross_callee:
| Field | Type | Role |
|---|---|---|
current_mod |
str |
Dotted name of the module being processed |
current_pkg |
PackageInfo |
Package the current module belongs to |
original_pkg |
PackageInfo |
Top-level package (for import resolution) |
depth |
int |
Current BFS depth |
current_chain |
list[str] |
Ancestor symbol path from entry to here |
_CrossModuleContext
A dataclass carrying shared mutable BFS state, passed by reference so cross-module resolution can append steps and update visited sets without return values:
| Field | Type | Role |
|---|---|---|
visited |
set[tuple[str, str]] |
(module, symbol) pairs — prevents re-visiting |
queue |
deque |
BFS frontier: (symbol, depth, chain, pkg, module) |
steps |
list[FlowStep] |
Ordered results (depth-then-insertion) |
parse_cache |
dict[str, tuple] |
Avoids re-parsing the same file with tree-sitter |
detail |
str |
"trace" or "source" — controls enrichment |
exclude_stdlib |
bool |
Whether to skip stdlib/builtin callees |
pkg_symbols |
frozenset[str] |
Package-defined symbol names for stdlib filtering |
FlowStep
Each BFS node produces a FlowStep (Pydantic model):
| Field | Role |
|---|---|
name |
Symbol name |
module |
Dotted module path |
line |
Source line number |
depth |
BFS depth from entry |
chain |
Full ancestor path from entry to this step |
resolved_module |
Set when resolved cross-module |
source |
Function source text (only when detail="source" or detail="compact") |
BFS Traversal (trace_flow)
- Symbol resolution —
_find_symbol_locationmaps the entry name to a(module_dotted, line)pair. If not found, raisesValueError. - Queue initialization — The entry is enqueued at depth 0 and immediately appended to
stepsas the rootFlowStep. - Main loop — Each iteration dequeues a symbol, resolves callees via
_get_callees(which checks the pre-built index or falls back tofind_callees), and delegates filtering and enqueuing to_process_local_callees, which skips stdlib/visited symbols and appends aFlowStepfor each new discovery atdepth + 1. - Depth cap — When
depth >= max_depth,_check_frontier_truncatedtests whether the frontier node has expandable callees (not visited, not stdlib) and sets thetruncatedflag toTrueif so. The node's callees are not explored. - Cross-module — When
cross_moduleis enabled,_resolve_cross_module_calleesreceives a_ResolutionScope(bundling the current module, packages, depth, and chain) and follows imported symbols into external files. - Source enrichment — After the BFS completes, if
detail="source",_enrich_steps_with_sourcepatches each step with the actual function source text.
Cross-Module Resolution (_resolve_cross_module_callees)
The outer function receives (callees, scope: _ResolutionScope, ctx: _CrossModuleContext) and iterates over callees, delegating each to _resolve_single_cross_callee:
- Filter callee —
_try_resolve_calleeskips stdlib/builtins (_is_stdlib_or_builtin) and symbols already defined in the current package. - Locate context —
_find_source_modulefinds theModuleInfofor the calling module, first viafind_module_for_symbolby context name, then by matchingmodule_dotted_nameagainst the current module. For src-layout packages,module_dotted_namestrips the leadingsrc.component so names remain importable (e.g.mypkg.coreinstead ofsrc.mypkg.core). The same helper is reused by_collect_module_candidatesinaxm_ast.tools.searchto populate suggestion module names when the parser leavesmod.nameunset. - Resolve import —
_resolve_importmaps the symbol's import statement to(resolved_path, resolved_dotted). - Locate symbol —
_locate_symbolparses the target file with tree-sitter (single file, no full package traversal). - Follow re-exports — If not found,
_follow_reexportchases__init__.pyre-exports (one level deep). - Record — On success, deduplicates via
visitedand appends aFlowStepwithresolved_modulepopulated.
Cross-module steps are not re-enqueued
Resolved symbols are added to steps but not pushed back into the BFS queue. Cross-module resolution adds visibility into external dependencies without recursing into them.
Re-Export Following (_follow_reexport)
Handles the common pattern where __init__.py re-exports a symbol from a submodule:
- Parse
resolved_pathvia_parse_source_safe(the file where the symbol was expected) with tree-sitter. ReturnsNoneon parse failure. - Walk top-level nodes, delegating each to
_try_resolve_reexport_nodewhich checks if the node is animport_from_statementimporting the target symbol. - Resolve relative imports via
_resolve_relative_module. - Map the import module to a file path using
_module_to_path. - Call
_locate_symbolon the actual target file.
Single-level only
_follow_reexport follows one level of re-export. Deeply chained re-exports (A → B → C) will not be resolved beyond the first hop.
Workspace-Level Callee Search (find_callees_workspace)
find_callees_workspace(ws, symbol) iterates every package in a WorkspaceInfo and delegates to find_callees per package. Results are disambiguated by prefixing each CallSite.module with pkg_name::. A shared _parse_cache dict is threaded across all packages to avoid redundant tree-sitter parsing.
The CalleesTool (ast_callees) automatically attempts workspace-level analysis first via analyze_workspace; if the path is not a workspace root (raises ValueError), it falls back to single-package find_callees.
Parse Caching
The parse_cache dict in _CrossModuleContext is threaded through find_callees to avoid redundant tree-sitter parsing. During BFS, find_callees is called once per depth level per symbol — without caching, this would be quadratic in the worst case.
The cache key is the file path; the value is the parsed tree and source text.