SSA-based generic resolver for reference resolution
## Problem to Solve Reference resolution — figuring out that `x()` calls `some_function` — is currently implemented per-language. Each language has its own resolver with its own approach to tracking variable bindings: | Language | Approach | Lines | |---|---|---| | Python | Custom symbol table visitor | 2,205 | | Ruby | Expression resolver + scope resolver + type map | 1,726 | | Kotlin | Expression resolver + file context | 2,166 | | Java | Expression resolver + file context | 1,206 | | C#, TS, Rust | Single-pass, limited resolution | ~1,000 | ~8,300 lines of resolution code. But the core question is the same for every language: *given a name at a call site, what definition was most recently bound to that name?* This is a reaching-definition problem. Compiler theory solved it decades ago with SSA (Static Single Assignment). ### SSA in 30 seconds ```python def caller(): x = foo x = bar x() # what does x call? ``` Simple: `x` was last assigned to `bar`, so `x()` calls `bar`. But add a branch: ```python def caller(): if condition: x = foo # ← block A else: x = bar # ← block B x() # ← block C (merge point) ``` Now `x` could be either `foo` or `bar`. SSA handles this with phi nodes: ``` Block A: write("x", foo) → x₁ = foo Block B: write("x", bar) → x₂ = bar Block C: read("x") → x₃ = φ(x₁, x₂) └─ "x is foo or bar — emit edges to both" ``` If both branches write the same value, the phi collapses trivially. The Braun et al. algorithm (CC 2013) builds this on the fly — no pre-computed CFG or dominance frontiers needed. Three operations: `write_variable`, `read_variable`, `seal_block`. ## Proposed Solution One generic SSA resolver parameterized by declarative rules. Per-language differences become configuration: ```rust struct PythonRules; impl HasRules for PythonRules { fn rules() -> ResolutionRules { ResolutionRules { import_strategies: vec![ ScopeFqnWalk, ExplicitImport, WildcardImport, SamePackage, SameFile, GlobalName, ], chain_mode: ChainMode::ValueFlow, receiver: ReceiverMode::Convention { name: "self", skip: true }, ..Default::default() } } } ``` The architecture: ``` CanonicalResult (defs, imports, refs, bindings) │ ▼ Scoped SSA Walker (one block per function/class scope, predecessor edges for scope nesting) │ write_variable / read_variable / seal_block │ ▼ SsaResolver (Braun et al.) phi insertion at join points trivial phi elimination │ ▼ RulesResolver + ImportStrategy chain (ScopeFqnWalk → ExplicitImport → WildcardImport → SamePackage → SameFile → GlobalName) │ ▼ Resolved edges inserted into CodeGraph ``` **Scoped walker** — one SSA block per function/class scope, not one flat block per file. A function's local `x = foo` can't collide with another function's `x = bar`. Predecessor edges connect inner scopes to outer scopes so name lookups walk outward. **Import strategy chain** — resolution tries strategies in order until one succeeds. Each strategy is a function from `(name, context)` to `Option<target_fqn>`. Languages differ only in which strategies they enable and in what order. **`EdgeSource`** — when the call site is at module level (no enclosing function), the source node is the File, producing `File → Definition` edges instead of `Definition → Definition`. ## Acceptance Criteria - [x] SSA engine: `write_variable`, `read_variable`, `seal_block`, phi insertion, trivial phi elimination - [x] Unit tests for SSA engine (single block, predecessor flow, phi at join, trivial collapse, loops, unsealed blocks, nested scopes) - [x] Scoped walker: one block per scope, predecessor edges for nesting, bindings written to correct scope - [x] `ResolutionRules` with `ImportStrategy`, `ChainMode`, `ReceiverMode` - [x] `RulesResolver<R: HasRules>` implementing `ReferenceResolver` - [x] Import strategy chain: ScopeFqnWalk, ExplicitImport, WildcardImport, SamePackage, SameFile, GlobalName - [x] `EdgeSource::File` for module-level calls - [x] Resolution rules for Python, Java, Kotlin - [x] Self-edges preserved for recursive calls, suppressed only for fallback sources
issue