Skip to content

Conversation

@hvitved
Copy link
Contributor

@hvitved hvitved commented Aug 25, 2025

Overview

This PR rewrites how we do call resolution and type inference for calls, to make it more faithful to what actually happens in the compiler.

Impact

The changes to expected test output shows that this PR resolves many shortcomings, as well as removes a lot of inconsistencies.

DCA is excellent: On some projects we achieve a whopping ~90 % reduction in analysis time, which follows the decrease in Nodes With Type At Length Limit for those projects. On coreutils, rendiation, peace, ruff, and gluon, however, we see increases in both analysis time and Nodes With Type At Length Limit.

I also did a QA run, which confirms the overall reduction in analysis time:

Top 50 largest absolute deltas
Project Analysis time before Analysis time after Diff
parbo/advent-of-code 3:43:52 0:10:09 -03:33:43
lambdaclass/sp1_poc_forger 3:14:07 0:30:51 -02:43:17
julianandrews/adventofcode 1:51:16 0:08:48 -01:42:28
uiua-lang/uiua 1:44:58 0:13:50 -01:31:09
renegade-fi/renegade 1:48:36 0:21:16 -01:27:21
mdsumner/zr 1:58:16 0:48:45 -01:09:32
to-omer/competitive-library 1:09:36 0:06:06 -01:03:30
dimforge/rapier 1:16:05 0:17:15 -00:58:51
rojo-rbx/rbx-dom 0:59:33 0:05:43 -00:53:50
nyx-space/nyx 0:55:50 0:09:36 -00:46:14
astral-sh/ruff 0:18:20 1:01:36 0:43:16
kjnapier/spacerocks 0:53:19 0:10:45 -00:42:35
lz520520/rust-1.75-ollvm 2:04:56 2:46:28 0:41:32
gluon-lang/gluon 0:09:06 0:38:28 0:29:21
azriel91/peace 0:14:06 0:41:52 0:27:46
sseemayer/aoc 0:28:06 0:06:45 -00:21:21
mpyle101/aoc 0:41:43 0:21:54 -00:19:49
tokio-rs/toasty 0:08:40 0:24:03 0:15:22
ferrocene/ferrocene 1:24:48 1:39:23 0:14:35
GraphiteEditor/Graphite 0:24:43 0:38:57 0:14:13
szbergeron/DaPaMIR-rustc 1:15:44 1:28:41 0:12:56
clear-crab/clear-crab 1:09:51 1:21:52 0:12:01
sigurd4/signal_processing 0:22:47 0:34:22 0:11:35
rust-lang/bors-kindergarten 1:13:03 1:24:09 0:11:06
RustVis/zu 0:56:20 0:45:16 -00:11:05
finos/perspective 0:26:00 0:36:31 0:10:30
misttech/mist-os 1:41:12 1:51:15 0:10:03
rust-lang/rust 1:15:01 1:24:31 0:09:30
mhogrefe/malachite 0:25:07 0:15:57 -00:09:11
mikialex/rendiation 0:55:48 0:47:29 -00:08:19
use-ink/ink 2:18:01 2:26:17 0:08:16
apache/datafusion 1:02:58 0:55:00 -00:07:58
dfinity/ic 2:47:07 2:54:52 0:07:44
tracel-ai/burn 0:45:35 0:37:59 -00:07:36
splashprotocol/splash-offchain-multiplatform 0:28:02 0:20:40 -00:07:22
ROCm/ROCK-Kernel-Driver 1:50:10 1:57:00 0:06:50
PyO3/pyo3 0:09:47 0:16:28 0:06:40
kolonialno/adventofcode 0:26:00 0:19:49 -00:06:12
Axnjr/snn_be_pro 0:44:35 0:38:26 -00:06:09
misttech/fuchsia 1:41:16 1:47:18 0:06:01
subcoin-project/subcoin 0:28:36 0:34:32 0:05:56
MDGSF/RustPractice 2:18:52 2:24:43 0:05:51
rustwasm/wasm-bindgen 0:21:24 0:26:41 0:05:17
Kalapaja/kampela-firmware 0:18:12 0:13:00 -00:05:13
oxidecomputer/third-party-api-clients 0:37:44 0:32:36 -00:05:08
zeitgeistpm/zeitgeist 2:30:08 2:25:33 -00:04:35
nazar-pc/abundance 0:37:50 0:33:18 -00:04:33
galacticcouncil/Basilisk-node 1:04:21 0:59:59 -00:04:23
microsoft/azure-devops-rust-api 0:21:15 0:17:01 -00:04:14
rivet-gg/rivet 1:22:04 1:17:52 -00:04:13

