Skip to content

fix(derive): #23 — type-discriminate rust_calls R2 (Type::assoc → impl-of-that-type only) - #466

Merged
Disentinel merged 1 commit into
mainfrom
fix/rust-calls-scope-walk
Jun 18, 2026
Merged

fix(derive): #23 — type-discriminate rust_calls R2 (Type::assoc → impl-of-that-type only)#466
Disentinel merged 1 commit into
mainfrom
fix/rust-calls-scope-walk

Conversation

@Disentinel

Copy link
Copy Markdown
Owner

What

rust_calls R2 (the ::-qualified fallback) matched a path call Type::assoc by its last ::-segment only, against every same-file function of that name. So IndexEntry::new resolved to all 3 fn new in the file, and std HashSet::new/Arc::new bound to local news (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-function violations by resolvedVia, before → after:

resolvedVia before after
rust-calls 1261 10 (fan-out 8→2)
rust-dyn-dispatch 324 324 (unchanged — different pack)
rust-cross-method 238 237 (unchanged — different pack)

This corrects the earlier #431 premise (it assumed rust-cross-method dominated; it was rust-calls).

How

R2 split 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::" leading, "::Type::" interior — MyArc never matches impl Arc). A foreign type with no local impl → no row (correctly unresolved, was an unsound local bind).
  • R2b (free fn) — path call to a file-MODULE free fn (utils::helper) kept flat on the last segment (inline mods are walked inline → module qualifier has no node; impl methods are HAS_METHOD'd, never MODULE-CONTAINS'd, so R2b never fires for the assoc case).

R2 remains the EDB seam for rust_cross_methods_ctor / rust_receiver_typing (they read Type::new() init CALLS) — discrimination only makes it precise. All 10 rust_* derive tests green (incl. the seam packs); new test rust_calls_r2_type_discriminated_assoc_fn.

#431 implication

rust-calls is now effectively a function (1261→10). The remaining violators are rust-dyn-dispatch (324, semantic dispatch — already excluded) + rust-cross-method (237, parity-ceiling — carve-out candidate) + 10 residual rust-calls (see below). With those carved/handled, resolution-is-a-function can ship severity:error over rust-calls.

Known residual (follow-up, not this PR)

The 10 remaining rust-calls violations are an R1 (scope-walk) nuance, not R2: in parser.rs a method Parser::parse_term and a free fn parse_term share a name, so a bare self.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

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
Disentinel merged commit ab1abe1 into main Jun 18, 2026
13 checks passed
@Disentinel
Disentinel deleted the fix/rust-calls-scope-walk branch June 18, 2026 06:45
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant