Part of #9. Phase 1 — level 1. The hardest thinking of level 1 — budget accordingly.
Learning goals
Name resolution — the classic compiler problem, hand-built: scope chains, import maps, receiver
type inference. Plus mutation discipline in Rust: backfilling callee_signature in place while
iterating the symbol table teaches you why the borrow checker exists (collect-then-apply, split
borrows, or id-indexed updates — pick one and understand the trade).
Task
Resolve each recorded call site to a declaration and build the identity-only call graph:
- Build a resolution context per module: the
use-map (alias → full path), the enclosing
module path, cargo metadata's crate roots.
- Resolve, in order of confidence: fully-qualified paths (
crate::a::b::f(...)) → exact;
bare names through the scope chain + use-map → exact; Type::assoc_fn(...) incl. ::new →
exact; method calls recv.method(...) → infer the receiver type from locals/params you
already recorded, look up inherent methods first, then trait impls.
- Dispatch policy decision (record it): for a
dyn Trait receiver, expand to the trait
method of every KNOWN implementing type (your CHA/RTA analog — declared-types-only vs
instantiated-types-only changes precision; the skill says surface this choice, not bury it).
- Backfill
callee_signature in place; emit coalesced edges (source, target, weight) with
provenance: ["canrs"]. Unresolved sites (closures, fn pointers, macro-generated) stay
recorded with None — an explicit fallback, never a guess, never a crash.
- Gate the whole thing behind
-a 2; -a 1 stays symbol-table-only with call_graph: [].
Teacher's notes
- Do NOT chase completeness: exact on monomorphic/static calls + explicit unresolved fallback
beats clever-but-wrong. The upgrade path (issue backlog: adopt ra_ap_hir as the oracle) is
planned; your hand-rolled resolver stays as the fast path and the thing you learned on.
signature_of() on BOTH sides is what makes edges byte-match — if a gate fails with a
near-miss string, the canonicalizer has two callers disagreeing.
Gate (call-graph gate)
- Zero dangling endpoints: every edge's source and target exists in the symbol table.
- Named expected edges asserted exactly: a cross-module call, a
Type::new call, an inherent
method call, a trait-method call under your dispatch policy.
- Every edge has non-empty provenance; callee_signature backfilled on resolved sites; output
still validates.
Part of #9. Phase 1 — level 1. The hardest thinking of level 1 — budget accordingly.
Learning goals
Name resolution — the classic compiler problem, hand-built: scope chains, import maps, receiver
type inference. Plus mutation discipline in Rust: backfilling
callee_signaturein place whileiterating the symbol table teaches you why the borrow checker exists (collect-then-apply, split
borrows, or id-indexed updates — pick one and understand the trade).
Task
Resolve each recorded call site to a declaration and build the identity-only call graph:
use-map (alias → full path), the enclosingmodule path,
cargo metadata's crate roots.crate::a::b::f(...)) → exact;bare names through the scope chain + use-map → exact;
Type::assoc_fn(...)incl.::new→exact; method calls
recv.method(...)→ infer the receiver type from locals/params youalready recorded, look up inherent methods first, then trait impls.
dyn Traitreceiver, expand to the traitmethod of every KNOWN implementing type (your CHA/RTA analog — declared-types-only vs
instantiated-types-only changes precision; the skill says surface this choice, not bury it).
callee_signaturein place; emit coalesced edges(source, target, weight)withprovenance: ["canrs"]. Unresolved sites (closures, fn pointers, macro-generated) stayrecorded with
None— an explicit fallback, never a guess, never a crash.-a 2;-a 1stays symbol-table-only withcall_graph: [].Teacher's notes
beats clever-but-wrong. The upgrade path (issue backlog: adopt
ra_ap_hiras the oracle) isplanned; your hand-rolled resolver stays as the fast path and the thing you learned on.
signature_of()on BOTH sides is what makes edges byte-match — if a gate fails with anear-miss string, the canonicalizer has two callers disagreeing.
Gate (call-graph gate)
Type::newcall, an inherentmethod call, a trait-method call under your dispatch policy.
still validates.