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 and rendiation (incorrectly tagged as radiance in the run), 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
@hvitved hvitved force-pushed the rust/type-inference-method-call-resolution-rework branch 3 times, most recently from 0c1d6df to 41275ca Compare October 3, 2025 09:41
| main.rs:212:24:212:33 | source(...) | main.rs:1:1:3:1 | fn source |
| main.rs:214:5:214:11 | sink(...) | main.rs:5:1:7:1 | fn sink |
| main.rs:228:10:228:14 | * ... | main.rs:235:5:237:5 | fn deref |
| main.rs:236:11:236:15 | * ... | main.rs:235:5:237:5 | fn deref |
Copy link
Contributor Author

Choose a reason for hiding this comment

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

These targets were actually wrong, so it is correct that they are now removed.

@hvitved hvitved force-pushed the rust/type-inference-method-call-resolution-rework branch 5 times, most recently from b3ddde3 to 63744d1 Compare October 5, 2025 13:52
Comment on lines -702 to -707
// `app` uses inconsistent type parameter instantiations
exists(TypeParameter tp |
potentialInstantiationOf(app, abs, tm) and
app.getTypeAt(getNthTypeParameterPath(tm, tp, _)) !=
app.getTypeAt(getNthTypeParameterPath(tm, tp, _))
)
Copy link
Contributor Author

@hvitved hvitved Oct 6, 2025

Choose a reason for hiding this comment

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

This check has been removed, because it would sometimes apply to entities that were assigned multiple copies of the same type, existing in different versions of a library.

@hvitved hvitved marked this pull request as ready for review October 6, 2025 07:36
@hvitved hvitved requested a review from a team as a code owner October 6, 2025 07:36
@hvitved hvitved requested a review from paldepind October 6, 2025 08:11
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 from 8a25e32 to 89da13c Compare October 19, 2025 14:46
@hvitved hvitved force-pushed the rust/type-inference-method-call-resolution-rework branch 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.

private newtype TNode =
TTrait(Trait t) { relevantTraitVisible(_, t) } or
TItemNode(ItemNode i) or
TElement(Element e) { relevantTraitVisible(e, _) }
Copy link
Contributor

Choose a reason for hiding this comment

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

Would performance be worse if instead of a newtype we just used AstNode (or maybe Locatable) for doublyBoundedFastTC? One could hope that it's performance only depends on the sizes of the predicates and not the type that they're on?

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 need to use a newtype to distinguish source and sink nodes from all the intermediate nodes, because edges out of sources and into sinks are different from all other edges.

* [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)

* `i` is `type`.
*/
pragma[nomagic]
predicate assocFunctionTypeAtPath(
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
predicate assocFunctionTypeAtPath(
predicate assocFunctionTypeAt(

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.

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.

* [1]: https://doc.rust-lang.org/reference/expressions/method-call-expr.html#r-expr.method.candidate-receivers
*/
pragma[nomagic]
Type getACandidateReceiverTypeAt(TypePath path, string derefChainBorrow) {
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 there can only be a single borrow at the end, could we encode this with a separate boolean instead of in the string?

Suggested change
Type getACandidateReceiverTypeAt(TypePath path, string derefChainBorrow) {
Type getACandidateReceiverTypeAt(TypePath path, string derefChain, boolean borrow) {

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 will, at some point, need to distinguish borrows from mut borrows, at which point a boolean does not suffice; but perhaps that is OK for now.

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?

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