The QA run also showed that we have resolved analysis timeouts/failures for 29 projects:

Projects timeout/failure before

williamlion218/rust-sgx-sdk
zkMIPS/zkMIPS
zama-ai/tfhe-rs
veloren/veloren
kentakom1213/kyopro
TimTheBig/geo-3d
Univa/rumcake
10XGenomics/cellranger
ricosjp/truck
okaponta/atcoder-rust
jblindsay/whitebox-tools
futureversecom/trn-seed
mycroft/challenges
galacticcouncil/hydration-node
ChristopherBiscardi/advent-of-code
gasp-xyz/gasp-monorepo
dimforge/nalgebra
Apollo-Lab-Yale/apollo-rust
awsdocs/aws-doc-sdk-examples
rickyota/genoboost
SparkyPotato/radiance
strawlab/strand-braid
10XGenomics/spaceranger
sarah-quinones/faer-rs
hackmad/pbrt-v3-rs
attack68/rateslib
wingrew/thcore
Gleb-Zaslavsky/RustedSciThe
feos-org/feos

However, timeouts/failures have been introduced for 7 new projects

Projects timeout/failure after

golemfactory/yagna
carthage-software/mago
MaterializeInc/materialize
stencila/stencila
typedb/typedb
Feodor2/Mypal68
mattwparas/steel

In summary, both DCA and QA indicate overall performans wins, which is not necessarily expected (and certainly not the case for many earlier iterations of this PR), as this PR extends on the kinds of calls we are able to resolve.

For the reviewer

Note for review: As usual, commit-by-commit review is encouraged. As for the changes to TypeInference.qll, I very much recommend using split diff view.

Method call resolution

According to the spec, when resolving a method call x.m():

The first step is to build a list of candidate receiver types. Obtain these by repeatedly dereferencing the receiver expression’s type, adding each type encountered to the list, then finally attempting an unsized coercion at the end, and adding the result type if that is successful.

Then, for each candidate T, add &T and &mut T to the list immediately after T.

For instance, if the receiver has type Box<[i32;2]>, then the candidate types will be Box<[i32;2]>, &Box<[i32;2]>, &mut Box<[i32;2]>, [i32; 2] (by dereferencing), &[i32; 2], &mut [i32; 2], [i32] (by unsized coercion), &[i32], and finally &mut [i32].

Before this PR, we handled the above in a very ad hoc way, where we did attempt to model implicit dereferencing and borrowing, but we did not model the construction of candidate receiver types and prioritized lookup order. In particular, if x had type &Foo, we would only lookup the method in Foo.

With this PR, we model prioritized method lookup in the list of candidate receiver types in the module MethodResolution, but instead of constructing the full list of candidate receiver types, we recursively compute a set of candidates, only adding a new candidate receiver type to the set when we can rule out that the method cannot be found for the current candidate:

forall method:
  not current_candidate matches method

Care must be taken to ensure that the not current_candidate matches method check is monotonic, which we achieve using the monotonic isNotInstantiationOf predicate from the shared type inference library.

Method lookup

For a given candidate receiver type C, we need to match that type against the type of the self parameters of all potential call targets, taking into account that self parameters can have both explicit types and use shorthand syntax. Further care must be taken for methods that are inherited (either a trait method with a default implementation inherited by an impl block or a trait method inherited by a sub trait), so it only makes sense to talk about the type of a self parameter in the context of a given impl block or trait where that method is available (either directly or inherited). We model this using the class AssocFunctionType in the newly introduced FunctionType.qll library.

