fix(derive): #23 — type-discriminate rust_calls R2 (Type::assoc → impl-of-that-type only) - #466
Merged
Merged
Conversation
The R2 fallback resolved a path call `Type::assoc` by matching only its last "::"-segment against every same-file FUNCTION of that name — so `IndexEntry::new` bound to EVERY `fn new` in the file and std `HashSet::new` bound to a LOCAL `new` (receiver type ignored). On the self-graph this was the dominant resolution-is-a-function violation class: 1261 violating (src,resolvedVia) pairs, fan-out up to 8 — yet rust-calls is meant to be a function. Split R2 into two type-discriminated arms: - R2a (associated fn): match `Type::assoc` to the IMPL_BLOCK-of-that-type's HAS_METHOD member only, boundary-anchored (starts_with "Type::" for a leading segment, "::Type::" for an interior one — so `MyArc` never matches `impl Arc`). A foreign type with no local impl (`HashSet::new`) now yields NO row — correctly unresolved locally, instead of an unsound bind to a same-named local. - R2b (free fn): a path call to a file-MODULE free function (`utils::helper`) is kept flat on the last segment — module qualifiers have no node (inline mods are walked inline); impl methods are HAS_METHOD'd, never MODULE-CONTAINS'd, so R2b never fires for the assoc-fn case R2a now owns. R2 stays the EDB seam for rust_cross_methods_ctor / rust_receiver_typing — type discrimination only makes the `Type::new()` constructor resolution precise. Measured on a fresh current-HEAD rust graph (69,991 CALLs): rust-calls resolution-is-a-function violations 1261 → 10 (99.2%), fan-out 8 → 2; downstream rust-cross-method (237) / rust-dyn-dispatch (324) unchanged. Unit test rust_calls_r2_type_discriminated_assoc_fn added; all rust_* derive tests green. Residual (follow-up, NOT this change): the 10 remaining rust-calls violations are R1 (scope-walk) bare-name calls in parser.rs where a method `Parser::parse_term` and a free `fn parse_term` share a name — R1's owner-arm and file-MODULE-arm both fire. A distinct, smaller R1 precision nuance on the dominant resolver; deferred. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Disentinel
added a commit
that referenced
this pull request
Jun 21, 2026
… method calls (#467) R1's file-MODULE free-function arm fired for `self.method()` calls too: in a file with both `impl Parser { fn parse }` and a free `fn parse`, a `self.parse()` call resolved to BOTH (the method via the owner arm + the free fn via the file-MODULE arm) — the 10 residual resolution-is-a-function violations left after #466 (all in parser.rs). In Rust `self.foo()` is always the impl method, never a same-named free fn. Gate the free-fn arm with `\+ has_receiver(C)`: a method call carries a CALL -READS_FROM-> receiver (the analyzer's method-call discriminator, the same signal rust_cross_methods_ctor DELTA 6 keys on); a free call never does. Free-fn calls (the legitimate target of this arm) are unaffected. Measured on a fresh rust graph (70,029 CALLs): rust-calls resolution-is-a-function violations 10 → 0 (now a true function); rust_calls total edges 2381 → 2371 (−10, exactly the spurious free-fn duplicates — no legitimate resolution lost). All rust_* derive tests green; new test rust_calls_r1_method_call_excludes_free_fn_of_same_name. With this + #466, rust-calls contributes ZERO #431 violations. The remaining violators are rust-dyn-dispatch (324, semantic dispatch — already excluded) and rust-cross-method (237, parity-ceiling carve-out). Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Disentinel
added a commit
that referenced
this pull request
Jun 21, 2026
…ck resolver soundness (#479) A new datalog guarantee: a CALL must not resolve to two distinct targets under the same resolvedVia tag — resolution must be a function. Carves out the two resolvers that are one-to-many BY DESIGN: rust-dyn-dispatch (trait-object semantic dispatch) and rust-cross-method (the heuristic parity ceiling; precise resolution = the team-server SCIP tier). This is the enforcement layer for the #23 rust_calls rework (#466 + #467): with rust-calls now a true function, the guarantee locks the property and catches regressions. Verified on a fresh rust graph via the derive engine (backend.checkGuarantee): 0 violations WITH the carve-outs, 561 WITHOUT them (= rust-cross-method 237 + rust-dyn-dispatch 324 — exactly the two excluded one-to-many resolvers, nothing else over-resolves). Uses the derive builtin edge_attr to read the CALLS edge's resolvedVia; the edge() generator legs lead so the planner binds before the edge_attr point-probes (E-PLAN-002 otherwise). severity:error. `grafema check` is NOT wired into CI, so this is opt-in enforcement (grafema check → process.exit(1) on violation), not a per-PR CI blocker. Known scope: the JS resolvers (runtime-globals residual, cross-file-calls, same-file-calls) have their own over-resolution not yet closed — the JS analogue of the #23 rust work; on a graph where they fire, this rule reports them (correctly, as real over-resolutions to fix). Supersedes the stale vm #431 branch (which carried no actual rule). Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
rust_callsR2 (the::-qualified fallback) matched a path callType::assocby its last::-segment only, against every same-file function of that name. SoIndexEntry::newresolved to all 3fn newin the file, and stdHashSet::new/Arc::newbound to localnews (receiver type ignored). On the self-graph this was the dominant resolution-is-a-function violation class.Evidence (fresh current-HEAD rust graph, 69,991 CALLs)
resolution-is-a-functionviolations byresolvedVia, before → after:This corrects the earlier #431 premise (it assumed
rust-cross-methoddominated; it wasrust-calls).How
R2 split into two type-discriminated arms:
Type::assocto theIMPL_BLOCK-of-that-type'sHAS_METHODmember only, boundary-anchored (starts_with "Type::"leading,"::Type::"interior —MyArcnever matchesimpl Arc). A foreign type with no local impl → no row (correctly unresolved, was an unsound local bind).MODULEfree fn (utils::helper) kept flat on the last segment (inline mods are walked inline → module qualifier has no node; impl methods areHAS_METHOD'd, neverMODULE-CONTAINS'd, so R2b never fires for the assoc case).R2 remains the EDB seam for
rust_cross_methods_ctor/rust_receiver_typing(they readType::new()init CALLS) — discrimination only makes it precise. All 10rust_*derive tests green (incl. the seam packs); new testrust_calls_r2_type_discriminated_assoc_fn.#431 implication
rust-callsis now effectively a function (1261→10). The remaining violators arerust-dyn-dispatch(324, semantic dispatch — already excluded) +rust-cross-method(237, parity-ceiling — carve-out candidate) + 10 residualrust-calls(see below). With those carved/handled,resolution-is-a-functioncan shipseverity:erroroverrust-calls.Known residual (follow-up, not this PR)
The 10 remaining
rust-callsviolations are an R1 (scope-walk) nuance, not R2: inparser.rsa methodParser::parse_termand a freefn parse_termshare a name, so a bareself.parse_term()hits both R1's owner-arm (method) and its file-MODULE arm (free fn). Distinct, smaller precision item on the dominant resolver — deferred to avoid risking R1 in this change.🤖 Generated with Claude Code