Skip to content

Conversation

@hvitved
Copy link
Contributor

@hvitved hvitved commented Jan 26, 2026

The following simple program would make our type inference implementation explode:

#[derive(Default)]
struct S;

fn f() -> S {
    let x = Default::default();
    From::from(x)
}

Firstly, because of a getSuccessor vs getAssocItem bug, From::from would allow for any sub trait of From, for example the Int trait, which meant that x could be inferred to have type bool. Secondly, since many from implementations are polymorphic in their argument, we should really be checking that the return type matches the type of the calling context.

This PR resolves the above, and other issues (see tests) by rewriting when and how we do type-based call disambiguation, aligning the cases for methods and non-methods. Key changes:

  • As for calls to methods, we now also always check the Self type for calls to associated non-methods f. The type at the call site may be obtained by looking at the qualifier, e.g. Vec::<i32>::new() (which also means we need extra care for default type arguments, such as the default Global for A in Vec). Additionally, when f is (an implementation of) a trait function (that is, not in an inherent implementation), we may additionally obtain the type at any of the positions at which Self is mentioned in the trait function, for example in Default::default() the type may be obtained from the context of the call.
  • Since the Self type is now always checked, both cases can now use the functionResolutionDependsOnArgument predicate from Overloading.qll, because the sibling logic assumes we only want to disambiguate targets with the same Self type.
  • The logic in ArgsAreInstantiationsOf has been changed to do a ranked forall over type parameters instead of over type positions, because what we really want is to check that there is some position at which a given type parameter occurs, where the types match. This allows us to handle cases like this one.
  • The typeCanBeUsedForDisambiguation logic has been removed, which gives us more call targets, but also some false targets like this one. I tried to take type parameter constraints into account as well, but that turned out quite difficult, since in cases like let mut i = 0i64; let mut pin = Pin::new(&i); we need type information from both the Pin qualifier and the &i argument to first match against the Self type, and subsequently to validate the Ptr : Deref constraint of the new function.

DCA DCA looks great; while we fix the performance issue, we increase Percentage of calls with call target by 1.67 1.73 % point. There is an increase in Nodes With Type At Length Limit, in particular on radiance, which is a consequence of removing typeCanBeUsedForDisambiguation. This PR also fixes 8 projects that currently timeout during analysis.

@github-actions github-actions bot added the Rust Pull requests that update Rust code label Jan 26, 2026
@hvitved hvitved force-pushed the rust/type-inference-perf branch 3 times, most recently from 38929dd to 50ce393 Compare January 26, 2026 19:16
@hvitved hvitved changed the title Rust: Type inference performance improvements Rust: Fix context-based type inference explosion Jan 26, 2026
@hvitved hvitved added the no-change-note-required This PR does not need a change note label Jan 27, 2026
Comment on lines +1002 to 1003
hasTypeConstraint(tt, constraint) and
t = getTypeAt(tt, path)
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This conjunction has non-linear recursion, hence the magic'ed version of hasTypeConstraint.

@hvitved hvitved marked this pull request as ready for review January 27, 2026 07:31
@hvitved hvitved requested review from a team as code owners January 27, 2026 07:31
@hvitved hvitved requested review from Copilot and paldepind January 27, 2026 07:31
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR fixes a type inference explosion issue in Rust queries by correcting two bugs in the type inference implementation:

  1. Path resolution bug: Changed getASuccessor(_) to getAnAssocItem() to correctly resolve trait functions only from the trait itself, not from sub-traits
  2. Context-based return type checking: Enhanced the logic to only use return type for disambiguation when all argument positions are trivially satisfied

Changes:

  • Fixed trait function resolution to prevent matching sub-trait implementations
  • Added logic to check return type matches calling context for polymorphic functions
  • Refactored Input modules into a unified structure
  • Added regression test for the Default::default() and From::from() pattern

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated no comments.

Show a summary per file
File Description
shared/typeinference/codeql/typeinference/internal/TypeInference.qll Added helper predicate with pragma[nomagic] to optimize type constraint checking
rust/ql/lib/codeql/rust/internal/typeinference/TypeInference.qll Fixed getASuccessorgetAnAssocItem bug, merged Input modules, fixed parameter ordering in context typing
rust/ql/lib/codeql/rust/internal/typeinference/FunctionOverloading.qll Split predicate and added return type disambiguation logic
rust/ql/test/library-tests/type-inference/main.rs Added regression test for from_default module
rust/ql/test/library-tests/type-inference/type-inference.expected Updated expected test output
rust/ql/test/query-tests/security/CWE-312/CONSISTENCY/PathResolutionConsistency.expected Removed spurious multiple resolution error

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@hvitved hvitved marked this pull request as draft January 27, 2026 13:14
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.

Looks helpful.