As before this PR, we use the IsInstantiationOf library for matching C against a given AssocFunctionType type S, now distinguishing between the following three cases:

  1. The method is defined in a blanket implementation: In this case, we additionally check that the part of C that matches the blanket type parameter also satisfies the blanket constraint. This means that blanket implementations are now also taken into account in the context of auto-dereferencing/borrowing.
  2. S represents the type of a self parameter for a method in a trait: In case C is e.g. dyn Trait, then we want C to match S, but only if the traits match up as well. We achieve this by substituting in the trait in both S and C before performing the IsInstantiationOf check.
  3. Not case 1 and 2: Simply perform the IsInstantiationOf check.

Method call type inference

When we have identified a valid call target for x.m() with a given candidate receiver type C, we need to use that type as well when doing type inference. Before this PR, we used the Matching module from the shared type inference library, but now we instead use MatchingWithEnvironment, where we record C in the environment via the sequence of auto-dereferences and borrows that happened to obtain C. This means we replace the ad hoc handling mentioned earlier, because we now have explicit knowledge about dereferencing/borrowing. The implementation is in the new MethodCallMatchingInput module.

Non-method call resolution

Resolution of non-method calls is much easier, since there is no such thing as auto-dereferencing and borrowing, even if the target is a method (Foo::m(&x) vs x.m()). However, as for method call resolution, we still need to take three cases into account:

  1. The function is defined in a blanket implementation: As before, we check that the blanket constraint is satisfied, but this time for an argument or the call context, when it provides information about the return type.
  2. The function is in a trait: As before, we substitute in the traits before performing the IsInstantiationOf check.
  3. Not case 1 and 2: As before, simply perform the IsInstantiationOf check, using an argument or the call context, when it provides information about the return type.

The implementation is in the module NonMethodResolution for calls that target non-methods, and in the MethodResolution module for calls that target methods.

Non-method call type inference

When the call is an operator call, we need to take into account that implicit borrowing may happen. For example, x == y is syntactic sugar for PartialEq::eq(&x, &y), so in order for the types to properly match up, we adjust the types of the operator, by stripping away the &s. This is done in the new OperationMatchingInput module.

When the call is not an operator call, we can match types directly, which happens in the NonMethodCallMatchingInput module for calls that target non-methods, and in the MethodCallMatchingInput module for calls that target methods.

Future work

  • As before this PR, we do not handle the Deref trait when performing auto-dereferencing and unsized coercions. With this PR, however, it should be much easier to support that.
  • Investigate the slowdowns/failures reported by DCA/QA.
  • When we rule out a given candidate receiver type in order to progress to the next candidate, we do not currently take blanket implementations into account. This means that even if a blanket implementation matches for a given candidate receiver type C_i, we will still lookup in C_(i+1) as well. Supporting this is not straightforward, since we need a monotonic way of checking blanket constraint non-satisfaction.

@github-actions github-actions bot added the Rust Pull requests that update Rust code label Aug 25, 2025
Copy link

@github-advanced-security github-advanced-security bot left a comment

Choose a reason for hiding this comment

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

CodeQL found more than 20 potential problems in the proposed changes. Check the Files changed tab for more details.

@hvitved hvitved force-pushed the rust/type-inference-method-call-resolution-rework branch 2 times, most recently from e4cfb86 to 4a8c37c Compare August 26, 2025 18:30
@hvitved hvitved force-pushed the rust/type-inference-method-call-resolution-rework branch from 4a8c37c to e75d79e Compare August 27, 2025 15:34
@hvitved hvitved force-pushed the rust/type-inference-method-call-resolution-rework branch 8 times, most recently from 61866bf to 2d1ed65 Compare September 1, 2025 09:45
@hvitved hvitved force-pushed the rust/type-inference-method-call-resolution-rework branch from 2d1ed65 to 3d19a06 Compare September 1, 2025 10:30
@hvitved hvitved force-pushed the rust/type-inference-method-call-resolution-rework branch from 3d19a06 to 153c10b Compare September 1, 2025 17:56
@hvitved hvitved force-pushed the rust/type-inference-method-call-resolution-rework branch from 153c10b to e161d4c Compare September 1, 2025 18:35
@hvitved hvitved force-pushed the rust/type-inference-method-call-resolution-rework branch from dd45f7b to a20c440 Compare September 2, 2025 07:24
@hvitved hvitved force-pushed the rust/type-inference-method-call-resolution-rework branch 5 times, most recently from f45d2d5 to f9f8782 Compare September 3, 2025 13:07
Copy link
Contributor

