feat: resolve C/C++ and Rust calls across files and typed receivers #3

Merged
trevon merged 2 commits from feat/cpp-rust-resolution into main 2026-09-22 21:14:53 +00:00
Owner

Summary

A dead-code audit found that 93% of C++ symbols had no resolved callers in Brick, so "unused" could not be told apart from "unresolved". This PR makes C/C++ and Rust call resolution good enough for agents to rely on. It also records why a call did not resolve.

C/C++

  • Calls resolve through the include closure. A header prototype pairs with its definition in another translation unit.

  • Lookup walks outward through class scopes (including in-repository bases), namespace scopes, and using namespace / using N::f scopes.

  • static and anonymous-namespace definitions stay file-local.

  • Member calls resolve only through explicitly declared receiver types:

    • parameters, locals, fields and this;
    • smart pointers;
    • auto x = new T / make_unique<T>;
    • type aliases.

    Template parameters stay unknown.

  • These calls are no longer lost: calls hidden by the most vexing parse (lock_guard g(f());), and calls in declarations with = {} default arguments (a benign tree-sitter recovery).

  • Casts are no longer counted as calls. Functions returning references and conversion operators now get correct names.

Rust

  • A crate-wide impl index resolves method calls on these receivers:

    • self;
    • typed parameters and let bindings;
    • struct fields, with autoderef through Box/Rc/Arc;
    • struct literals and Self-returning constructors;
    • dyn Trait.

    It also resolves Type::/Self:: associated calls.

  • A type name imported from another crate (for example std::fs::File next to a repository File) never matches a repository type, and generics are never guessed.

  • Calls inside expression macros (assert!, format!, vec!, …) are recovered by re-parsing the macro body.

Model (schema 4, optional fields only)

  • Call edges gain source.unresolved-reason, source.resolution and source.extraction.
  • metrics.resolution gives per-language call-site coverage.
  • Declarations gain facts used by the follow-up caller-status PR: qualified names, linkage, virtual, trait impls, attributes, name references and parse-incomplete.
  • Intentional identity changes:
    • C++/Rust External targets include the reason in their identity.
    • Reference-returning C++ functions get corrected names.
    • Out-of-line C++ methods keep their IDs.

Performance: index lookups replace linear scans and per-node ancestor walks, so a YAMS scan now takes 23 s instead of 575 s.

Results

Before After
cpp_xtu labeled corpus (new) 12/17 edges, 6 unexpected 24/24 exact
rust_methods labeled corpus (new) 13/23, precision 0.65 23/23 exact
YAMS C++ functions with no resolved caller 84.5% 49.7%
Fold C++ functions with no resolved caller 66.3% 39.0%
Paperbridge Rust functions with no resolved caller 66.7% 50.6%

The unlabeled-repository numbers are coverage only. Spot checks of receiver-typed edges on Fold and Paperbridge found no wrong targets.

Test plan

  • cargo fmt --check, cargo test and cargo clippy --all-targets -- -D warnings pass on this commit alone.
  • The source_languages, rust_imports, cpp_xtu and rust_methods labeled benchmarks are exact over 3 repeats with stable full-model JSON.
  • ./scripts/quality-gate.sh passes on the full stack.

Follow-up: #4 (caller-status queries) is stacked on this branch.

🤖 Generated with Claude Code

