Skip to content

Commit 1c58d8b

Browse files
authored
Merge pull request #74180 from hamishknight/complete-conversion
[Completion] Better handle merging of lookup base types
2 parents f17d3d3 + 30af99e commit 1c58d8b

11 files changed

+189
-70
lines changed

include/swift/IDE/PostfixCompletion.h

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -66,13 +66,9 @@ class PostfixCompletionCallback : public TypeCheckCompletionCallback {
6666
llvm::DenseMap<AbstractClosureExpr *, ActorIsolation>
6767
ClosureActorIsolations;
6868

69-
/// Checks whether this result has the same \c BaseTy and \c BaseDecl as
70-
/// \p Other and if the two can thus be merged to be one value lookup in
71-
/// \c deliverResults.
72-
bool canBeMergedWith(const Result &Other, DeclContext &DC) const;
73-
74-
/// Merge this result with \p Other. Assumes that they can be merged.
75-
void merge(const Result &Other, DeclContext &DC);
69+
/// Merge this result with \p Other, returning \c true if
70+
/// successful, else \c false.
71+
bool tryMerge(const Result &Other, DeclContext *DC);
7672
};
7773

7874
CodeCompletionExpr *CompletionExpr;

include/swift/IDE/UnresolvedMemberCompletion.h

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -33,13 +33,9 @@ class UnresolvedMemberTypeCheckCompletionCallback
3333
/// functions is supported.
3434
bool IsInAsyncContext;
3535

36-
/// Checks whether this result has the same \c BaseTy and \c BaseDecl as
37-
/// \p Other and if the two can thus be merged to be one value lookup in
38-
/// \c deliverResults.
39-
bool canBeMergedWith(const Result &Other, DeclContext &DC) const;
40-
41-
/// Merge this result with \p Other. Assumes that they can be merged.
42-
void merge(const Result &Other, DeclContext &DC);
36+
/// Attempts to merge this result with \p Other, returning \c true if
37+
/// successful, else \c false.
38+
bool tryMerge(const Result &Other, DeclContext *DC);
4339
};
4440

4541
CodeCompletionExpr *CompletionExpr;

include/swift/Sema/IDETypeChecking.h

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,11 +60,20 @@ namespace swift {
6060
/// Typecheck binding initializer at \p bindingIndex.
6161
void typeCheckPatternBinding(PatternBindingDecl *PBD, unsigned bindingIndex);
6262

63+
/// Attempt to merge two types for the purposes of completion lookup. In
64+
/// general this means preferring a subtype over a supertype, but can also e.g
65+
/// prefer an optional over a non-optional. If the two types are incompatible,
66+
/// null is returned.
67+
Type tryMergeBaseTypeForCompletionLookup(Type ty1, Type ty2, DeclContext *dc);
68+
6369
/// Check if T1 is convertible to T2.
6470
///
6571
/// \returns true on convertible, false on not.
6672
bool isConvertibleTo(Type T1, Type T2, bool openArchetypes, DeclContext &DC);
6773

74+
/// Check whether \p T1 is a subtype of \p T2.
75+
bool isSubtypeOf(Type T1, Type T2, DeclContext *DC);
76+
6877
void collectDefaultImplementationForProtocolMembers(ProtocolDecl *PD,
6978
llvm::SmallDenseMap<ValueDecl*, ValueDecl*> &DefaultMap);
7079

include/swift/Sema/IDETypeCheckingRequests.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@ class IsDeclApplicableRequest:
8686
//----------------------------------------------------------------------------//
8787
enum class TypeRelation: uint8_t {
8888
ConvertTo,
89+
SubtypeOf
8990
};
9091

9192
struct TypePair {
@@ -153,6 +154,7 @@ struct TypeRelationCheckInput {
153154
switch(owner.Relation) {
154155
#define CASE(NAME) case TypeRelation::NAME: out << #NAME << " "; break;
155156
CASE(ConvertTo)
157+
CASE(SubtypeOf)
156158
#undef CASE
157159
}
158160
}

lib/IDE/IDETypeChecking.cpp

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -941,13 +941,58 @@ bool swift::isMemberDeclApplied(const DeclContext *DC, Type BaseTy,
941941
IsDeclApplicableRequest(DeclApplicabilityOwner(DC, BaseTy, VD)), false);
942942
}
943943

944+
Type swift::tryMergeBaseTypeForCompletionLookup(Type ty1, Type ty2,
945+
DeclContext *dc) {
946+
// Easy case, equivalent so just pick one.
947+
if (ty1->isEqual(ty2))
948+
return ty1;
949+
950+
// Check to see if one is an optional of another. In that case, prefer the
951+
// optional since we can unwrap a single level when doing a lookup.
952+
{
953+
SmallVector<Type, 4> ty1Optionals;
954+
SmallVector<Type, 4> ty2Optionals;
955+
auto ty1Unwrapped = ty1->lookThroughAllOptionalTypes(ty1Optionals);
956+
auto ty2Unwrapped = ty2->lookThroughAllOptionalTypes(ty2Optionals);
957+
958+
if (ty1Unwrapped->isEqual(ty2Unwrapped)) {
959+
// We currently only unwrap a single level of optional, so if the
960+
// difference is greater, don't merge.
961+
if (ty1Optionals.size() == 1 && ty2Optionals.empty())
962+
return ty1;
963+
if (ty2Optionals.size() == 1 && ty1Optionals.empty())
964+
return ty2;
965+
}
966+
// We don't want to consider subtyping for optional mismatches since
967+
// optional promotion is modelled as a subtype, which isn't useful for us
968+
// (i.e if we have T? and U, preferring U would miss members on T?).
969+
if (ty1Optionals.size() != ty2Optionals.size())
970+
return Type();
971+
}
972+
973+
// In general we want to prefer a subtype over a supertype.
974+
if (isSubtypeOf(ty1, ty2, dc))
975+
return ty1;
976+
if (isSubtypeOf(ty2, ty1, dc))
977+
return ty2;
978+
979+
// Incomparable, return null.
980+
return Type();
981+
}
982+
944983
bool swift::isConvertibleTo(Type T1, Type T2, bool openArchetypes,
945984
DeclContext &DC) {
946985
return evaluateOrDefault(DC.getASTContext().evaluator,
947986
TypeRelationCheckRequest(TypeRelationCheckInput(&DC, T1, T2,
948987
TypeRelation::ConvertTo, openArchetypes)), false);
949988
}
950989

990+
bool swift::isSubtypeOf(Type T1, Type T2, DeclContext *DC) {
991+
return evaluateOrDefault(DC->getASTContext().evaluator,
992+
TypeRelationCheckRequest(TypeRelationCheckInput(DC, T1, T2,
993+
TypeRelation::SubtypeOf, /*openArchetypes*/ false)), false);
994+
}
995+
951996
Type swift::getRootTypeOfKeypathDynamicMember(SubscriptDecl *SD) {
952997
return evaluateOrDefault(SD->getASTContext().evaluator,
953998
RootTypeOfKeypathDynamicMemberRequest{SD}, Type());

lib/IDE/PostfixCompletion.cpp

Lines changed: 13 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -21,31 +21,20 @@ using namespace swift;
2121
using namespace swift::constraints;
2222
using namespace swift::ide;
2323

24-
bool PostfixCompletionCallback::Result::canBeMergedWith(const Result &Other,
25-
DeclContext &DC) const {
26-
if (BaseDecl != Other.BaseDecl) {
24+
bool PostfixCompletionCallback::Result::tryMerge(const Result &Other,
25+
DeclContext *DC) {
26+
if (BaseDecl != Other.BaseDecl)
2727
return false;
28-
}
29-
if (!BaseTy->isEqual(Other.BaseTy) &&
30-
!isConvertibleTo(BaseTy, Other.BaseTy, /*openArchetypes=*/true, DC) &&
31-
!isConvertibleTo(Other.BaseTy, BaseTy, /*openArchetypes=*/true, DC)) {
32-
return false;
33-
}
34-
return true;
35-
}
3628

37-
void PostfixCompletionCallback::Result::merge(const Result &Other,
38-
DeclContext &DC) {
39-
assert(canBeMergedWith(Other, DC));
4029
// These properties should match if we are talking about the same BaseDecl.
4130
assert(IsBaseDeclUnapplied == Other.IsBaseDeclUnapplied);
4231
assert(BaseIsStaticMetaType == Other.BaseIsStaticMetaType);
4332

44-
if (!BaseTy->isEqual(Other.BaseTy) &&
45-
isConvertibleTo(Other.BaseTy, BaseTy, /*openArchetypes=*/true, DC)) {
46-
// Pick the more specific base type as it will produce more solutions.
47-
BaseTy = Other.BaseTy;
48-
}
33+
auto baseTy = tryMergeBaseTypeForCompletionLookup(BaseTy, Other.BaseTy, DC);
34+
if (!baseTy)
35+
return false;
36+
37+
BaseTy = baseTy;
4938

5039
// There could be multiple results that have different actor isolations if the
5140
// closure is an argument to a function that has multiple overloads with
@@ -66,18 +55,15 @@ void PostfixCompletionCallback::Result::merge(const Result &Other,
6655
ExpectsNonVoid &= Other.ExpectsNonVoid;
6756
IsImpliedResult |= Other.IsImpliedResult;
6857
IsInAsyncContext |= Other.IsInAsyncContext;
58+
return true;
6959
}
7060

7161
void PostfixCompletionCallback::addResult(const Result &Res) {
72-
auto ExistingRes =
73-
llvm::find_if(Results, [&Res, DC = DC](const Result &ExistingResult) {
74-
return ExistingResult.canBeMergedWith(Res, *DC);
75-
});
76-
if (ExistingRes != Results.end()) {
77-
ExistingRes->merge(Res, *DC);
78-
} else {
79-
Results.push_back(Res);
62+
for (auto idx : indices(Results)) {
63+
if (Results[idx].tryMerge(Res, DC))
64+
return;
8065
}
66+
Results.push_back(Res);
8167
}
8268

8369
void PostfixCompletionCallback::fallbackTypeCheck(DeclContext *DC) {

lib/IDE/UnresolvedMemberCompletion.cpp

Lines changed: 11 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -21,43 +21,27 @@ using namespace swift;
2121
using namespace swift::constraints;
2222
using namespace swift::ide;
2323

24-
bool UnresolvedMemberTypeCheckCompletionCallback::Result::canBeMergedWith(
25-
const Result &Other, DeclContext &DC) const {
26-
if (!isConvertibleTo(ExpectedTy, Other.ExpectedTy, /*openArchetypes=*/true,
27-
DC) &&
28-
!isConvertibleTo(Other.ExpectedTy, ExpectedTy, /*openArchetypes=*/true,
29-
DC)) {
24+
bool UnresolvedMemberTypeCheckCompletionCallback::Result::tryMerge(
25+
const Result &Other, DeclContext *DC) {
26+
auto expectedTy = tryMergeBaseTypeForCompletionLookup(ExpectedTy,
27+
Other.ExpectedTy, DC);
28+
if (!expectedTy)
3029
return false;
31-
}
32-
return true;
33-
}
3430

35-
void UnresolvedMemberTypeCheckCompletionCallback::Result::merge(
36-
const Result &Other, DeclContext &DC) {
37-
assert(canBeMergedWith(Other, DC));
38-
if (!ExpectedTy->isEqual(Other.ExpectedTy) &&
39-
isConvertibleTo(ExpectedTy, Other.ExpectedTy, /*openArchetypes=*/true,
40-
DC)) {
41-
// ExpectedTy is more general than Other.ExpectedTy. Complete based on the
42-
// more general type because it offers more completion options.
43-
ExpectedTy = Other.ExpectedTy;
44-
}
31+
ExpectedTy = expectedTy;
4532

4633
IsImpliedResult |= Other.IsImpliedResult;
4734
IsInAsyncContext |= Other.IsInAsyncContext;
35+
return true;
4836
}
4937

5038
void UnresolvedMemberTypeCheckCompletionCallback::addExprResult(
5139
const Result &Res) {
52-
auto ExistingRes =
53-
llvm::find_if(ExprResults, [&Res, DC = DC](const Result &ExistingResult) {
54-
return ExistingResult.canBeMergedWith(Res, *DC);
55-
});
56-
if (ExistingRes != ExprResults.end()) {
57-
ExistingRes->merge(Res, *DC);
58-
} else {
59-
ExprResults.push_back(Res);
40+
for (auto idx : indices(ExprResults)) {
41+
if (ExprResults[idx].tryMerge(Res, DC))
42+
return;
6043
}
44+
ExprResults.push_back(Res);
6145
}
6246

6347
void UnresolvedMemberTypeCheckCompletionCallback::sawSolutionImpl(

lib/Sema/IDETypeCheckingRequests.cpp

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -246,10 +246,14 @@ IsDeclApplicableRequest::evaluate(Evaluator &evaluator,
246246
bool
247247
TypeRelationCheckRequest::evaluate(Evaluator &evaluator,
248248
TypeRelationCheckInput Owner) const {
249-
std::optional<constraints::ConstraintKind> CKind;
249+
using namespace constraints;
250+
std::optional<ConstraintKind> CKind;
250251
switch (Owner.Relation) {
251252
case TypeRelation::ConvertTo:
252-
CKind = constraints::ConstraintKind::Conversion;
253+
CKind = ConstraintKind::Conversion;
254+
break;
255+
case TypeRelation::SubtypeOf:
256+
CKind = ConstraintKind::Subtype;
253257
break;
254258
}
255259
assert(CKind.has_value());
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
// RUN: %empty-directory(%t)
2+
// RUN: %target-swift-ide-test(mock-sdk: %clang-importer-sdk) -batch-code-completion -source-filename %s -filecheck %raw-FileCheck -completion-output-dir %t
3+
4+
// REQUIRES: objc_interop
5+
6+
import Foundation
7+
8+
func foo(_ x: CGFloat) {}
9+
func foo(_ x: Double) {}
10+
11+
// Make sure we suggest completions for both CGFloat and Double.
12+
foo(.#^FOO^#)
13+
// FOO-DAG: Decl[Constructor]/CurrNominal/TypeRelation[Convertible]: init()[#CGFloat#]; name=init()
14+
// FOO-DAG: Decl[Constructor]/CurrNominal/IsSystem/TypeRelation[Convertible]: init()[#Double#]; name=init()

test/IDE/complete_rdar126168123.swift

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
// RUN: %empty-directory(%t)
2+
// RUN: %swift-ide-test -batch-code-completion -source-filename %s -filecheck %raw-FileCheck -completion-output-dir %t
3+
4+
// rdar://126168123
5+
6+
protocol MyProto {}
7+
protocol MyProto2 {}
8+
9+
struct MyStruct : MyProto {}
10+
11+
extension MyProto where Self == MyStruct {
12+
static var automatic: MyStruct { fatalError() }
13+
}
14+
15+
func use<T: MyProto>(_ someT: T) {}
16+
func use<T: MyProto2>(_ someT: T) {}
17+
18+
func test() {
19+
use(.#^COMPLETE^#)
20+
}
21+
22+
// COMPLETE: Decl[StaticVar]/CurrNominal/TypeRelation[Convertible]: automatic[#MyStruct#]; name=automatic
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
// RUN: %empty-directory(%t)
2+
// RUN: %swift-ide-test -batch-code-completion -source-filename %s -filecheck %raw-FileCheck -completion-output-dir %t
3+
4+
class C {
5+
static func cMethod() -> C {}
6+
}
7+
class D : C {
8+
static func dMethod() -> D {}
9+
}
10+
11+
func test1(_ x: C) {}
12+
func test1(_ x: D) {}
13+
14+
// We prefer the subtype here, so we show completions for D.
15+
test1(.#^TEST1^#)
16+
// TEST1-DAG: Decl[StaticMethod]/Super: cMethod()[#C#]; name=cMethod()
17+
// TEST1-DAG: Decl[StaticMethod]/CurrNominal/Flair[ExprSpecific]/TypeRelation[Convertible]: dMethod()[#D#]; name=dMethod()
18+
// TEST1-DAG: Decl[Constructor]/CurrNominal/TypeRelation[Convertible]: init()[#D#]; name=init()
19+
20+
func test2(_ x: C?) {}
21+
func test2(_ x: D?) {}
22+
23+
test2(.#^TEST2^#)
24+
// TEST2-DAG: Decl[StaticMethod]/Super: cMethod()[#C#]; name=cMethod()
25+
// TEST2-DAG: Decl[StaticMethod]/CurrNominal/Flair[ExprSpecific]/TypeRelation[Convertible]: dMethod()[#D#]; name=dMethod()
26+
// TEST2-DAG: Decl[Constructor]/CurrNominal/TypeRelation[Convertible]: init()[#D#]; name=init()
27+
// TEST2-DAG: Decl[EnumElement]/CurrNominal/IsSystem/TypeRelation[Convertible]: none[#Optional<D>#]; name=none
28+
// TEST2-DAG: Decl[EnumElement]/CurrNominal/IsSystem/TypeRelation[Convertible]: some({#D#})[#Optional<D>#]; name=some()
29+
30+
func test3(_ x: C?) {}
31+
func test3(_ x: D) {}
32+
33+
// We can still provide both C and D completions here.
34+
test3(.#^TEST3^#)
35+
// TEST3-DAG: Decl[StaticMethod]/CurrNominal/Flair[ExprSpecific]/TypeRelation[Convertible]: cMethod()[#C#]; name=cMethod()
36+
// TEST3-DAG: Decl[StaticMethod]/CurrNominal/Flair[ExprSpecific]/TypeRelation[Convertible]: dMethod()[#D#]; name=dMethod()
37+
// TEST3-DAG: Decl[Constructor]/CurrNominal/TypeRelation[Convertible]: init()[#D#]; name=init()
38+
// TEST3-DAG: Decl[EnumElement]/CurrNominal/IsSystem/TypeRelation[Convertible]: none[#Optional<C>#]; name=none
39+
// TEST3-DAG: Decl[EnumElement]/CurrNominal/IsSystem/TypeRelation[Convertible]: some({#C#})[#Optional<C>#]; name=some()
40+
41+
func test4(_ x: Int) {}
42+
func test4(_ x: AnyHashable) {}
43+
44+
// Make sure we show Int completions.
45+
test4(.#^TEST4^#)
46+
// TEST4: Decl[StaticVar]/Super/Flair[ExprSpecific]/IsSystem/TypeRelation[Convertible]: zero[#Int#]; name=zero
47+
48+
protocol P {}
49+
extension P {
50+
func pMethod() {}
51+
}
52+
struct S : P {
53+
func sMethod() {}
54+
}
55+
56+
func test5() -> any P {}
57+
func test5() -> S {}
58+
59+
test5().#^TEST5^#
60+
// TEST5-DAG: Decl[InstanceMethod]/CurrNominal: pMethod()[#Void#]; name=pMethod()
61+
// TEST5-DAG: Decl[InstanceMethod]/CurrNominal: sMethod()[#Void#]; name=sMethod()

0 commit comments

Comments
 (0)