@geoffw0 geoffw0 left a comment

Choose a reason for hiding this comment

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

This sounds great, and I agree the DCA and QA results look fantastic overall. As does the reduction of inconsistencies in tests. Thank you for the explanation, thorough testing and explaining the impact here so there won't be any surprises after this is merged - and keeping track of some of the regressions that are part of this net improvement. 👍

I haven't looked at the code changes yet. @paldepind is probably the better reviewer for this, but I'll have a look as well.

@hvitved hvitved force-pushed the rust/type-inference-method-call-resolution-rework branch from 88c9f0f to 2ad8e2b Compare October 8, 2025 07:35
@hvitved
Copy link
Contributor Author

hvitved commented Oct 8, 2025

Rebased to resolve merge conflicts in .expected files.

Copy link
Contributor

@geoffw0 geoffw0 left a comment

Choose a reason for hiding this comment

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

I looked through the code, I don't have much to say there. It's a complicated area of code and thus important it stays well documented.

I also tried running a data flow query on one of the projects that slowed down the most on DCA - it does seem like something may be going wrong in the type inference recursion in some cases. Given that there's a net performance improvement on DCA (and QA) I don't think this needs to block merging the PR, but it might be worth looking into afterwards.

@hvitved
Copy link
Contributor Author

hvitved commented Oct 10, 2025

Given that there's a net performance improvement on DCA (and QA) I don't think this needs to block merging the PR, but it might be worth looking into afterwards.

Already on it :-)

@hvitved hvitved requested a review from a team as a code owner October 10, 2025 07:39
@hvitved hvitved force-pushed the rust/type-inference-method-call-resolution-rework branch from 6e54fa0 to 8a25e32 Compare October 10, 2025 11:11
@github github deleted a comment from Copilot AI Oct 17, 2025
@hvitved hvitved force-pushed the rust/type-inference-method-call-resolution-rework branch 2 times, most recently from 89da13c to b6bbffd Compare October 20, 2025 12:50
Copy link
Contributor

@paldepind paldepind left a comment

Choose a reason for hiding this comment

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

Submitting the comments that I have thus far, in case you want to look at some of this before I'm completely done.

