Repository navigation
fix(cpp): preserve template ownership and guarded initializer calls - #5115
xRedLeader wants to merge 6 commits into
Conversation
Reuse a same-file declared member only for unique primary template owners with direct ordered type-parameter correspondence and exact parameterless primitive signatures. Keep specialized, overloaded, dependent and shadowed forms separate instead of stripping template arguments.
Apply recovered source ranges to resolver-created edges and parked C++ callsites, then carry their conditional evidence through cross-repository linking. Preserve existing parking deduplication and isolate the metadata transfer from other languages.
Track template-template parameter names without leaking their inner parameters. Preserve dependent local/parameter bindings as unknown during in-file call selection and defer unknown owner fields, preventing unrelated same-named methods from being selected while preserving auto receiver behavior.
|
Thanks for the pull request, @xRedLeader. A maintainer will review it soon. Want to talk it through while it is in review? Come join us on our Discord server. For longer-form discussion there is also GitHub Discussions. A couple of things that speed up review: make sure the test suite passes on Python 3.10 and 3.13, and that the change keeps extraction deterministic. |
There was a problem hiding this comment.
Graphify reviewed this change.
No issues found by static checks — no coupling regressions or blocking issues in the code graph. That is not the same as safe to merge: see what was not checked.
Not checked: tests were not run; formal verification proved 0 of 9 changed function(s) (1 sampled, not proven, 8 not verified).
Formal verification. PR-changed functions: 1/9 verified (0 proven, 1 may-equivalent, 0 distinguished) · 8 not verified (7 vacuous, 1 unsupported).
Not verified on this run: extract (vacuous: never exercised), extract\_cpp (vacuous: never exercised), \_cpp\_declared\_receiver\_type (vacuous: never exercised), \_cpp\_local\_binding\_ranges (vacuous: never exercised), \_cpp\_receiver\_chain (vacuous: never exercised), \_cpp\_type\_parameters\_in\_scope (vacuous: never exercised), \_extract\_generic (unsupported), \_get\_cpp\_func\_name (vacuous: never exercised).
Graphify review — findings
Adds C++ template-wrapper member ownership, so calls like worker.Tick<int>() and same-file qualified definitions such as Box<T>::Run() resolve through the existing declared-receiver rules. Dependent, unknown and ambiguous receivers, specializations and overloads stay unresolved rather than getting guessed targets. Adds bounded recovery for C++ initializers and argument lists broken by conditional directives, parsing separate byte-preserving branch views without evaluating defines or running a preprocessor. Recovery is capped at 128 conditional groups and 32 branch views, and malformed, conflicting or over-budget input keeps its original parse errors; recovered branches are recorded in metadata.cpp_conditional_recovery and guarded records carry metadata.cpp_conditions. Bumps the AST cache schema to 43 so older template and conditional-recovery records re-extract, and stops cross-repo member calls from binding across languages (a TypeScript call no longer binds to a Python declaration).
Review partial — this diff was larger than one review pass covers, so later files were not reviewed; some findings may be missing.
Analysis details — impact, health, verification
Impact & health
Graphify review
Impact — 5525 functions depend on the 1043 functions this change touches.
Health — this change adds coupling hotspots:
- worse:
extract()— 1178 callers, 72 callees - worse:
_extract_generic()— 21 callers, 54 callees - worse:
link_cross_repo_member_calls()— 37 callers, 11 callees - worse:
extract_cpp()— 41 callers, 6 callees - worse:
walk()— 1 callers, 86 callees - worse:
cache_dir()— 28 callers, 3 callees - worse:
walk_calls()— 1 callers, 42 callees - new:
_recover_cpp_conditional_initializers()— 1 callers, 7 callees
Verification — 5525 functions in the blast radius were not formally verified this run (proofs are advisory here).
Gate & verification
graphify gate
PASS — objectively clean (no health regressions, tests not run — proofs not run this pass (advisory)). Grounded, not self-assessed.
Advisory (not blocking):
- verification_scope: 4832 function(s) in the blast radius were not formally verified this run
Test selection
Test selection
543 of 543 test file(s) selected (100%) via static blast radius.
Escalated to a full run for safety (
noncode-change,uncovered-code) — the selection is not trustworthy on its own (see below). CI should run the whole suite.
tests/pi.test.ts— full-run-safetytests/test_4874_jvm_resolution.py— impact, full-run-safetytests/test_4909_reference_binding_scale.py— full-run-safetytests/test_4978_reconcile_cost.py— impact, full-run-safetytests/test_4992_ts_import_types.py— impact, full-run-safetytests/test_5072_splittable_under_cap_retry.py— full-run-safetytests/test_affected_cli.py— full-run-safetytests/test_affected_defines.py— full-run-safetytests/test_affected_member_seed.py— full-run-safetytests/test_affected_overrides.py— impact, full-run-safetytests/test_affected_path_file_node.py— full-run-safetytests/test_affected_seed_reference_forms.py— full-run-safetytests/test_agents_platform.py— full-run-safetytests/test_alias_folding.py— impact, full-run-safetytests/test_analyze.py— impact, full-run-safetytests/test_anthropic_custom_endpoint.py— full-run-safetytests/test_antigravity_install.py— full-run-safetytests/test_apm_fallback_version.py— full-run-safetytests/test_architecture_doc.py— full-run-safetytests/test_astro_extraction.py— impact, full-run-safetytests/test_astro_import_ids.py— impact, full-run-safetytests/test_atomic_canvas_export.py— full-run-safetytests/test_atomic_version_stamp.py— full-run-safetytests/test_atomic_writes.py— full-run-safetytests/test_backend_env_isolation.py— full-run-safetytests/test_backend_extras.py— full-run-safetytests/test_backend_opt_in.py— full-run-safetytests/test_benchmark.py— full-run-safetytests/test_benchmark_raw_graph.py— full-run-safetytests/test_benchmark_real_query.py— full-run-safetytests/test_binary_media_words.py— impact, full-run-safetytests/test_blade_extractor.py— impact, full-run-safetytests/test_build.py— impact, full-run-safetytests/test_build_located_semantic_identity.py— impact, full-run-safetytests/test_build_merge_dedup_scope.py— impact, full-run-safetytests/test_build_merge_ghost_fold_4955.py— impact, full-run-safetytests/test_build_merge_hyperedges_and_prune.py— impact, full-run-safetytests/test_build_merge_shrink_guard.py— impact, full-run-safetytests/test_builtin_global_type_refs.py— impact, full-run-safetytests/test_c_include_resolution.py— impact, full-run-safetytests/test_cache.py— impact, full-run-safetytests/test_cache_long_paths.py— impact, full-run-safetytests/test_cache_stale_import_target.py— impact, full-run-safetytests/test_cache_windows_case_4925.py— impact, full-run-safetytests/test_callflow_html.py— full-run-safetytests/test_cargo_introspect.py— full-run-safetytests/test_cargo_missing_manifest.py— full-run-safetytests/test_carried_hyperedge_remap.py— impact, full-run-safetytests/test_case_only_decl_ids_4904.py— impact, full-run-safetytests/test_case_sensitive_resolution.py— impact, full-run-safety- … and 493 more
non-code file(s) changed (
ARCHITECTURE.md) → running the full suite for safety (a code graph can't see config/fixture/data deps)
changed code file(s) with no mapped test (
ARCHITECTURE.md) — a coverage gap or a missing link — running the full suite rather than only the selected tests
Selection is safe under the controlled-regression assumption; always-run tests + a periodic full run are the backstops. Advisory — it never changes the check verdict.
Docs that may be stale (advisory)
CHANGELOG.md§ 0.8.50 (2026-06-27) (lines 1653-1670): references changed symbolsrecordsgraphify/skill-agents.md§ Step 9 - Save manifest, update cost tracker, clean up, and report (lines 639-749): references changed symbolsstampgraphify/skill-amp.md§ Step 9 - Save manifest, update cost tracker, clean up, and report (lines 639-749): references changed symbolsstampgraphify/skill-claw.md§ Step 9 - Save manifest, update cost tracker, clean up, and report (lines 642-752): references changed symbolsstampgraphify/skill-codex.md§ Step 9 - Save manifest, update cost tracker, clean up, and report (lines 643-753): references changed symbolsstampgraphify/skill-copilot.md§ Step 9 - Save manifest, update cost tracker, clean up, and report (lines 642-752): references changed symbolsstampgraphify/skill-droid.md§ Step 9 - Save manifest, update cost tracker, clean up, and report (lines 639-749): references changed symbolsstampgraphify/skill-kilo.md§ Step 9 - Save manifest, update cost tracker, clean up, and report (lines 642-752): references changed symbolsstampgraphify/skill-kiro.md§ Step 9 - Save manifest, update cost tracker, clean up, and report (lines 642-752): references changed symbolsstampgraphify/skill-opencode.md§ Step 9 - Save manifest, update cost tracker, clean up, and report (lines 634-744): references changed symbolsstamp
…and 10 more.
Formal verification
No difference found (not proven): No behavior difference found in link\_cross\_repo\_member\_calls (not a proof).
The verifier ran both versions of link\_cross\_repo\_member\_calls on many inputs and saw identical behavior every time. Strong evidence the change is safe, but evidence, not a proof.
Guarantee: Empirical: differential testing (both versions run on many generated inputs). A divergence on an untested input remains possible, so this is 'no counterexample found', not 'proven equivalent'.
Note: An input the sampler did not try could still differ.
Could not verify: Could not verify extract.
The verifier did not have enough to check extract, so it is saying so rather than guessing. No false assurance is the whole point.
Guarantee: No guarantee either way, this is an honest abstention, not a pass.
Note: Reason: no capturable inputs from the test suite; property tier: not verifiable: all 90 sampled inputs raised on both versions — the function never executed, so 'no divergence' would be vacuous (mostly TypeError — names the real obstacle, not a sampling gap)
Could not verify: Could not verify extract\_cpp.
The verifier did not have enough to check extract\_cpp, so it is saying so rather than guessing. No false assurance is the whole point.
Guarantee: No guarantee either way, this is an honest abstention, not a pass.
Note: Reason: no capturable inputs from the test suite; property tier: not verifiable: all 9 sampled inputs raised on both versions — the function never executed, so 'no divergence' would be vacuous (mostly NameError — names the real obstacle, not a sampling gap)
Could not verify: Could not verify \_cpp\_declared\_receiver\_type.
The verifier did not have enough to check \_cpp\_declared\_receiver\_type, so it is saying so rather than guessing. No false assurance is the whole point.
Guarantee: No guarantee either way, this is an honest abstention, not a pass.
Note: Reason: no capturable inputs from the test suite; property tier: not verifiable: all 220 sampled inputs raised on both versions — the function never executed, so 'no divergence' would be vacuous (mostly AttributeError — names the real obstacle, not a sampling gap)
Could not verify: Could not verify \_cpp\_local\_binding\_ranges.
The verifier did not have enough to check \_cpp\_local\_binding\_ranges, so it is saying so rather than guessing. No false assurance is the whole point.
Guarantee: No guarantee either way, this is an honest abstention, not a pass.
Note: Reason: no capturable inputs from the test suite; property tier: not verifiable: all 190 sampled inputs raised on both versions — the function never executed, so 'no divergence' would be vacuous (mostly AttributeError — names the real obstacle, not a sampling gap)
Could not verify: Could not verify \_cpp\_receiver\_chain.
The verifier did not have enough to check \_cpp\_receiver\_chain, so it is saying so rather than guessing. No false assurance is the whole point.
Guarantee: No guarantee either way, this is an honest abstention, not a pass.
Note: Reason: no capturable inputs from the test suite; property tier: not verifiable: all 190 sampled inputs raised on both versions — the function never executed, so 'no divergence' would be vacuous (mostly AttributeError — names the real obstacle, not a sampling gap)
Could not verify: Could not verify \_cpp\_type\_parameters\_in\_scope.
The verifier did not have enough to check \_cpp\_type\_parameters\_in\_scope, so it is saying so rather than guessing. No false assurance is the whole point.
Guarantee: No guarantee either way, this is an honest abstention, not a pass.
Note: Reason: no capturable inputs from the test suite; property tier: not verifiable: all 190 sampled inputs raised on both versions — the function never executed, so 'no divergence' would be vacuous (mostly AttributeError — names the real obstacle, not a sampling gap)
Could not verify: Could not verify \_extract\_generic.
The verifier did not have enough to check \_extract\_generic, so it is saying so rather than guessing. No false assurance is the whole point.
Guarantee: No guarantee either way, this is an honest abstention, not a pass.
Note: Reason: no capturable inputs from the test suite; property tier: parameter `config` is annotated `LanguageConfig` — outside the synthesizable primitive/collection set
Could not verify: Could not verify \_get\_cpp\_func\_name.
The verifier did not have enough to check \_get\_cpp\_func\_name, so it is saying so rather than guessing. No false assurance is the whole point.
Guarantee: No guarantee either way, this is an honest abstention, not a pass.
Note: Reason: no capturable inputs from the test suite; property tier: not verifiable: all 190 sampled inputs raised on both versions — the function never executed, so 'no divergence' would be vacuous (mostly AttributeError — names the real obstacle, not a sampling gap)
· 2 grounded finding(s) anchored inline below; 6 more finding(s) on lines outside this diff (see the check run).
| return selected | ||
|
|
||
|
|
||
| def _recover_cpp_conditional_initializers(path: Path, source: bytes, original: dict) -> dict: |
There was a problem hiding this comment.
_recover_cpp_conditional_initializers()
fans out to 7 callees (efferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
| stamp(entry, "file", "line") | ||
|
|
||
|
|
||
| def extract_cpp(path: Path) -> dict: |
There was a problem hiding this comment.
extract_cpp()
fans out to 6 callees (efferent coupling); 41 callers depend on it (afferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
Co-landing the scoped-alias/inherited-receiver work (#5118) with the template/ guarded-initializer work (#5115) required merging their _cpp_local_binding_ranges tuple: #5118 appends the lexical lookup scope, #5115 appends the qualified name and a dependent-type flag. Carry all three (receiver_type, lookup_scope, qualified_name, dependent), widen the type annotations to the 6-tuple, and move the dependent-flag read in the member-defer predicate to its new index. Full C++ suite (443 tests incl. #5098/#4916/#4946) green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… trio #5113 (Python subscript generic-arg count) and #5117/#5118/#5115 (C++ decl/def reconcile, scoped aliases + inherited receivers, template ownership + guarded initializers) all change per-file extractor output and land in one window, so they share a single bump to 43 chained over prior (42). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
Landed on |
C++ template wrappers can lose member ownership/reference-returning declarations, while directives inside initializer or argument lists can leave otherwise valid source partially extracted. This preserves template member families, matches narrowly proven same-file primary-template owners, and recovers bounded branch-wise syntax with original source positions and diagnostics. Dependent, unknown and ambiguous receivers remain unresolved.
Recovered call guards survive later resolution, deferred cross-repository calls and linking. This builds on #5098 and preserves its 0.9.105 follow-up.
Earlier verification
Commands included:
The broader run enumerated all test_cpp*.py files alongside the C# and cross-repository control modules.
Review follow-up
codex/cpp-template-conditionals-v105headb3133327, andv8base before editing. Follow-upb68d9d8eadds tests only.extract_cpp()adds one direct helper dependency to the five already present. The recovery helper owns bounded branch parsing, validation, and merging; its callees reuse the parser and existing extractor. No forwarding layers or changes to the coupling threshold were introduced.Boundaries
AST cache schema 44 retires stale records; rebuild existing graphs with
graphify extract . --code-only --force. Reconcile this bump if another extractor PR lands first.No macro evaluation, compiler preprocessing, build-configuration enumeration, overload/specialization resolution or runtime-dispatch claims. Recovery is capped at 128 groups/32 views; malformed, conflicting or unattributable cases retain errors. Primary-owner normalization supports only the documented exact, nondependent, parameterless signatures.
Conditional-recovery repair and node audit
9eea9f81fixes a confirmed failure with repeated calls in multiple conditional initializer lists: pair deduplication selected different first call sites across branch views, causing valid guards to fail validation and later declarations to disappear. Recovery now preserves each call site through the existing strict validation; ordinary extraction keeps its prior deduplication behavior.(file, line, label)records were found. Unlocated placeholders are not definitions, and bounded fixture/source audits are not a general proof for arbitrary C++.