-
Notifications
You must be signed in to change notification settings - Fork 1.9k
Rust: Distinguish &mut T from &T in type inference
#20973
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Rust: Distinguish &mut T from &T in type inference
#20973
Conversation
f8e7c13 to
a74c4e5
Compare
b13cf2b to
c62c00a
Compare
078f152 to
d1eb4ff
Compare
19551e3 to
5682de7
Compare
5682de7 to
99b0d0c
Compare
| string derefChain, boolean borrow, TypePath path | ||
| ) { | ||
| result = substituteLookupTraits(this.getACandidateReceiverTypeAt(derefChain, borrow, path)) | ||
| Type getANonPseudoCandidateReceiverTypeAt(string derefChain, BorrowKind borrow, TypePath path) { |
Check warning
Code scanning / CodeQL
Missing QLDoc for parameter Warning
85c8f0d to
63e381c
Compare
There was a problem hiding this 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 improves Rust type inference by distinguishing between mutable references (&mut T) and shared references (&T), which were previously treated as a single RefType. The change significantly reduces path resolution inconsistencies by eliminating ambiguity when methods are implemented for both borrow types.
Key Changes:
- Split
RefTypeintoRefSharedTypeandRefMutTypein the type system - Updated method resolution to prioritize shared borrows over mutable borrows when constructing candidate receiver types
- Enhanced type inference to track borrow mutability through patterns, reference expressions, and method calls
Reviewed changes
Copilot reviewed 34 out of 34 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| rust/ql/lib/codeql/rust/internal/Type.qll | Refactored RefType into an abstract base class with RefSharedType and RefMutType subclasses |
| rust/ql/lib/codeql/rust/frameworks/stdlib/Builtins.qll | Updated builtin type definitions to distinguish shared and mutable reference types |
| rust/ql/lib/codeql/rust/internal/TypeInference.qll | Enhanced method resolution with BorrowKind enum to track shared/mutable borrows, prioritizing shared borrows in candidate generation |
| rust/ql/lib/codeql/rust/internal/TypeMention.qll | Updated type mention resolution to correctly handle mutable vs shared references |
| rust/ql/lib/codeql/rust/internal/PathResolution.qll | Updated builtin type resolution to distinguish reference types based on mutability |
| rust/ql/lib/codeql/rust/elements/internal/OperationImpl.qll | Added TODO comment for handling DerefMut trait |
| rust/ql/test/**/*.expected | Updated test expectations reflecting reduced path resolution inconsistencies |
| rust/ql/test/**/main.rs, dereference.rs, pattern_matching.rs | Updated test annotations to reflect correct TRefMut types |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| // todo: handle `core::ops::deref::DerefMut` | ||
| op = "*" and path = "core::ops::deref::Deref" and method = "deref" and borrows = 1 |
Copilot
AI
Dec 17, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The TODO comment indicates that core::ops::deref::DerefMut is not yet handled. Since this PR introduces distinction between mutable and shared borrows, the dereference operator for mutable references should also be handled to properly support the DerefMut trait, which is used when dereferencing &mut T types. Without this, type inference for operations on mutable references may be incomplete.
| // todo: handle `core::ops::deref::DerefMut` | |
| op = "*" and path = "core::ops::deref::Deref" and method = "deref" and borrows = 1 | |
| op = "*" and path = "core::ops::deref::Deref" and method = "deref" and borrows = 1 | |
| or | |
| op = "*" and path = "core::ops::deref::DerefMut" and method = "deref_mut" and borrows = 1 |
paldepind
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks really great! I only have a bunch of nitpicky comments.
| // forex using recursion | ||
| pragma[nomagic] | ||
| private predicate hasNoCompatibleNonBlanketTargetBorrowToIndex( | ||
| private predicate hasNoCompatibleTargetMutBorrowToIndex( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The code repetition here is a bit unfortunate, but I guess it's not that easy to get rid of.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, I don't think we can easily get rid of the duplication.
| pos.isSelf() and borrows >= 1 | ||
| pos.isSelf() and | ||
| borrows >= 1 and | ||
| if this instanceof AssignmentOperation |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could this be
| if this instanceof AssignmentOperation | |
| if this instanceof CompoundAssignmentExpr |
?
I understand why we want this for stuff like a += 1 which is equivalent to (&mut a).add_assign(1). But do we need it for normal assignments that (in some cases) are handled by SSA?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
CompoundAssignmentExpr will do the same here because of the .isOverloaded restriction, but happy to change.
|
|
||
| /** Holds if `n` is implicitly borrowed. */ | ||
| cached | ||
| predicate implicitBorrow(AstNode n) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I wonder if it might make sense to expose mutability here as well.
| predicate implicitBorrow(AstNode n) { | |
| predicate implicitBorrow(AstNode n, boolean mutable) { |
That might be useful elsewhere. For instance in mutablyBorrows in SsaImpl.qll this
e = any(MethodCallExpr mc).getReceiver() and
e.(VariableAccess).getVariable() = v and
v.isMutable()is a rough approximation that could (in the future) benefit from using this predicate instead.
| abstract class RefType extends StructType { } | ||
|
|
||
| pragma[nomagic] | ||
| TypeParamTypeParameter getRefTypeParameter() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Instead of having getRefTypeParameter, getRefMutTypeParameter, and getRefSharedTypeParameter we could have a single getRefTypeParameter(boolean mutable):
pragma[nomagic]
RefType getRefType(boolean mutable) {
mutable = true and result = any(RefMutType t)
or
mutable = false and result = any(RefSharedType t)
}
pragma[nomagic]
TypeParamTypeParameter getRefTypeParameter(boolean mutable) {
result = getRefType(mutable).getPositionalTypeParameter(0)
}| or | ||
| ref = any(RefPat rp | if rp.isMut() then isMut = true else isMut = false) | ||
| | | ||
| if isMut = true then result instanceof RefMutType else result instanceof RefSharedType |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Using the getRefType suggested above.
| if isMut = true then result instanceof RefMutType else result instanceof RefSharedType | |
| result = getRefType(isMut) |
| exists(BorrowKind borrow, RefType rt | | ||
| this.implicitBorrowAt(pos, borrow) and | ||
| rt = borrow.getRefType() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| exists(BorrowKind borrow, RefType rt | | |
| this.implicitBorrowAt(pos, borrow) and | |
| rt = borrow.getRefType() | |
| exists(boolean mutable, RefType rt | | |
| this.implicitBorrowAt(pos, mutable) and | |
| rt = getRefType(mutable) |
| override predicate argumentHasImplicitBorrow(AstNode arg, BorrowKind borrow) { | ||
| exists(ArgumentPosition pos | | ||
| this.implicitBorrowAt(pos) and | ||
| this.implicitBorrowAt(pos, borrow) and |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| override predicate argumentHasImplicitBorrow(AstNode arg, BorrowKind borrow) { | |
| exists(ArgumentPosition pos | | |
| this.implicitBorrowAt(pos) and | |
| this.implicitBorrowAt(pos, borrow) and | |
| override predicate argumentHasImplicitBorrow(AstNode arg, boolean mutable) { | |
| exists(ArgumentPosition pos | | |
| this.implicitBorrowAt(pos, mutable) and |
| private newtype TBorrowKind = | ||
| TNoBorrowKind() or | ||
| TSharedBorrowKind() or | ||
| TMutBorrowKind() | ||
|
|
||
| private class BorrowKind extends TBorrowKind { | ||
| predicate isNoBorrow() { this = TNoBorrowKind() } | ||
|
|
||
| RefType getRefType() { | ||
| this = TSharedBorrowKind() and | ||
| result instanceof RefSharedType | ||
| or | ||
| this = TMutBorrowKind() and | ||
| result instanceof RefMutType | ||
| } | ||
|
|
||
| string toString() { | ||
| this = TNoBorrowKind() and | ||
| result = "" | ||
| or | ||
| this = TSharedBorrowKind() and | ||
| result = "&" | ||
| or | ||
| this = TMutBorrowKind() and | ||
| result = "&mut" | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Perhaps this
| private newtype TBorrowKind = | |
| TNoBorrowKind() or | |
| TSharedBorrowKind() or | |
| TMutBorrowKind() | |
| private class BorrowKind extends TBorrowKind { | |
| predicate isNoBorrow() { this = TNoBorrowKind() } | |
| RefType getRefType() { | |
| this = TSharedBorrowKind() and | |
| result instanceof RefSharedType | |
| or | |
| this = TMutBorrowKind() and | |
| result instanceof RefMutType | |
| } | |
| string toString() { | |
| this = TNoBorrowKind() and | |
| result = "" | |
| or | |
| this = TSharedBorrowKind() and | |
| result = "&" | |
| or | |
| this = TMutBorrowKind() and | |
| result = "&mut" | |
| } | |
| } | |
| private newtype TBorrowKind = | |
| TNoBorrowKind() or | |
| TSomeBorrowKind(Boolean mutable) | |
| private class BorrowKind extends TBorrowKind { | |
| predicate isNoBorrow() { this = TNoBorrowKind() } | |
| RefType getRefType() { | |
| result = getRefType(any(boolean mutable | this = TSomeBorrowKind(mutable))) | |
| } | |
| string toString() { | |
| this.isNoBorrow() and | |
| result = "" | |
| or | |
| result = this.getRefType().toString() | |
| } | |
| } |
or using Option.
| predicate argumentHasImplicitBorrow(AstNode arg, BorrowKind borrow) { | ||
| exists(this.resolveCallTarget(_, "", borrow)) and | ||
| borrow != TNoBorrowKind() and |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| predicate argumentHasImplicitBorrow(AstNode arg, BorrowKind borrow) { | |
| exists(this.resolveCallTarget(_, "", borrow)) and | |
| borrow != TNoBorrowKind() and | |
| predicate argumentHasImplicitBorrow(AstNode arg, boolean mutable) { | |
| exists(this.resolveCallTarget(_, "", TSomeBorrowKind(mutable))) and |
| // adjust for implicit borrow | ||
| apos.isSelf() and | ||
| derefChainBorrow = ";borrow" and | ||
| derefChainBorrow = [";&", ";&mut"] and |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| derefChainBorrow = [";&", ";&mut"] and | |
| derefChainBorrow = encodeDerefChainBorrow("", TSomeBorrowKind(_)) and |
To avoid hard coding the encoding too much and instead use encodeDerefChainBorrow as the source of truth for the encoding.
63e381c to
aae6cd9
Compare
| apos.isSelf() and | ||
| derefChainBorrow = ".ref;" and | ||
| path = TypePath::cons(getRefTypeParameter(), path0) | ||
| derefChainBorrow = MethodCallMatchingInput::encodeDerefChainBorrow(".ref", TNoBorrowKind()) and |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice!
paldepind
left a comment
There was a problem hiding this 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! Really great stuff 🚀
Does what the PR title says, and additionally makes sure to prioritize shared borrows over mutable borrows when constructing candidate receiver types.
DCA is excellent; as expected, we see a large decrease in resolution inconsistencies, because we no longer have resolution overlap for methods with implementations for both shared and mutable borrows.