* [1]: https://doc.rust-lang.org/stable/reference/items/associated-items.html#r-items.associated.fn.method.self-ty
*/
pragma[nomagic]
predicate complexSelfRoot(Type root, TypeParameter tp) {
Copy link
Contributor

Choose a reason for hiding this comment

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

This predicate does two orthogonal things. 1/ Picking the types that can appear for self and 2/ getting the first positional type parameter of a type. I think it would be worthwhile to have to former as a separate predicate called something like validSelfType?

Comment on lines 643 to 649
s instanceof BoxStruct
or
s instanceof RcStruct
or
s instanceof ArcStruct
or
s instanceof PinStruct
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
s instanceof BoxStruct
or
s instanceof RcStruct
or
s instanceof ArcStruct
or
s instanceof PinStruct
s instanceof BoxStruct or
s instanceof RcStruct or
s instanceof ArcStruct or
s instanceof PinStruct

class FunctionTypePosition extends TFunctionTypePosition {
predicate isSelf() { this.asArgumentPosition().isSelf() }

int asPositional() { result = this.asArgumentPosition().asPosition() }
Copy link
Contributor

Choose a reason for hiding this comment

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

Wouldn't it be nicer to reuse the same name? It also seems more natural to say that the predicate returns a "position" rather than a "positional".

Suggested change
int asPositional() { result = this.asArgumentPosition().asPosition() }
int asPosition() { result = this.asArgumentPosition().asPosition() }

Comment on lines 182 to 183
private predicate hasTypeParameterAt(TypePath path, TypeParameter tp) {
this.getDeclaredTypeAt(path) = tp
Copy link
Contributor

Choose a reason for hiding this comment

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

This would be natural as a predicate with result:

Suggested change
private predicate hasTypeParameterAt(TypePath path, TypeParameter tp) {
this.getDeclaredTypeAt(path) = tp
private TypeParameter getTypeParameterAt(TypePath path) {
result = this.getDeclaredTypeAt(path)

Comment on lines 819 to 826
private predicate assocFunctionInfo(
Function f, string name, int arity, ImplOrTraitItemNode i, FunctionTypePosition pos,
AssocFunctionType t
) {
f = i.getASuccessor(name) and
arity = f.getParamList().getNumberOfParams() and
t.appliesTo(f, pos, i)
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Given that (f, i, pos) uniquely determines and is uniquely determined by t, we're including the same data twice. Could we remove one of them like this?

Suggested change
private predicate assocFunctionInfo(
Function f, string name, int arity, ImplOrTraitItemNode i, FunctionTypePosition pos,
AssocFunctionType t
) {
f = i.getASuccessor(name) and
arity = f.getParamList().getNumberOfParams() and
t.appliesTo(f, pos, i)
}
private predicate assocFunctionInfo(AssocFunctionType t, string name, int arity) {
t.getFunction() = t.getImpl().getASuccessor(name) and
arity = t.getFunction().getParamList().getNumberOfParams()
}

Same question for several predicates below, like methodInfo, methodCallNonBlanketCandidate, etc.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It is true that some columns are uniquely determined by others, but it is quite convenient to have predicates like these, where one can -- in a single predicate call -- derive some columns from others.

Copy link
Contributor

Choose a reason for hiding this comment

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

Ok, though I'm not entirely convinced that we need to carry around all the values in all the predicates.

Comment on lines 1085 to 1088
abstract class MethodCall extends Expr {
abstract predicate hasNameAndArity(string name, int arity);

override TypeParameter getTypeParameter(TypeParameterPosition ppos) {
typeParamMatchPosition(this.getGenericParamList().getATypeParam(), result, ppos)
}
abstract Expr getArgument(ArgumentPosition pos);
Copy link
Contributor

Choose a reason for hiding this comment

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

This class duplicates a bunch of stuff already present in Call. For instance, the getArgument overrides below are identical to the getArgument implementations in Call.

I think we should be able to extend Call, remove hasNameAndArity (which duplicates getMethodName, and remove getNumberOfArguments) and getArgument (which duplicates getArgument).

Suggested change
abstract class MethodCall extends Expr {
abstract predicate hasNameAndArity(string name, int arity);
override TypeParameter getTypeParameter(TypeParameterPosition ppos) {
typeParamMatchPosition(this.getGenericParamList().getATypeParam(), result, ppos)
}
abstract Expr getArgument(ArgumentPosition pos);
abstract class MethodCall extends Call {
MethodCall() { exists(super.getMethodName()) }

The suggestion doesn't work as-is, because there is a difference in which CallExprs are considered method calls. For instance, this call is considered a method call by MethodCall but not by Call. I suppose we should just change Call to behave like MethodCall is currently doing?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I would like to ultimately get rid of the Call class, which is why I duplicated some of the logic here.

Copy link
Contributor

Choose a reason for hiding this comment

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

Why? It's useful both in type inference, data flow, and for queries (for instance if you want a query that considers all calls to Index::index).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Sorry, what I meant was I want to get rid of some of the functionality in the user-visible Call class (such as implicitBorrowAt and getTrait), and move it in here instead, but I would prefer to do that follow-up.

Copy link
Contributor

Choose a reason for hiding this comment

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

Ok. I completely agree that is makes sense to remove implicitBorrowAt if we're no longer using it in type inference as it was only added for type inference 👍

But wouldn't getArgument, getMethodName, and getNumberOfArguments be things we would keep and thus could reuse here?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

We definitely want to keep getArgument and getNumberOfArguments, not sure about getMethodName, because determining whether a call targets a method is non-trivial. We can't replace hasNameAndArity with getMethodName and getNumberOfArguments because then joining will no longer happen on the two values simultaneously.

Copy link
Contributor

Choose a reason for hiding this comment

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

If we extend Call we can just define hasNameAndArity once using getMethodName and getNumberOfArguments. Wouldn't that be fine in terms of joins as it'd only happen once. Otherwise, we could add that predicate on Call.

exists(FunctionTypePosition pos |
assocFunctionInfo(m, name, arity, i, pos, selfType) and
strippedType = selfType.getTypeAt(strippedTypePath) and
isComplexRootStripped(strippedTypePath, strippedType) and
Copy link
Contributor

Choose a reason for hiding this comment

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

Does this work if we are in fact adding a method to one of the types that can appear for self, like Box or Pin?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

In that case strippedType will be a type parameter, and it is handled by methodInfoBlanket, which cover all blanket (like) implementations.

@hvitved
Copy link
Contributor Author

hvitved commented Oct 21, 2025

Submitting the comments that I have thus far, in case you want to look at some of this before I'm completely done.

Thanks a lot. I have addressed your comments.

Copy link
Contributor

@paldepind paldepind left a comment

Choose a reason for hiding this comment

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

Thanks for addressing my comments.

Here's a few more. And, again, feel free to ignore them until I'm completely done if that's easiest for you. I just prefer not to have too many "pending" comments.

@hvitved hvitved force-pushed the rust/type-inference-method-call-resolution-rework branch from 819a537 to 41602d3 Compare October 21, 2025 16:45
Copy link
Contributor

@paldepind paldepind left a comment

Choose a reason for hiding this comment

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

Just a few more comments. After that, let's get this merged 💪

exists(pos.getTypeMention(f))
} or
MkInheritedAssocFunctionType(
Function f, FunctionPosition pos, TypeMention parentMention, ImplOrTraitItemNode parent,
Copy link
Contributor

Choose a reason for hiding this comment

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

The parent can only be an impl block.

Suggested change
Function f, FunctionPosition pos, TypeMention parentMention, ImplOrTraitItemNode parent,
Function f, FunctionPosition pos, TypeMention parentMention, TraitItemNode parent,

}

private predicate isInheritedFunctionType(
Function f, FunctionPosition pos, TypeMention parentMention, ImplOrTraitItemNode parent,
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
Function f, FunctionPosition pos, TypeMention parentMention, ImplOrTraitItemNode parent,
Function f, FunctionPosition pos, TypeMention parentMention, TraitItemNode parent,

)
or
exists(
Function f, FunctionPosition pos, TypeMention parentMention, ImplOrTraitItemNode parent,
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
Function f, FunctionPosition pos, TypeMention parentMention, ImplOrTraitItemNode parent,
Function f, FunctionPosition pos, TypeMention parentMention, TraitItemNode parent,

Comment on lines +1175 to +1206
/**
* Holds if the candidate receiver type represented by `derefChain` does not
* have a matching method target.
*/
pragma[nomagic]
predicate hasNoCompatibleTargetNoBorrow(string derefChain) {
(
this.supportsAutoDerefAndBorrow()
or
result = TTypeParamTypeParameter(enum.getGenericParamList().getATypeParam()) and
path = TypePath::singleton(result)
// needed for the `hasNoCompatibleTarget` check in
// `SatisfiesBlanketConstraintInput::hasBlanketCandidate`
derefChain = ""
) and
exists(TypePath strippedTypePath, Type strippedType |
not derefChain.matches("%.ref") and // no need to try a borrow if the last thing we did was a deref
strippedType = this.getComplexStrippedType(derefChain, false, strippedTypePath) and
this.hasNoCompatibleTargetCheck(derefChain, false, strippedTypePath, strippedType)
)
}
}

additional class FunctionDecl extends Declaration, Function {
override TypeParameter getTypeParameter(TypeParameterPosition ppos) {
typeParamMatchPosition(this.getGenericParamList().getATypeParam(), result, ppos)
/**
* Holds if the candidate receiver type represented by `derefChain`, followed
* by a borrow, does not have a matching method target.
*/
pragma[nomagic]
predicate hasNoCompatibleTargetBorrow(string derefChain) {
exists(TypePath strippedTypePath, Type strippedType |
this.hasNoCompatibleTargetNoBorrow(derefChain) and
strippedType = this.getComplexStrippedType(derefChain, true, strippedTypePath) and
this.hasNoCompatibleTargetCheck(derefChain, true, strippedTypePath, strippedType)
)
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Maybe now that borrow is just a boolean, it would make sense to combine these two predicates into one. That would also be a bit simpler at the usage down in hasNoCompatibleTargetEq.

Suggested change
/**
* Holds if the candidate receiver type represented by `derefChain` does not
* have a matching method target.
*/
pragma[nomagic]
predicate hasNoCompatibleTargetNoBorrow(string derefChain) {
(
this.supportsAutoDerefAndBorrow()
or
result = TTypeParamTypeParameter(enum.getGenericParamList().getATypeParam()) and
path = TypePath::singleton(result)
// needed for the `hasNoCompatibleTarget` check in
// `SatisfiesBlanketConstraintInput::hasBlanketCandidate`
derefChain = ""
) and
exists(TypePath strippedTypePath, Type strippedType |
not derefChain.matches("%.ref") and // no need to try a borrow if the last thing we did was a deref
strippedType = this.getComplexStrippedType(derefChain, false, strippedTypePath) and
this.hasNoCompatibleTargetCheck(derefChain, false, strippedTypePath, strippedType)
)
}
}
additional class FunctionDecl extends Declaration, Function {
override TypeParameter getTypeParameter(TypeParameterPosition ppos) {
typeParamMatchPosition(this.getGenericParamList().getATypeParam(), result, ppos)
/**
* Holds if the candidate receiver type represented by `derefChain`, followed
* by a borrow, does not have a matching method target.
*/
pragma[nomagic]
predicate hasNoCompatibleTargetBorrow(string derefChain) {
exists(TypePath strippedTypePath, Type strippedType |
this.hasNoCompatibleTargetNoBorrow(derefChain) and
strippedType = this.getComplexStrippedType(derefChain, true, strippedTypePath) and
this.hasNoCompatibleTargetCheck(derefChain, true, strippedTypePath, strippedType)
)
}
/**
* Holds if the candidate receiver type represented by `derefChain` and
* `borrow` does not have a matching method target.
*/
pragma[nomagic]
predicate hasNoCompatibleTarget2(string derefChain, boolean borrow) {
exists(TypePath strippedTypePath, Type strippedType |
strippedType = this.getComplexStrippedType(derefChain, borrow, strippedTypePath) and
this.hasNoCompatibleTargetCheck(derefChain, borrow, strippedTypePath, strippedType)
|
borrow = true and this.hasNoCompatibleTarget2(derefChain, false)
or
borrow = false and
not derefChain.matches("%.ref") and // no need to try a borrow if the last thing we did was a deref
(
this.supportsAutoDerefAndBorrow()
or
// needed for the `hasNoCompatibleTarget` check in
// `SatisfiesBlanketConstraintInput::hasBlanketCandidate`
derefChain = ""
)
)
}

Copy link
Contributor Author

Choose a reason for hiding this comment

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

In terms of performance, having the predicates separate is better, because they appear in a non-linear recursion context, so I prefer to keep them like this.

Copy link
Contributor

Choose a reason for hiding this comment

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

Makes sense.


private module CallExprBaseMatching = Matching<CallExprBaseMatchingInput>;
/** A method call tagged with a candidate receiver type. */
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
/** A method call tagged with a candidate receiver type. */
/** A method call with a dereference chain and a potential borrow. */

Comment on lines 819 to 826
private predicate assocFunctionInfo(
Function f, string name, int arity, ImplOrTraitItemNode i, FunctionTypePosition pos,
AssocFunctionType t
) {
f = i.getASuccessor(name) and
arity = f.getParamList().getNumberOfParams() and
t.appliesTo(f, pos, i)
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Ok, though I'm not entirely convinced that we need to carry around all the values in all the predicates.

Copy link
Contributor

@paldepind paldepind left a comment

Choose a reason for hiding this comment

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

🎉 🔥

@hvitved hvitved merged commit 7d0509b into github:main Oct 22, 2025
43 checks passed
@hvitved hvitved deleted the rust/type-inference-method-call-resolution-rework branch October 22, 2025 14:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Rust Pull requests that update Rust code

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants