Skip to content

Resolve multi-candidate methods on impls defined outside the type's module - #4778

Open
kasimte wants to merge 2 commits into
model-checking:mainfrom
kasimte:fix-impl-path-resolution
Open

Resolve multi-candidate methods on impls defined outside the type's module#4778
kasimte wants to merge 2 commits into
model-checking:mainfrom
kasimte:fix-impl-path-resolution

Conversation

@kasimte

@kasimte kasimte commented Sep 2, 2026

Copy link
Copy Markdown

Resolves #4777.

last_two_items_of_path_match compares the user's turbofish against def_path_str's rendering of each candidate. Impls living outside their type's home module render as <impl Type<Args>>, which no user-spellable path can equal — so multi-candidate methods in that position (e.g. the three Box<dyn Any(+Send)(+Sync), A>::downcast_unchecked impls in alloc's boxed/convert.rs) fail to resolve under every spelling, while single-candidate methods skip refinement and work.

This is the out-of-module half of #3773 (whose fix and multiple_inherent_impls.rs test cover the same-module rendering). It adds a fallback that unwraps the <impl SELF_TYPE> form, extracts SELF_TYPE's generic arguments, strips the disambiguation parentheses def_path_str adds around trait-object bounds (tuple-type parentheses are semantic and preserved), and retries the comparison.

Scope: this keeps the existing string-comparison approach and only unblocks the out-of-module rendering; the milder same-module symptom (concrete turbofish arguments not matching a structured self-type such as MaybeUninit<u32> vs the impl's MaybeUninit<T> — the generic-parameter spelling works there) is unchanged, and a semantic-resolution rewrite would subsume both. Two smaller spelling asymmetries are also known and left to that same follow-up: a trait-object bound nested inside another generic argument still requires def_path_str's parenthesized spelling (normalization is not recursive), and same-module dyn-argument candidates still require it too (the primary comparison does no paren normalization). Happy to take direction if the deeper rewrite is preferred.

Tests: eight unit tests in the existing simple_last_two_items_of_path_match module (dyn-args match + mismatch, dyn in second argument position, tuple parens preserved, non-generic no-fallback, fn-pointer renderings decline cleanly, arrow-bearing lists skip normalization, spaced turbofish matches on the primary path) and a new end-to-end regression test tests/kani/FunctionContracts/cross_module_multiple_impls.rs with tuple and trait-object candidate pairs (fails to resolve without the fix, verifies with it; the dyn pair fails if the paren-strip is disabled). Also validated on the motivating case: all three Box<dyn Any…>::downcast_unchecked contracts in verify-rust-std resolve and verify as proof_for_contract targets under the patched resolver.

Applies cleanly on current main (last_two_items_of_path_match is unchanged there). One call-out: candidates whose rendered path contains -> (fn-pointer / Fn-sugar types) are not unwrapped by the fallback — the top-level :: split already leaves such paths unmatchable, and the helpers additionally decline anything their bracket counting cannot parse — so these keep the existing failed-to-resolve behavior rather than matching a different candidate (unit-tested).

Motivating case: model-checking/verify-rust-std#669 — the three Box<dyn Any…>::downcast_unchecked contract targets there.

By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 and MIT licenses.

…odule

resolve_in_type_def refines multiple same-named inherent-impl candidates
by comparing the user's turbofish generic arguments against the last two
::-separated segments of def_path_str(candidate). When a candidate's impl
block lives in a different module than its self type, def_path_str renders
it as `path::to::module::<impl path::to::Type<Args>>::method` instead of
`Type::<Args>::method`, and no user-spellable turbofish can match that
<impl ...> wrapper.

Real instance: Box<dyn Any(+Send)(+Sync), A>::downcast_unchecked's three
impls live in alloc/src/boxed/convert.rs while Box is defined in boxed.rs.

Add a fallback in last_two_items_of_path_match: when the direct comparison
fails and the candidate's second-to-last path segment is in <impl SELF_TYPE>
form, extract SELF_TYPE's own generic arguments and retry the comparison
against those. The retry strips the redundant parens def_path_str adds
around a trait-object bound in a generic-argument list (e.g.
`(dyn Any + 'static)` -> `dyn Any + 'static`) before comparing; tuple-type
parens are semantic and preserved.
@kasimte
kasimte requested review from a team as code owners September 2, 2026 00:36
@github-actions github-actions Bot added Z-EndToEndBenchCI Tag a PR to run benchmark CI Z-CompilerBenchCI Tag a PR to run benchmark CI labels Sep 2, 2026
@feliperodri
feliperodri requested a balanced review from Copilot September 2, 2026 20:19

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Warning

Copilot couldn't run its full agentic review because it didn't start before the timeout. Make sure your repository has a runner available, or add a copilot-code-review.yml file specifying one with the runs-on attribute. See the docs for more details.

Pull request overview

Fixes Kani’s method contract resolution when multiple inherent impl candidates exist and the impl blocks are defined outside the self type’s home module (where def_path_str renders candidates as <impl path::Type<Args>>), and adds regression tests to cover this case.

Changes:

  • Extend last_two_items_of_path_match to unwrap <impl SELF_TYPE> renderings and retry matching against extracted generic args.
  • Add helpers to extract self-type generic args and normalize redundant parens around trait-object bounds.
  • Add unit tests plus an end-to-end regression test covering cross-module multiple-impl resolution.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.

File Description
tests/kani/FunctionContracts/cross_module_multiple_impls.rs Adds an integration regression reproducing cross-module multiple-impl resolution for proof_for_contract.
kani-compiler/src/kani_middle/resolve.rs Implements <impl SELF_TYPE> fallback matching and adds targeted unit tests.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines +850 to +867
fn impl_self_type_generic_args(part: &str) -> Option<&str> {
let self_type = part.strip_prefix("<impl ")?.strip_suffix('>')?;
let start = self_type.find('<')?;
let mut depth = 0;
for (i, c) in self_type[start..].char_indices() {
match c {
'<' => depth += 1,
'>' => {
depth -= 1;
if depth == 0 {
return Some(&self_type[start + 1..start + i]);
}
}
_ => {}
}
}
None
}

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in f3a28a3: extraction returns None unless the depth-0 close consumes the self type's full generic list, so a truncated extraction is impossible (impl_self_type_fn_ptr_args_decline). For accuracy: through the full pipeline, arrow-bearing candidate paths already fail one stage earlier — the top-level :: split leaves them unmatchable — so this is helper-level hardening; either way the outcome is the clean failed-to-resolve described in the body's call-out.

Comment on lines +876 to +887

for (i, c) in args.char_indices() {
match c {
'<' | '(' => depth += 1,
'>' | ')' => depth -= 1,
',' if depth == 0 => {
parts.push(&args[part_start..i]);
part_start = i + 1;
}
_ => {}
}
}

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in f3a28a3: strip_redundant_parens returns its input unchanged when the list contains -> — the construct that could drive the depth negative — so such lists skip normalization entirely rather than risk a wrong split. Unit test: strip_redundant_parens_arrow_list_unchanged.

Comment on lines +824 to +826
if last_two.chars().eq(actual_last_two.chars().filter(|c| !c.is_whitespace())) {
return true;
}

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In production the user side cannot carry whitespace — generic_args_to_string (resolve.rs:593) strips it when rendering the turbofish, which is why the pre-existing comparison filtered one side only; the spaced strings in the new unit tests exercise the helper directly, bypassing that rendering step. f3a28a3 filters both sides anyway so the helper is self-contained and consistent with the fallback path (whitespace_insensitive_primary_match).

- Trim each top-level argument before the paren checks: the ", "
  separator's space otherwise defeats fully_parenthesized for every
  trait-object bound after the first argument position, wrongly
  declining the bare spelling there.

- Drop the paren-strip call on the user's turbofish and correct the
  comment that claimed both-sides stripping: the caller renders the
  turbofish whitespace-free with its `::<...>` wrapper intact, which
  keeps every comma below top level, so the call could never strip
  anything. def_path_str's disambiguation parens are stripped from the
  candidate side only; the user spells the bound bare.

- Decline renderings the bracket counting cannot parse: extraction
  returns None unless it consumes the self type's full generic list
  (a premature close, e.g. the `>` of a fn-pointer's `->`, declines),
  and arrow-bearing argument lists skip paren normalization. These are
  helper-level guards: on the full pipeline the top-level `::` split
  already leaves arrow-bearing candidate paths unmatchable, so such
  candidates were and remain a clean failed-to-resolve.

- Make the primary comparison whitespace-insensitive on both sides
  rather than relying on the caller's pre-stripping.

- Pin the trait-object paren-strip end-to-end: the regression test
  gains a cross-module dyn-argument candidate pair (fails to resolve
  if the strip is disabled), alongside four new unit tests.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Z-CompilerBenchCI Tag a PR to run benchmark CI Z-EndToEndBenchCI Tag a PR to run benchmark CI

Projects

None yet

Development

Successfully merging this pull request may close these issues.

proof_for_contract cannot resolve methods on impls defined outside the type's own module when multiple same-named candidates exist

2 participants