feat: add brick_callers and brick_uncalled caller-status queries #7

Merged
trevon merged 3 commits from feat/caller-status into main 2026-09-25 02:51:17 +00:00
3 changed files with 196 additions and 2 deletions
Showing only changes of commit 6d4335d3ae - Show all commits

docs: record C/C++ and Rust resolution research and validation
Some checks failed
build / build-macos (pull_request) Has been cancelled
build / release (pull_request) Has been cancelled
build / build-linux (pull_request) Has been cancelled

Adds the audit, literature rationale, experiment contract, labeled-corpus
results, Fold/Paperbridge/Brick/YAMS coverage and dead-code spot checks,
model changes, identity changes, and remaining limits.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
trvon 2026-09-22 15:12:13 -06:00
Signed by: trevon
GPG key ID: 083EB1C25C575E81

View file

@ -182,7 +182,9 @@ The source-language regression corpus checks exact declarations, owners, and
dependencies across six language strata, including clean-negative files. Its
three-repeat benchmark runs in the quality gate. These labeled cases establish
the covered behaviors, not language-wide accuracy. See
`docs/research/language-correctness.md` for the acceptance evidence and limits.
`docs/research/language-correctness.md` for the acceptance evidence and limits,
and `docs/research/cpp-rust-resolution.md` for C/C++ and Rust resolution and the
agent-facing caller-status contract.
P4 extraction is a conservative P4_16 lexical preview, not a p4c frontend.
The optional Tree-sitter pilot does not preprocess macros and rejects all pilot

View file

@ -0,0 +1,191 @@
# C/C++ and Rust call resolution for agents
## Problem
A dead-code audit of a real C++ repository found that 93% of symbols had no
resolved incoming edge. Brick could not tell "unresolved" from "unused", so
reporting those symbols as dead would have produced the false findings the
audit was looking for. The audit on 2026-09-22 found these root causes:
| Cause | Where |
| --- | --- |
| C++ bare calls resolved only within one file; `#include` never authorized a cross-file call | `scan.rs` owner-chain lookup |
| Header prototypes not recorded; out-of-line `A::f` named `"A::f"` with a file owner | `source.rs` `declaration` |
| `T& f()` named `"& f(...)"` (fieldless `reference_declarator`) | `source.rs` |
| Every member call (`obj.f()`, `this->f()`, `self.m()`) External | `scan.rs` |
| Calls inside Rust macros (`assert!`, `format!`, `vec!`) dropped (`token_tree` skipped) | `source.rs` |
| `std::lock_guard<M> g(f());` hides `f()` (most vexing parse) | tree-sitter-cpp |
| `static_cast<T>(x)` recorded as a call | `source.rs` |
| No unresolved reason; per-caller External identity; no coverage metric | model |
| Linear node/edge scans and per-node ancestor walks | `scan.rs`, `source.rs` |
## Literature (paperbridge, arXiv/OpenAlex, 2026-09-22)
- Murphy, Notkin et al., "An empirical study of static call graph extractors",
TOSEM 1998, doi:10.1145/279310.279314. Extractors disagree widely, so each
edge keeps its backend and the resolution method that produced it.
- Grove, DeFouw, Dean, Chambers, "Call graph construction in object-oriented
languages", OOPSLA 1997, doi:10.1145/263698.264352; Grove and Chambers,
TOPLAS 2001, doi:10.1145/506315.506316. Receiver type sets bound dispatch.
Brick uses declared static types only, and treats virtual and trait targets as
unknown rather than enumerating overrides.
- Srivastava, "Unreachable procedures in object-oriented programming", LOPLAS
1992, doi:10.1145/161494.161517. Deadness is relative to roots and dispatch
assumptions. `brick_callers` therefore names its reasons and its evidence
basis instead of issuing a verdict.
- Creager and van Antwerpen, "Stack graphs: Name resolution at scale",
arXiv:2211.01224. This is file-local, build-free scope resolution. The C++
include-closure lookup and the Rust impl index follow the same idea at
much lower fidelity.
- Cai and Zhang, "A Cocktail Approach to Practical Call Graph Construction",
OOPSLA 2023, doi:10.1145/3622833, and Li, He et al., Rupta, CC 2024,
doi:10.1145/3640537.3641574. Function pointers (C) and trait/dyn dispatch
(Rust) need pointer or MIR analysis. That work belongs to Fold's compiler lane,
not to syntax.
- LocAgent (ACL 2025, doi:10.18653/v1/2025.acl-long.426) and CodexGraph
(NAACL 2025). Agents navigate code graphs effectively, which makes edge
provenance and uncertainty part of the agent contract.
## Experiment contract
```json
{
"hypothesis": "Include-visible C++ lookup, declared-receiver typing, Rust impl indexing and macro re-parsing raise labeled call recall without clean-negative regressions, and reasons make zero-caller symbols distinguishable from unresolved ones",
"owner": "source extractor and heuristic resolver; caller-status query",
"fixtures": ["tests/fixtures/cpp_xtu", "tests/fixtures/rust_methods", "tests/fixtures/languages", "tests/fixtures/rust_imports"],
"expected": {
"increases": ["labeled C++/Rust relationship precision and recall", "resolved call-site share on unlabeled repositories"],
"preserves": ["schema 4 reader contract", "Heuristic lane and 0.65 confidence", "existing labeled corpora", "stable IDs of unrelated and out-of-line C++ declarations"]
},
"decision": "promote in the default extractor"
}
```
## Results
The labeled corpora were authored before the implementation. Each has 3
repeats with stable full-model JSON, and the quality gate runs both.
| Corpus | Baseline (050b1b1) relationships | Candidate |
| --- | ---: | ---: |
| `cpp_xtu` (headers, out-of-line methods, namespaces, overloads, internal linkage, macros, receivers, bases, a duplicate-program clean negative, `std::string` / untyped receiver negatives) | 12/17, 6 unexpected (first label version) | 24/24 exact |
| `rust_methods` (`Self::`, fallible constructors, `Arc` fields, macro bodies, `dyn` trait, competing trait impls, `std::fs::File` shadowing a repository `File`, generics) | 13/23, precision 0.65 | 23/23 exact |
| `source_languages`, `rust_imports` | exact | exact |
Coverage on unlabeled repositories. These are coverage observations, not
precision claims:
| Repository | Functions with no resolved caller | Resolved call sites | Scan time |
| --- | --- | --- | --- |
| Fold `src/`+`include/` (C++, 2341 functions) | 66.3% → 39.0% | 9.5% → 23.1% | 8.8 s → 2.3 s |
| Paperbridge (Rust, 1175 functions) | 66.7% → 50.6% | 985 → 1606 sites | 2.5 s → 0.8 s |
| Brick (Rust, 329 functions) | 45.3% → 28.9% | 575 → 723 sites | 1.2 s → 0.4 s |
Precision spot checks drew random samples of receiver-typed edges: 12 from Fold
and 14 from Paperbridge. Every sampled edge was plausible, and the three Fold
receiver declarations that were checked by hand were correct.
`brick_uncalled` on Fold (open world) lists 5 `unreferenced` functions, and
grep confirms that all 5 have no uses. It first listed 8; the 3 false
candidates were vexing-parse calls, which extraction now recovers.
On Paperbridge (open world) it lists 0 unreferenced functions. In closed world
it lists 8, all of which are used only by the excluded `tests/` directories.
The candidates that exposed framework-attribute (`#[tool]`, `#[test]`) and
`deserialize_with` string uses are now reported as `unknown` with those
reasons.
## YAMS validation (C++, 15,607 functions including tests)
| | Baseline (050b1b1) | Candidate |
| --- | ---: | ---: |
| Scan time | 575 s | 23 s |
| Functions with no resolved caller | 84.5% | 49.7% |
| Resolved call sites | 2.5% | 13.9% |
| Files reported as incomplete syntax | 616 | 316 |
The YAMS run exposed six defects, each fixed with a regression test:
1. tree-sitter-cpp 0.23.4 parses a `= {}` default argument as a compound
literal with a zero-width MISSING type. The syntax is otherwise intact, so
it is now benign. Before, it dropped every such prototype and flagged half
the files. Of the 910 production files, 70 still have real errors, mostly
`#if` blocks that split braces.
2. `using namespace N;` and `using N::f;` were ignored. They now extend C++
lookup through the include closure, which resolved about 4,500 more sites.
3. The receiver type of a template parameter (`template <class Ptr> f(Ptr p)
{ p->g(); }`) was classified as a foreign type. That is unsound, because it
hid a real caller. Such receivers are now `receiver-type-unknown`. Type
aliases and typedefs resolve to their target, through a smart pointer where
applicable.
4. Functions reachable only through a macro body (`#define dlopen
dlopen_impl`) now count as referenced by name. So do identifiers inside
syntax-error regions.
5. A file with real syntax errors can misplace scope and linkage: the error
recovery for an `#if` that splits a brace moved a public function into an
anonymous namespace. Its functions now carry `source.parse-incomplete`, and
`brick_callers` reports that reason.
6. These are now `implicit-invocation`: coroutine protocol members
(`await_ready`, `final_suspend`, and the rest), range-for `begin`/`end`, ADL
`swap`, and constructors of explicit specializations (`Expected<void>`).
Out-of-line members of classes that are not in the repository
(`tinyfsm::Fsm<..>::set_initial_state`) are `outside-class-member`.
`brick_uncalled` results after the fixes:
- **Open world (default):** 2 candidates, and both are unused. One is
`neutralText`. The other is a Windows-only static `describeProcess`: every
call is made inside a `DaemonCommand` method, where the static member of the
same name wins under C++ class-scope lookup.
- **Closed world:** 357 candidates in `src/` and `include/`.
- The first random sample of 25 was 20 correct; its five errors led to
fixes 3 and 6 above.
- A second, independent sample of 30 was 29 correct, and the error was the
specialization constructor, now fixed.
- "Correct" means the function has no mentions outside its own
declarations, comments, log strings, or unrelated same-named functions.
## Model changes (schema 4, optional fields)
- **Function node attributes:**
- `source.parse-incomplete`;
- `source.qualifier` (out-of-line `A::f`, whose node name is now `f`; the
ID still hashes `A::f`);
- `source.qualified-name` (C++);
- `source.linkage: internal`;
- `source.virtual`;
- `source.implements` (Rust trait impl);
- `source.attributes` (Rust);
- `source.referenced-by-name`.
- **Call edge attributes:**
- `source.unresolved-reason`, with values `not-visible`, `declared-only`,
`ambiguous`, `macro`, `receiver-type-unknown`, `receiver-type-external`,
`member-not-found`, `qualifier-unknown`, `member-receiver`,
`not-in-repository` and `runtime-parameter-binding`;
- `source.resolution` (`declared-receiver-type`, `self-receiver`,
`impl-type`, `using-directive`);
- `source.extraction` (`macro-reparse`, `vexing-parse`).
- **`metrics.resolution`:** per language, the counts of call sites, resolved
sites, unresolved sites by reason, functions, and functions without resolved
callers.
### Intentional identity changes
- C++ and Rust External targets now include the unresolved reason in their
identity, so two differently failing sites never merge and lose a reason.
- Reference-returning C++ functions get correct names and therefore new IDs.
- The labeled-corpus runner renders `source.qualifier` in symbol identities.
## Limits
- All inferred relationships remain Heuristic.
- **C++:** no preprocessing, overload resolution, template instantiation,
ADL, `auto` inference beyond `new T`/`make_unique<T>`, or
`compile_commands.json` include paths.
- **Rust:** types are matched by leaf name, and there is no Cargo workspace,
`mod` tree, or re-export resolution.
- **Dispatch:** virtual and trait dispatch reach only the statically declared
method; overrides and trait implementations are reported as `unknown`, never
as dead.
- The compiler lane (Fold `ProjectCallGraph`, `fold-rust` MIR callees) and
candidate edges are the next stages (see `docs/ROADMAP.md`).

View file

@ -36,4 +36,5 @@ A Paperbridge search on 2026-09-22 (arXiv and OpenAlex) informed the C/C++
and Rust resolution work: Murphy and Notkin (TOSEM 1998), Grove et al. (OOPSLA
1997, TOPLAS 2001), Srivastava (LOPLAS 1992), Creager and van Antwerpen (stack
graphs, arXiv:2211.01224), Cai and Zhang (OOPSLA 2023), Li et al. (Rupta,
CC 2024), and LocAgent (ACL 2025).
CC 2024), and LocAgent (ACL 2025). See `cpp-rust-resolution.md` for how each
shaped the design.