-
Notifications
You must be signed in to change notification settings - Fork 10.5k
[CodeComplete] Compute type relations for global cached results #41000
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
[CodeComplete] Compute type relations for global cached results #41000
Conversation
@swift-ci Please smoke test macOS |
afb517a
to
ff4114c
Compare
@swift-ci Please smoke test |
ff4114c
to
26b045e
Compare
@swift-ci Please smoke test |
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'm still thinking how to avoid using vector for result type...
for (auto Proto : OpaqueType->getConformsTo()) { | ||
USRConforms.push_back( | ||
USRBasedType::fromType(Proto->getDeclaredInterfaceType(), Arena)); | ||
} |
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.
Handle Opaque->getSuperclass()
too?
class MyClass {}
protocol MyProto {}
func fn<T: MyClass & MyProto>() -> T {
return #^HERE^#
}
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.
Which result are you expecting here?
class MyClass {}
protocol MyProto {}
func fn<T: MyClass & MyProto>() -> T {
return MyClass() // error: cannot convert return expression of type 'MyClass' to return type 'T'
}
When adding MyClass: MyProto
.
class MyClass: MyProto {}
protocol MyProto {}
func fn<T: MyClass & MyProto>() -> T { // warning: redundant conformance constraint 'T' : 'MyProto'
return MyClass() // error: cannot convert return expression of type 'MyClass' to return type 'T'
}
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.
Hm, OK, we can't know the actual expected type if the context type is a generic parameter. But that applies to this as well, right?
class MyClass {}
func fn<T: MyProto>() -> T {
return #^HERE^#
}
So should we limit this to OpaqueTypeArchetypeType
?
Anyway, I believe we still need to handle getSuperclass()
too.
class C {}
class P {}
class D: C, P {}
class E: P {}
func fn() -> some C & P {
return #^HERE^#
}
D
is OK, but E
is not.
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, that makes sense. Implemented in 10d5d0d
lib/IDE/CodeCompletionResultType.cpp
Outdated
} | ||
|
||
const USRBasedType *USRBasedType::fromType(Type Ty, USRBasedTypeArena &Arena) { | ||
// FIXME: We can't mangle archetypes, so we treat them as null USRBasedTypes. |
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.
An example of archetype case is something like this?
func returnCollecton<T: MyProto>() -> T {}
If so, can we construct USRBasedType
with empty USR and non-empty super types?
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.
This actually works. We are reporting convertible type relations for types that conform to MyProto
here. I added a test case for it. (dab4a2f)
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 mean when suggesting it. The test case would be something like:
//-- Lib.swift
public protocol MyProto {}
public func returnSomeMyProto() -> some MyProto { ... }
//-- test.swift
import Lib
func foo<T: MyProto>(_: T)
func test() {
foo(#^HERE^#) // 'returnSomeMyProto()' should be prioritized.
}
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 see, I decided to special case some
with a single protocol for now as representing the conjunction of two types isn’t currently supported in USRBasedType
and it’s not too simple to put it in. Since the entire USRBasedType thing is just an under-approximation anyway, I think it’s fine if we can’t represent some MyProto & MyOtherProto
for now. 6aff8ef
I actually think the vector is a rather nice solution.
|
@swift-ci Please smoke test |
/// This is not a single unique type because for code completion we consider | ||
/// e.g. \c Int as producing both an \c Int metatype and an \c Int instance | ||
/// type. | ||
SmallVector<PointerUnion<Type, const USRBasedType *>, 1> ResultTypes; |
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 understand that using a "collection" might be a good solution.
However, again, CodeCompletionResultType
resides in BumpPtrAllocator
so we can't use SmallVector
which requires the destructor call. ArrayRef
might be OK, but how about using llvm::TrailingObject
so we can pack the size()
with IsNotApplicable
flag, and save a pointer size of data()
. USRBasedType
could use TrailingObject
too...
(I'm OK withArrayRef
for now. TrailingObject
can be followups )
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.
Ah, now I finally understand what your concern about the collection was. As discussed offline, I decided to store the result types using two pointers since we don’t need a collection with more than 2 elements at the moment anyway 9ffc3a0
for (auto Proto : OpaqueType->getConformsTo()) { | ||
USRConforms.push_back( | ||
USRBasedType::fromType(Proto->getDeclaredInterfaceType(), Arena)); | ||
} |
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.
Hm, OK, we can't know the actual expected type if the context type is a generic parameter. But that applies to this as well, right?
class MyClass {}
func fn<T: MyProto>() -> T {
return #^HERE^#
}
So should we limit this to OpaqueTypeArchetypeType
?
Anyway, I believe we still need to handle getSuperclass()
too.
class C {}
class P {}
class D: C, P {}
class E: P {}
func fn() -> some C & P {
return #^HERE^#
}
D
is OK, but E
is not.
lib/IDE/CodeCompletionResultType.cpp
Outdated
} | ||
|
||
const USRBasedType *USRBasedType::fromType(Type Ty, USRBasedTypeArena &Arena) { | ||
// FIXME: We can't mangle archetypes, so we treat them as null USRBasedTypes. |
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 mean when suggesting it. The test case would be something like:
//-- Lib.swift
public protocol MyProto {}
public func returnSomeMyProto() -> some MyProto { ... }
//-- test.swift
import Lib
func foo<T: MyProto>(_: T)
func test() {
foo(#^HERE^#) // 'returnSomeMyProto()' should be prioritized.
}
lib/IDE/CodeCompletionResultType.cpp
Outdated
if (auto Nominal = Ty->getAnyNominal()) { | ||
// Sorted conformances so we get a deterministic supertype order and can | ||
// assert that USRBasedTypes with the same USR have the same supertypes. | ||
auto Conformances = Nominal->getAllConformances(/*sorted=*/true); |
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.
protocol P1 {}
protocol P2: P1 {}
class B: P2 {}
protocol Q1 {}
protocol Q2: Q1 {}
class C: B, Q2 {}
In this case, USRBasedType::forType(C)
would be like
p1 = {"P1"}
p2 = {"P2", &p1}
b = {"B", &p1, &p2}
q1 = {"Q1"}
q2 = {"Q2", &q1}
c = {"C", &p1, &p2, &q1, &q2, &b}
right? This looks like a waste of the space. Could you limit Supertypes
only contains the "direct" super types? So the structure be like:
p1 = {"P1"}
p2 = {"P2", &p1}
b = {"B", &p2}
q1 = {"Q1"}
q2 = {"Q2", &q1}
c = {"C", &q2, &b}
When calculating the type relation, we should be able to collect all super type USRs by recursively look into the graph. Of course we need to be careful not to visit the same node twice, but it shouldn't be hard.
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. It might slow down the computation of USRBasedTypes a little but since that should only happen once to build up the cache, I think it’s a good tradeoff daf8fb0
ebdd76f
to
9ffc3a0
Compare
@swift-ci Please smoke test |
1 similar comment
@swift-ci Please smoke test |
2d66a9c
to
d5a8435
Compare
@swift-ci Please smoke test |
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.
Added some comment, but nothing critical. We can do them in followups
|
||
// For opaque types like 'some View', consider them equivalent to 'View'. | ||
if (auto OpaqueType = Ty->getAs<ArchetypeType>()) { | ||
if (auto Existential = OpaqueType->getExistentialType()) { |
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.
Does this support protocol with associated types? Adding a test case would be great.
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.
It does, but while playing around with them I discovered another bug
public protocol ProtoWithAssocType {
associatedtype MyAssoc
}
public func makeProtoWithAssocType() -> some ProtoWithAssocType { return StructWithAssocType() }
func protoWithAssocTypeInGenericContext<T: ProtoWithAssocType>() -> T {
return #^COMPLETE^#
}
would suggest makeProtoWithAssocType
as “Identical” even though it is not valid here. The problem was that we were doing some of the special opaque return type handling for USRBasedTypes for all archetypes. Fix was to only apply them to OpaqueTypeArchetypeType
. This regressed test cases which are using a generic context, but I think that’s fine since the main known limitation of the USR-based type comparisons is that they don’t support generic contexts. 5a1dd49
lib/IDE/CodeCompletionResultType.cpp
Outdated
return TypeRelation::Identical; | ||
} | ||
for (auto *Supertype : ResultType->getSupertypes()) { | ||
if (this->typeRelation(Supertype, VoidType) >= TypeRelation::Convertible) { |
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.
When the hierarchy contains diamond conformance,
protocol Base {}
protocol P1: Base {}
protocol P2: Base {}
struct S: P1, P2 {}
I think Base
is visited twice. I'm not sure this happens lot, and not sure it causes excessive waste. But this logic should have "visited" check.
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.
Added it b12d82a
PointerUnion<Type, const USRBasedType *> ResultType1, | ||
PointerUnion<Type, const USRBasedType *> ResultType2) | ||
: ResultType1AndIsApplicable(ResultType1, IsApplicable), | ||
ResultType2(ResultType2) {} |
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.
Insert assert(ResultType1.is<Type>() == ResultType2.is<Type>());
or do we have a scenario mixing 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.
We don’t have a scenario where we’re mixing them but we never make assumptions that both of them are of the same kind either. Adding the assertion just adds an artificial restriction that’s not necessary IMO.
d5a8435
to
b12d82a
Compare
@swift-ci Please smoke test |
@swift-ci Please SourceKit stress test |
Computing the type relation for every item in the code completion cache is way to expensive (~4x slowdown for global completion that imports `SwiftUI`). Instead, compute a type’s supertypes (protocol conformances and superclasses) once and write their USRs to the cache. To compute a type relation we can then check if the contextual type is in the completion item’s supertypes. This reduces the overhead of computing the type relations (again global completion that imports `SwiftUI`) to ~6% – measured by instructions executed. Technically, we might miss some conversions like - retroactive conformances inside another module (because we can’t cache them if that other module isn’t imported) - complex generic conversions (just too complicated to model using USRs) Because of this, we never report an `unrelated` type relation for global items but always default to `unknown`. But I believe this change covers the most common cases and is a good tradeoff between accuracy and performance. rdar://83846531
b12d82a
to
640cfac
Compare
@swift-ci Please smoke test |
@swift-ci Please SourceKit stress test |
Sendable conformances are lazily synthesized as they are needed by the compiler. Depending on whether we checked whether a type conforms to Sendable before constructing the USRBasedType, we get different results for its conformance, causing assertion failures. For now, always ignore the Sendable conformance. We need to come up with a better solution for this problem in the future.
…based types Previously, for every type, we were reconstructing its entire supertype hierarchy as USRBasedTypes before checking the already computed USRBasedTypes. This lead to unneccary duplicated work.
@swift-ci Please SourceKit stress test |
@swift-ci Please smoke test |
https://ci.swift.org/job/oss-swift-pr-test-macoss/123 started failing after this change, reverting in #41627 |
Computing the type relation for every item in the code completion cache is way to expensive (~4x slowdown for global completion that imports
SwiftUI
). Instead, compute a type’s supertypes (protocol conformances and superclasses) once and write their USRs to the cache. To compute a type relation we can then check if the contextual type is in the completion item’s supertypes.This reduces the overhead of computing the type relations (again global completion that imports
SwiftUI
) to ~6% – measured by instructions executed.Technically, we might miss some conversions like
Because of this, we never report an
unrelated
type relation for global items but always default tounknown
.But I believe this change covers the most common cases and is a good tradeoff between accuracy and performance.
rdar://83846531
@rintaro I needed to undo the change you suggested in #40999 (comment):
When we were only storing AST-based types, we could have a boolean flag to also consider metatypes. This trick is harder to pull off with USR-based types, because we can’t just invoke the MetatypeType constructor with the instance type.
We could hack together a USR-based instance type to Metatype transformation by appending an "m" to the instance type’s USR but I decided to store the metatype as another possible result type of
CodeCompletionResultType
because:USRBasedType::fromType
CodeCompletionResultType
containSmallVector<Type, 1>
instead ofType
.