## Summary A dead-code audit found that 93% of C++ symbols had no resolved callers in Brick, so "unused" could not be told apart from "unresolved". This PR makes C/C++ and Rust call resolution good enough for agents to rely on. It also records *why* a call did not resolve. **C/C++** - Calls resolve through the include closure. A header prototype pairs with its definition in another translation unit. - Lookup walks outward through class scopes (including in-repository bases), namespace scopes, and `using namespace` / `using N::f` scopes. - `static` and anonymous-namespace definitions stay file-local. - Member calls resolve only through explicitly declared receiver types: - parameters, locals, fields and `this`; - smart pointers; - `auto x = new T` / `make_unique<T>`; - type aliases. Template parameters stay unknown. - These calls are no longer lost: calls hidden by the most vexing parse (`lock_guard g(f());`), and calls in declarations with `= {}` default arguments (a benign tree-sitter recovery). - Casts are no longer counted as calls. Functions returning references and conversion operators now get correct names. **Rust** - A crate-wide impl index resolves method calls on these receivers: - `self`; - typed parameters and `let` bindings; - struct fields, with autoderef through `Box`/`Rc`/`Arc`; - struct literals and `Self`-returning constructors; - `dyn Trait`. It also resolves `Type::`/`Self::` associated calls. - A type name imported from another crate (for example `std::fs::File` next to a repository `File`) never matches a repository type, and generics are never guessed. - Calls inside expression macros (`assert!`, `format!`, `vec!`, …) are recovered by re-parsing the macro body. **Model (schema 4, optional fields only)** - Call edges gain `source.unresolved-reason`, `source.resolution` and `source.extraction`. - `metrics.resolution` gives per-language call-site coverage. - Declarations gain facts used by the follow-up caller-status PR: qualified names, linkage, virtual, trait impls, attributes, name references and parse-incomplete. - **Intentional identity changes:** - C++/Rust External targets include the reason in their identity. - Reference-returning C++ functions get corrected names. - Out-of-line C++ methods keep their IDs. **Performance:** index lookups replace linear scans and per-node ancestor walks, so a YAMS scan now takes 23 s instead of 575 s. ## Results | | Before | After | |---|---|---| | `cpp_xtu` labeled corpus (new) | 12/17 edges, 6 unexpected | 24/24 exact | | `rust_methods` labeled corpus (new) | 13/23, precision 0.65 | 23/23 exact | | YAMS C++ functions with no resolved caller | 84.5% | 49.7% | | Fold C++ functions with no resolved caller | 66.3% | 39.0% | | Paperbridge Rust functions with no resolved caller | 66.7% | 50.6% | The unlabeled-repository numbers are coverage only. Spot checks of receiver-typed edges on Fold and Paperbridge found no wrong targets. ## Test plan - [x] `cargo fmt --check`, `cargo test` and `cargo clippy --all-targets -- -D warnings` pass on this commit alone. - [x] The `source_languages`, `rust_imports`, `cpp_xtu` and `rust_methods` labeled benchmarks are exact over 3 repeats with stable full-model JSON. - [x] `./scripts/quality-gate.sh` passes on the full stack. Follow-up: #4 (caller-status queries) is stacked on this branch. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
C++ calls now resolve through the include closure: header prototypes pair
with definitions in other translation units, lookup walks class (including
in-repository bases), namespace, and `using` scopes outward, and static or
anonymous-namespace definitions stay file-local. Member calls resolve only
through explicitly declared receiver types (parameters, locals, fields,
this, smart pointers, aliases); template parameters stay unknown. Calls
hidden by the most vexing parse are recovered, casts are no longer calls,
reference-returning and conversion-operator declarations are named
correctly, and benign `= {}` default-argument recoveries no longer drop
prototypes or flag files.

Rust method calls resolve through a crate-wide impl index for self, typed
parameters and lets, struct fields, Self-returning constructors, dyn
traits, and Type::/Self:: associated calls, with foreign imports and
generics never guessed. Calls inside expression macros are re-parsed.

Every unresolved call records source.unresolved-reason, weaker resolutions
record source.resolution/source.extraction, and metrics.resolution reports
per-language call-site coverage. Declarations gain facts consumed by
caller-status queries (qualified names, linkage, virtual, trait impls,
attributes, name references, parse-incomplete). C++/Rust External targets
include the reason in their identity; out-of-line C++ methods keep their
IDs.

Linear node/edge scans and per-node ancestor walks are replaced with
indexes, cutting a YAMS scan from 575 s to 23 s.

New labeled cpp_xtu and rust_methods corpora run three times in the
quality gate.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
docs: describe C/C++ and Rust resolution limits and next stages
Some checks failed
build / build-linux (pull_request) Successful in 1m3s
build / build-macos (pull_request) Has been cancelled
build / release (pull_request) Has been cancelled
7c664cb5a0
README limits now describe include-closure C++ lookup, declared-receiver
member resolution, Rust macro re-parsing, unresolved reasons, and
metrics.resolution, plus the remaining adapter gaps. The roadmap records
candidate edges, the Fold compiler call lane, and compile_commands/Cargo
module resolution as next stages; literature notes cite the call-graph and
name-resolution papers found through paperbridge.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
trevon merged commit fb0dcdc19d into main 2026-09-22 21:14:53 +00:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
trevon/brick!3
No description provided.