Is there a reason this is still draft?

@hvitved
Copy link
Contributor Author

hvitved commented Jan 29, 2026

Is there a reason this is still draft?

I would like to rework this a bit more; while it does fix the example posted in the description, there are still cases resulting in timeouts that I would like to cover as well.

@hvitved hvitved force-pushed the rust/type-inference-perf branch from 50ce393 to 3244471 Compare January 29, 2026 15:58
@hvitved hvitved force-pushed the rust/type-inference-perf branch 2 times, most recently from c1b3551 to 9079fde Compare January 30, 2026 10:13
@hvitved hvitved force-pushed the rust/type-inference-perf branch 7 times, most recently from e2ebf44 to 026ddfe Compare February 2, 2026 18:13
@hvitved hvitved force-pushed the rust/type-inference-perf branch from 026ddfe to ee7c24b Compare February 3, 2026 12:32
@hvitved hvitved force-pushed the rust/type-inference-perf branch from ee7c24b to dde7c24 Compare February 3, 2026 12:54
* for non-methods.
*/
pragma[nomagic]
Type getAssocFunctionTypeInclNonMethodSelfAt(

Check warning

Code scanning / CodeQL

Missing QLDoc for parameter Warning

The QLDoc has no documentation for f, or i, or path, or pos, but the QLDoc mentions self
@hvitved hvitved force-pushed the rust/type-inference-perf branch from dde7c24 to 9aad861 Compare February 3, 2026 14:15
@hvitved hvitved force-pushed the rust/type-inference-perf branch 5 times, most recently from c663247 to b61ef72 Compare February 5, 2026 09:12
@hvitved hvitved changed the title Rust: Fix context-based type inference explosion Rust: Rework call disambiguation logic Feb 5, 2026
@hvitved hvitved force-pushed the rust/type-inference-perf branch from b61ef72 to 7b9ae9f Compare February 5, 2026 09:49
@hvitved hvitved marked this pull request as ready for review February 5, 2026 10:36
@hvitved hvitved force-pushed the rust/type-inference-perf branch 2 times, most recently from 2158fdb to 7289906 Compare February 5, 2026 14:18
geoffw0
geoffw0 previously approved these changes Feb 5, 2026
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.

LGTM. This is a complex CodeQL change that's difficult to review closely - if there are any particular areas you'd like to be scrutinised in more detail, please point to them. The excellent new tests, the existing tests, and the DCA run add a lot of confidence for me.

* Holds if all arguments of `call` have types that are instantiations of the
* types of the corresponding parameters of `f` inside `i`.
*
* TODO: Check type parameter constraints as well.
Copy link
Contributor

Choose a reason for hiding this comment

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

Are the two TODO comments intended as follow-up work?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes.

geoffw0
geoffw0 previously approved these changes Feb 6, 2026
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.

Great improvements!

#[derive(Default)]
struct S;
impl FirstTrait for S {
// S::method2
Copy link
Contributor

Choose a reason for hiding this comment

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

This method and the other method2 below have the same comment, so we can't distinguish them in the expectations.

Suggested change
// S::method2
// S_as_FirstTrait::method2

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Whoops.

* Gets a prefix of this list, including the list itself.
*/
bindingset[this]
UnboundList getAPrefixOrSelf() { result = [this, this.getAPrefix()] }
Copy link
Contributor

Choose a reason for hiding this comment

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

What about this renaming:

  • getPrefix -> getProperPrefix
  • getAPrefix -> getAProperPrefix
  • getAPrefixOrSelf -> getAPrefix

These names a more precise and the most used predicate, getAPrefixOrSelf, gets a shorter name.

result = this.getDefaultPositionalTypeArgument(pragma[only_bind_into](i), path) and
tp = this.resolveRootType().getPositionalTypeParameter(pragma[only_bind_into](i))
)
}
Copy link
Contributor

Choose a reason for hiding this comment

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

There's a bit of duplication here and in getTypeForTypeParameterAt below.

Perhaps we could get rid of it with these predicates:

    private Type getPositionalTypeArgument(int i, TypePath path, boolean isDefault) {
      result = getPathTypeArgument(this, i).getTypeAt(path) and
      isDefault = false
      or
      result = this.getDefaultPositionalTypeArgument(i, path) and
      isDefault = true
    }

    pragma[nomagic]
    Type getTypeArgument(TypeParameter tp, TypePath path, boolean isDefault) {
      exists(int i |
        result = this.getPositionalTypeArgument(pragma[only_bind_into](i), path, isDefault) and
        tp = this.resolveRootType().getPositionalTypeParameter(pragma[only_bind_into](i))
      )
    }

Where getTypeArgument can be used in getTypeForTypeParameterAt and in place of getDefaultTypeForTypeParameterInNonAnnotationAt as in this diff:

diff --git a/rust/ql/lib/codeql/rust/internal/typeinference/TypeInference.qll b/rust/ql/lib/codeql/rust/internal/typeinference/TypeInference.qll
index 3e9c823c570..27052410294 100644
--- a/rust/ql/lib/codeql/rust/internal/typeinference/TypeInference.qll
+++ b/rust/ql/lib/codeql/rust/internal/typeinference/TypeInference.qll
@@ -1071,8 +1071,7 @@ private Type getCallExprTypeQualifier(CallExpr ce, TypePath path, boolean isDefa
     isDefaultTypeArg = false
     or
     exists(TypeParameter tp, TypePath suffix |
-      result =
-        tm.(NonAliasPathTypeMention).getDefaultTypeForTypeParameterInNonAnnotationAt(tp, suffix) and
+      result = tm.(NonAliasPathTypeMention).getTypeArgument(tp, suffix, true) and
       path = TypePath::cons(tp, suffix) and
       isDefaultTypeArg = true
     )
diff --git a/rust/ql/lib/codeql/rust/internal/typeinference/TypeMention.qll b/rust/ql/lib/codeql/rust/internal/typeinference/TypeMention.qll
index a1177e55cc9..b41e86ed9a9 100644
--- a/rust/ql/lib/codeql/rust/internal/typeinference/TypeMention.qll
+++ b/rust/ql/lib/codeql/rust/internal/typeinference/TypeMention.qll
@@ -247,12 +247,20 @@ private module MkTypeMention<getAdditionalPathTypeAtSig/2 getAdditionalPathTypeA
       )
     }
 
-    private Type getPositionalTypeArgument(int i, TypePath path) {
-      result = getPathTypeArgument(this, i).getTypeAt(path)
+    private Type getPositionalTypeArgument(int i, TypePath path, boolean isDefault) {
+      result = getPathTypeArgument(this, i).getTypeAt(path) and
+      isDefault = false
       or
-      // Defaults only apply to type mentions in type annotations
-      this.isInTypeAnnotation() and
-      result = this.getDefaultPositionalTypeArgument(i, path)
+      result = this.getDefaultPositionalTypeArgument(i, path) and
+      isDefault = true
+    }
+
+    pragma[nomagic]
+    Type getTypeArgument(TypeParameter tp, TypePath path, boolean isDefault) {
+      exists(int i |
+        result = this.getPositionalTypeArgument(pragma[only_bind_into](i), path, isDefault) and
+        tp = this.resolveRootType().getPositionalTypeParameter(pragma[only_bind_into](i))
+      )
     }
 
     /**
@@ -260,9 +268,8 @@ private module MkTypeMention<getAdditionalPathTypeAtSig/2 getAdditionalPathTypeA
      * type parameter does not correspond directly to a type mention.
      */
     private Type getTypeForTypeParameterAt(TypeParameter tp, TypePath path) {
-      exists(int i |
-        result = this.getPositionalTypeArgument(pragma[only_bind_into](i), path) and
-        tp = this.resolveRootType().getPositionalTypeParameter(pragma[only_bind_into](i))
+      exists(boolean isDefault | result = this.getTypeArgument(tp, path, isDefault) |
+        isDefault = false or this.isInTypeAnnotation()
       )
       or
       // Handle the special syntactic sugar for function traits. The syntactic

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 went for a slightly different refactor.

Comment on lines 2816 to 2817
result = this.getPathResolutionResolved() and
result = i.getASuccessor(_) and
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
result = this.getPathResolutionResolved() and
result = i.getASuccessor(_) and
result = this.getPathResolutionResolved(i) and

This is equivalent to changing i.getASuccessor(_) to i.getAnAssocItem() which I think this is what we want.

On rust db this roughly halves the size of this predicate with no changes to inferType.

More generally, I rarely think that we actually want to use getASuccessor on ImplItem in type inference. With recent changes that I made to path resolution getASuccessor can give anything that is available on Self inside the impl block, which is not necessarily functions that are actually within the impl block (see relevantSelfFunctionName).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good catch.

f = impl.getASuccessor(_) and
not impl.(Impl).hasTrait() and
tp = TTypeParamTypeParameter(impl.resolveSelfTy().getTypeParam(0)) and
not f.hasSelfParam() and
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
not f.hasSelfParam() and

f has type NonMethodFunction so this is superflous.

// For inherent implementations of generic types, we also need to check the type being
// implemented. We arbitrarily choose the first type parameter of the type being implemented
// to represent this case.
f = impl.getASuccessor(_) and
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
f = impl.getASuccessor(_) and
f = impl.getAnAssocItem() and

Similar to this comment.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

no-change-note-required This PR does not need a change note Rust Pull requests that update Rust code

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants