Cross-Module Resolution
trace_flow uses a breadth-first search (BFS) to trace execution flow from an entry point through the call graph. 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{"stdlib / builtin?"}
N -- Yes --> M
N -- No --> 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 FlowStep list"]
Key Data Structures
_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 |
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") |
BFS Traversal (trace_flow)
- Symbol resolution —
_find_symbol_locationmaps the entry name to a(module_dotted, line)pair. If not found, returns an empty list. - Queue initialization — The entry is enqueued at depth 0 and immediately appended to
stepsas the rootFlowStep. - Main loop — Each iteration dequeues a symbol, calls
find_calleesto get direct callees, and for each unvisited callee appends aFlowStepand enqueues atdepth + 1. - Depth cap — When
depth >= max_depth, the node is dequeued but its callees are not explored. - Cross-module — After same-package callees are processed,
_resolve_cross_module_calleesfollows 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)
For each callee found in the current BFS node:
- Skip builtins —
_is_stdlib_or_builtinfilters stdlib and built-in symbols. - Locate context — Finds the
ModuleInfofor the calling module viafind_module_for_symbol. - Skip internal — If the callee is already defined in the current package, skip it (handled by the main BFS).
- 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_path(the file where the symbol was expected) with tree-sitter. - Walk top-level
import_from_statementnodes looking for one that imports 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.
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.