-
Notifications
You must be signed in to change notification settings - Fork 1.8k
Rust: Support blanket implementations #20133
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: Support blanket implementations #20133
Conversation
983257d
to
2e98c24
Compare
2e98c24
to
f130198
Compare
f130198
to
3b9f683
Compare
bea1e3b
to
8fa4d0d
Compare
8fa4d0d
to
0efa72e
Compare
0314813
to
8c91ef0
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 implements method resolution for blanket implementations in Rust type inference, specifically targeting blanket implementations where the type parameter has trait bounds. The implementation enables CodeQL to better resolve method calls that depend on blanket trait implementations.
Key changes:
- Added support for blanket implementation method resolution with trait bounds
- Enhanced type inference to handle universally quantified type parameters in constraints
- Updated test cases to reflect improved method resolution capabilities
Reviewed Changes
Copilot reviewed 15 out of 15 changed files in this pull request and generated 4 comments.
Show a summary per file
File | Description |
---|---|
shared/typeinference/codeql/typeinference/internal/TypeInference.qll | Added useUniversalConditions predicate to control constraint satisfaction for universally quantified type parameters |
rust/ql/lib/codeql/rust/internal/TypeInference.qll | Core implementation of blanket implementation support including detection, canonicalization, and method resolution |
rust/ql/lib/codeql/rust/internal/PathResolution.qll | Added resolveBound method to resolve type parameter bounds by index |
rust/ql/test/library-tests/type-inference/blanket_impl.rs | New test file demonstrating blanket implementation scenarios |
Multiple test expected files | Updated to reflect improved method resolution results from blanket implementation support |
not trait.getName().getText() = | ||
[ | ||
"Sized", "Clone", | ||
// The auto traits | ||
"Send", "Sync", "Unpin", "UnwindSafe", "RefUnwindSafe" | ||
] |
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 hardcoded list of excluded trait names should be extracted to a constant or configuration to improve maintainability and make it easier to extend.
Copilot uses AI. Check for mistakes.
* We detect these duplicates based on some simple heuristics (same trait | ||
* name, file name, etc.). For these duplicates we select the one with the | ||
* greatest file name (which usually is also the one with the greatest library | ||
* version in the path) |
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 heuristic for selecting the 'canonical' implementation based on greatest file name seems fragile and may not always correspond to the greatest library version. Consider documenting the limitations of this approach or exploring more robust versioning detection.
* version in the path) | |
* version in the path). **Note:** This heuristic is fragile and may not always | |
* correspond to the greatest library version, as file names may not reliably | |
* encode version information and lexicographical order does not match semantic | |
* versioning. Users should be aware of this limitation and consider more robust | |
* version detection if precise canonicalization is required. |
Copilot uses AI. Check for mistakes.
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 work!
// Here `TryFlagExt::try_read_flag_twice` is since there is a blanket | ||
// implementaton of `TryFlag` for `Flag`. |
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.
grammar
private TypeParamItemNode getBlanketImplementationTypeParam(Impl impl) { | ||
result = impl.(ImplItemNode).resolveSelfTy() and | ||
result = impl.getGenericParamList().getAGenericParam() and | ||
// This impl block is not superseded by the expansion of an attribute macro. | ||
not exists(impl.getAttributeMacroExpansion()) | ||
} | ||
|
||
predicate isBlanketImplementation(Impl impl) { exists(getBlanketImplementationTypeParam(impl)) } |
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 make these two predicates member predicates on ImplItemNode
instead?
) | ||
or | ||
isCanonicalImpl(impl) and result = impl | ||
} |
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.
How about replacing from line 2169 with
/**
* Gets the canonical blanket implementation for `impl`.
*
* Libraries can often occur several times in the database for different
* library versions. This causes the same blanket implementations to exist
* multiple times, and these add no useful information.
*
* We detect these duplicates based on some simple heuristics (same trait
* name, file name, etc.). For these duplicates we select the one with the
* greatest file name (which usually is also the one with the greatest library
* version in the path)
*/
Impl getCanonicalImpl(Impl impl) {
exists(string fileName, string traitName, int arity, string tpName |
impl = getPotentialDuplicated(fileName, traitName, arity, tpName)
|
result =
max(Impl impl0, Location l, string absolutePath, int startLine |
impl0 = getPotentialDuplicated(fileName, traitName, arity, tpName) and
l = impl0.getLocation() and
absolutePath = l.getFile().getAbsolutePath() and
startLine = l.getStartLine()
|
impl0 order by absolutePath, startLine
)
)
}
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.
As mentioned offline I realized that we don't actually need getCanonicalImpl
at all. It was only used in isCanonicalBlanketImplementation
which was just a re-implementation of isCanonicalImpl
.
* Holds if `impl` is a blanket implementation for a type parameter and the type | ||
* parameter must implement `trait`. |
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.
nit: how about Holds if `impl` is a blanket implementation for a type parameter with trait bound `trait`.
?
not exists(impl.getAssocItem(name)) and | ||
f = impl.resolveTraitTy().getAssocItem(name) |
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.
How come this case is not filtered away by the constraint below?
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.
If we have impl<T : Bar> Foo for T
then:
- If
Foo
has a super trait with a default implementation, then we want to consider that default implementation as a target. That's what this case allows for. - If
Foo
andBar
has a common super traitBaz
then methods onBaz
are available both throughBar
andFoo
, and finding them through the blanket implementation doesn't add anything. That what the below filter does.
So the filter might remove some targets added by the case, but not all of them.
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.
- If
Foo
has a super trait with a default implementation, then we want to consider that default implementation as a target. That's what this case allows for.
But f = impl.resolveTraitTy().getAssocItem(name)
means that f
is defined inside Foo
and not in a super trait of Foo
?
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.
You're right, my example doesn't add up.
I think I added the filter for cases like this:
trait ApplicationImpl: ApplicationImplExt {
fn activate(&self) { /* body */ }
}
impl<T: ApplicationImpl> ApplicationImplExt for T {}
Here we can get the activate
method through the trait bound ApplicationImpl
and hence it's not interesting to get it through the blanket implementation. I don't understand what purpose this blanket implementation serves in the first case, but it's from the gio
library used in tauri
.
* `arity`. | ||
*/ | ||
private predicate blanketImplementationMethod( | ||
ImplItemNode impl, Trait trait, string name, int arity, Function f |
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 rename trait
to traitBound
, and in predicates that call this predicate? This makes it clear that it is not the same as trait
in relevantTraitVisible
.
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.
Good idea. I myself got confused by that at some point ;)
predicate methodCallMatchesBlanketImpl(MethodCall mc, Type t, Impl impl, Trait trait, Function f) { | ||
// Only check method calls where we have ruled out inherent method targets. | ||
// Ideally we would also check if non-blanket method targets have been ruled | ||
// out. | ||
methodCallHasNoInherentTarget(mc) and | ||
exists(string name, int arity | | ||
isMethodCall(mc, t, name, arity) and | ||
blanketImplementationMethod(impl, trait, name, arity, f) | ||
) | ||
} | ||
|
||
private predicate relevantTraitVisible(Element mc, Trait trait) { | ||
exists(ImplItemNode impl | | ||
methodCallMatchesBlanketImpl(mc, _, impl, _, _) and | ||
trait = impl.resolveTraitTy() | ||
) | ||
} | ||
|
||
module SatisfiesConstraintInput implements SatisfiesConstraintInputSig<MethodCall> { | ||
pragma[nomagic] | ||
predicate relevantConstraint(MethodCall mc, Type constraint) { | ||
exists(Trait trait, Trait trait2, ImplItemNode impl | | ||
methodCallMatchesBlanketImpl(mc, _, impl, trait, _) and | ||
TraitIsVisible<relevantTraitVisible/2>::traitIsVisible(mc, pragma[only_bind_into](trait2)) and | ||
trait2 = pragma[only_bind_into](impl.resolveTraitTy()) and | ||
trait = constraint.(TraitType).getTrait() | ||
) | ||
} | ||
|
||
predicate useUniversalConditions() { none() } | ||
} | ||
|
||
predicate hasBlanketImpl(MethodCall mc, Type t, Impl impl, Trait trait, Function f) { | ||
SatisfiesConstraint<MethodCall, SatisfiesConstraintInput>::satisfiesConstraintType(mc, | ||
TTrait(trait), _, _) and | ||
methodCallMatchesBlanketImpl(mc, t, impl, trait, f) | ||
} |
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.
How about
pragma[nomagic]
predicate methodCallMatchesBlanketImpl(
MethodCall mc, Type t, ImplItemNode impl, Trait traitBound, Trait traitImpl, Function f
) {
// Only check method calls where we have ruled out inherent method targets.
// Ideally we would also check if non-blanket method targets have been ruled
// out.
methodCallHasNoInherentTarget(mc) and
exists(string name, int arity |
isMethodCall(mc, t, name, arity) and
blanketImplementationMethod(impl, traitBound, name, arity, f)
) and
traitImpl = impl.resolveTraitTy()
}
private predicate relevantTraitVisible(Element mc, Trait trait) {
methodCallMatchesBlanketImpl(mc, _, _, _, trait, _)
}
module SatisfiesConstraintInput implements SatisfiesConstraintInputSig<MethodCall> {
pragma[nomagic]
predicate relevantConstraint(MethodCall mc, Type constraint) {
exists(Trait traitBound, Trait traitImpl, ImplItemNode impl |
methodCallMatchesBlanketImpl(mc, _, impl, traitBound, traitImpl, _) and
TraitIsVisible<relevantTraitVisible/2>::traitIsVisible(mc, traitImpl) and
traitBound = constraint.(TraitType).getTrait()
)
}
predicate useUniversalConditions() { none() }
}
predicate hasBlanketImpl(MethodCall mc, Type t, Impl impl, Trait traitBound, Function f) {
SatisfiesConstraint<MethodCall, SatisfiesConstraintInput>::satisfiesConstraintType(mc,
TTrait(traitBound), _, _) and
methodCallMatchesBlanketImpl(mc, t, impl, traitBound, _, f)
}
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, factors out the resolveTraitTy
call so it appears just once.
0799edc
to
71a5549
Compare
71a5549
to
b8eb19a
Compare
b8eb19a
to
e2e6fd0
Compare
Thanks for the review :) I should've addressed the comments now. |
} | ||
|
||
/** | ||
* Holds if `mc` has `rootType` as the root type of the reciever and the target |
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.
nit: reciever
-> receiver
.
not exists(impl.getAssocItem(name)) and | ||
f = impl.resolveTraitTy().getAssocItem(name) |
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.
- If
Foo
has a super trait with a default implementation, then we want to consider that default implementation as a target. That's what this case allows for.
But f = impl.resolveTraitTy().getAssocItem(name)
means that f
is defined inside Foo
and not in a super trait of Foo
?
This PR implements method resolution for blanket implementations that satisfies the criteria: the type parameter that the implementation targets has a trait bound.
For instance, this
impl
blockis handled as there is a trait bound
AsyncRead
on the type parameterR
.On the other hand, this
impl
blockis not handled since there is not trait bound for
T
.At a high level, the implementation works as follows:
foo.bar
check if there's a blanket implementation that has the methodbar
.foo
satisfies the trait bound.foo.bar
to the method from the blanket implementation.If there are multiple trait bounds on the type parameter, then only one is checked to be satisfied. This could give false results, but from looking at some results in doesn't look too bad after filtering away some trivial traits (
Clone
,Send
, etc.).The DCA report seems great. A 0.819% point increase in resolved calls with little to no change to performance. Some inconsistencies are up, but a good chunk of that is probably explained by more type information in general.