[Completion] Better handle merging of lookup base types

For unresolved member completion, we were preferring
the more general type, when we ought to be preferring
the more specific type. Additionally, for both
unresolved member and postfix completion we were
opening archetypes, which doesn't work as expected
since we don't compare requirements. Factor out
the logic that deals with merging base types for
lookup, and have it prefer either the subtype, or
the optional type in the case of optional promotion.

rdar://126168123
This commit is contained in:
Hamish Knight
2024-06-07 10:04:31 +01:00
parent f17d3d35d6
commit 30af99e47e
11 changed files with 189 additions and 70 deletions

View File

@@ -66,13 +66,9 @@ class PostfixCompletionCallback : public TypeCheckCompletionCallback {
llvm::DenseMap<AbstractClosureExpr *, ActorIsolation> llvm::DenseMap<AbstractClosureExpr *, ActorIsolation>
ClosureActorIsolations; ClosureActorIsolations;
/// Checks whether this result has the same \c BaseTy and \c BaseDecl as /// Merge this result with \p Other, returning \c true if
/// \p Other and if the two can thus be merged to be one value lookup in /// successful, else \c false.
/// \c deliverResults. bool tryMerge(const Result &Other, DeclContext *DC);
bool canBeMergedWith(const Result &Other, DeclContext &DC) const;
/// Merge this result with \p Other. Assumes that they can be merged.
void merge(const Result &Other, DeclContext &DC);
}; };
CodeCompletionExpr *CompletionExpr; CodeCompletionExpr *CompletionExpr;

View File

@@ -33,13 +33,9 @@ class UnresolvedMemberTypeCheckCompletionCallback
/// functions is supported. /// functions is supported.
bool IsInAsyncContext; bool IsInAsyncContext;
/// Checks whether this result has the same \c BaseTy and \c BaseDecl as /// Attempts to merge this result with \p Other, returning \c true if
/// \p Other and if the two can thus be merged to be one value lookup in /// successful, else \c false.
/// \c deliverResults. bool tryMerge(const Result &Other, DeclContext *DC);
bool canBeMergedWith(const Result &Other, DeclContext &DC) const;
/// Merge this result with \p Other. Assumes that they can be merged.
void merge(const Result &Other, DeclContext &DC);
}; };
CodeCompletionExpr *CompletionExpr; CodeCompletionExpr *CompletionExpr;

View File

@@ -60,11 +60,20 @@ namespace swift {
/// Typecheck binding initializer at \p bindingIndex. /// Typecheck binding initializer at \p bindingIndex.
void typeCheckPatternBinding(PatternBindingDecl *PBD, unsigned bindingIndex); void typeCheckPatternBinding(PatternBindingDecl *PBD, unsigned bindingIndex);
/// Attempt to merge two types for the purposes of completion lookup. In
/// general this means preferring a subtype over a supertype, but can also e.g
/// prefer an optional over a non-optional. If the two types are incompatible,
/// null is returned.
Type tryMergeBaseTypeForCompletionLookup(Type ty1, Type ty2, DeclContext *dc);
/// Check if T1 is convertible to T2. /// Check if T1 is convertible to T2.
/// ///
/// \returns true on convertible, false on not. /// \returns true on convertible, false on not.
bool isConvertibleTo(Type T1, Type T2, bool openArchetypes, DeclContext &DC); bool isConvertibleTo(Type T1, Type T2, bool openArchetypes, DeclContext &DC);
/// Check whether \p T1 is a subtype of \p T2.
bool isSubtypeOf(Type T1, Type T2, DeclContext *DC);
void collectDefaultImplementationForProtocolMembers(ProtocolDecl *PD, void collectDefaultImplementationForProtocolMembers(ProtocolDecl *PD,
llvm::SmallDenseMap<ValueDecl*, ValueDecl*> &DefaultMap); llvm::SmallDenseMap<ValueDecl*, ValueDecl*> &DefaultMap);

View File

@@ -86,6 +86,7 @@ public:
//----------------------------------------------------------------------------// //----------------------------------------------------------------------------//
enum class TypeRelation: uint8_t { enum class TypeRelation: uint8_t {
ConvertTo, ConvertTo,
SubtypeOf
}; };
struct TypePair { struct TypePair {
@@ -153,6 +154,7 @@ struct TypeRelationCheckInput {
switch(owner.Relation) { switch(owner.Relation) {
#define CASE(NAME) case TypeRelation::NAME: out << #NAME << " "; break; #define CASE(NAME) case TypeRelation::NAME: out << #NAME << " "; break;
CASE(ConvertTo) CASE(ConvertTo)
CASE(SubtypeOf)
#undef CASE #undef CASE
} }
} }

View File

@@ -941,6 +941,45 @@ bool swift::isMemberDeclApplied(const DeclContext *DC, Type BaseTy,
IsDeclApplicableRequest(DeclApplicabilityOwner(DC, BaseTy, VD)), false); IsDeclApplicableRequest(DeclApplicabilityOwner(DC, BaseTy, VD)), false);
} }
Type swift::tryMergeBaseTypeForCompletionLookup(Type ty1, Type ty2,
DeclContext *dc) {
// Easy case, equivalent so just pick one.
if (ty1->isEqual(ty2))
return ty1;
// Check to see if one is an optional of another. In that case, prefer the
// optional since we can unwrap a single level when doing a lookup.
{
SmallVector<Type, 4> ty1Optionals;
SmallVector<Type, 4> ty2Optionals;
auto ty1Unwrapped = ty1->lookThroughAllOptionalTypes(ty1Optionals);
auto ty2Unwrapped = ty2->lookThroughAllOptionalTypes(ty2Optionals);
if (ty1Unwrapped->isEqual(ty2Unwrapped)) {
// We currently only unwrap a single level of optional, so if the
// difference is greater, don't merge.
if (ty1Optionals.size() == 1 && ty2Optionals.empty())
return ty1;
if (ty2Optionals.size() == 1 && ty1Optionals.empty())
return ty2;
}
// We don't want to consider subtyping for optional mismatches since
// optional promotion is modelled as a subtype, which isn't useful for us
// (i.e if we have T? and U, preferring U would miss members on T?).
if (ty1Optionals.size() != ty2Optionals.size())
return Type();
}
// In general we want to prefer a subtype over a supertype.
if (isSubtypeOf(ty1, ty2, dc))
return ty1;
if (isSubtypeOf(ty2, ty1, dc))
return ty2;
// Incomparable, return null.
return Type();
}
bool swift::isConvertibleTo(Type T1, Type T2, bool openArchetypes, bool swift::isConvertibleTo(Type T1, Type T2, bool openArchetypes,
DeclContext &DC) { DeclContext &DC) {
return evaluateOrDefault(DC.getASTContext().evaluator, return evaluateOrDefault(DC.getASTContext().evaluator,
@@ -948,6 +987,12 @@ bool swift::isConvertibleTo(Type T1, Type T2, bool openArchetypes,
TypeRelation::ConvertTo, openArchetypes)), false); TypeRelation::ConvertTo, openArchetypes)), false);
} }
bool swift::isSubtypeOf(Type T1, Type T2, DeclContext *DC) {
return evaluateOrDefault(DC->getASTContext().evaluator,
TypeRelationCheckRequest(TypeRelationCheckInput(DC, T1, T2,
TypeRelation::SubtypeOf, /*openArchetypes*/ false)), false);
}
Type swift::getRootTypeOfKeypathDynamicMember(SubscriptDecl *SD) { Type swift::getRootTypeOfKeypathDynamicMember(SubscriptDecl *SD) {
return evaluateOrDefault(SD->getASTContext().evaluator, return evaluateOrDefault(SD->getASTContext().evaluator,
RootTypeOfKeypathDynamicMemberRequest{SD}, Type()); RootTypeOfKeypathDynamicMemberRequest{SD}, Type());

View File

@@ -21,31 +21,20 @@ using namespace swift;
using namespace swift::constraints; using namespace swift::constraints;
using namespace swift::ide; using namespace swift::ide;
bool PostfixCompletionCallback::Result::canBeMergedWith(const Result &Other, bool PostfixCompletionCallback::Result::tryMerge(const Result &Other,
DeclContext &DC) const { DeclContext *DC) {
if (BaseDecl != Other.BaseDecl) { if (BaseDecl != Other.BaseDecl)
return false; return false;
}
if (!BaseTy->isEqual(Other.BaseTy) &&
!isConvertibleTo(BaseTy, Other.BaseTy, /*openArchetypes=*/true, DC) &&
!isConvertibleTo(Other.BaseTy, BaseTy, /*openArchetypes=*/true, DC)) {
return false;
}
return true;
}
void PostfixCompletionCallback::Result::merge(const Result &Other,
DeclContext &DC) {
assert(canBeMergedWith(Other, DC));
// These properties should match if we are talking about the same BaseDecl. // These properties should match if we are talking about the same BaseDecl.
assert(IsBaseDeclUnapplied == Other.IsBaseDeclUnapplied); assert(IsBaseDeclUnapplied == Other.IsBaseDeclUnapplied);
assert(BaseIsStaticMetaType == Other.BaseIsStaticMetaType); assert(BaseIsStaticMetaType == Other.BaseIsStaticMetaType);
if (!BaseTy->isEqual(Other.BaseTy) && auto baseTy = tryMergeBaseTypeForCompletionLookup(BaseTy, Other.BaseTy, DC);
isConvertibleTo(Other.BaseTy, BaseTy, /*openArchetypes=*/true, DC)) { if (!baseTy)
// Pick the more specific base type as it will produce more solutions. return false;
BaseTy = Other.BaseTy;
} BaseTy = baseTy;
// There could be multiple results that have different actor isolations if the // There could be multiple results that have different actor isolations if the
// closure is an argument to a function that has multiple overloads with // closure is an argument to a function that has multiple overloads with
@@ -66,18 +55,15 @@ void PostfixCompletionCallback::Result::merge(const Result &Other,
ExpectsNonVoid &= Other.ExpectsNonVoid; ExpectsNonVoid &= Other.ExpectsNonVoid;
IsImpliedResult |= Other.IsImpliedResult; IsImpliedResult |= Other.IsImpliedResult;
IsInAsyncContext |= Other.IsInAsyncContext; IsInAsyncContext |= Other.IsInAsyncContext;
return true;
} }
void PostfixCompletionCallback::addResult(const Result &Res) { void PostfixCompletionCallback::addResult(const Result &Res) {
auto ExistingRes = for (auto idx : indices(Results)) {
llvm::find_if(Results, [&Res, DC = DC](const Result &ExistingResult) { if (Results[idx].tryMerge(Res, DC))
return ExistingResult.canBeMergedWith(Res, *DC); return;
});
if (ExistingRes != Results.end()) {
ExistingRes->merge(Res, *DC);
} else {
Results.push_back(Res);
} }
Results.push_back(Res);
} }
void PostfixCompletionCallback::fallbackTypeCheck(DeclContext *DC) { void PostfixCompletionCallback::fallbackTypeCheck(DeclContext *DC) {

View File

@@ -21,43 +21,27 @@ using namespace swift;
using namespace swift::constraints; using namespace swift::constraints;
using namespace swift::ide; using namespace swift::ide;
bool UnresolvedMemberTypeCheckCompletionCallback::Result::canBeMergedWith( bool UnresolvedMemberTypeCheckCompletionCallback::Result::tryMerge(
const Result &Other, DeclContext &DC) const { const Result &Other, DeclContext *DC) {
if (!isConvertibleTo(ExpectedTy, Other.ExpectedTy, /*openArchetypes=*/true, auto expectedTy = tryMergeBaseTypeForCompletionLookup(ExpectedTy,
DC) && Other.ExpectedTy, DC);
!isConvertibleTo(Other.ExpectedTy, ExpectedTy, /*openArchetypes=*/true, if (!expectedTy)
DC)) {
return false; return false;
}
return true;
}
void UnresolvedMemberTypeCheckCompletionCallback::Result::merge( ExpectedTy = expectedTy;
const Result &Other, DeclContext &DC) {
assert(canBeMergedWith(Other, DC));
if (!ExpectedTy->isEqual(Other.ExpectedTy) &&
isConvertibleTo(ExpectedTy, Other.ExpectedTy, /*openArchetypes=*/true,
DC)) {
// ExpectedTy is more general than Other.ExpectedTy. Complete based on the
// more general type because it offers more completion options.
ExpectedTy = Other.ExpectedTy;
}
IsImpliedResult |= Other.IsImpliedResult; IsImpliedResult |= Other.IsImpliedResult;
IsInAsyncContext |= Other.IsInAsyncContext; IsInAsyncContext |= Other.IsInAsyncContext;
return true;
} }
void UnresolvedMemberTypeCheckCompletionCallback::addExprResult( void UnresolvedMemberTypeCheckCompletionCallback::addExprResult(
const Result &Res) { const Result &Res) {
auto ExistingRes = for (auto idx : indices(ExprResults)) {
llvm::find_if(ExprResults, [&Res, DC = DC](const Result &ExistingResult) { if (ExprResults[idx].tryMerge(Res, DC))
return ExistingResult.canBeMergedWith(Res, *DC); return;
});
if (ExistingRes != ExprResults.end()) {
ExistingRes->merge(Res, *DC);
} else {
ExprResults.push_back(Res);
} }
ExprResults.push_back(Res);
} }
void UnresolvedMemberTypeCheckCompletionCallback::sawSolutionImpl( void UnresolvedMemberTypeCheckCompletionCallback::sawSolutionImpl(

View File

@@ -246,10 +246,14 @@ IsDeclApplicableRequest::evaluate(Evaluator &evaluator,
bool bool
TypeRelationCheckRequest::evaluate(Evaluator &evaluator, TypeRelationCheckRequest::evaluate(Evaluator &evaluator,
TypeRelationCheckInput Owner) const { TypeRelationCheckInput Owner) const {
std::optional<constraints::ConstraintKind> CKind; using namespace constraints;
std::optional<ConstraintKind> CKind;
switch (Owner.Relation) { switch (Owner.Relation) {
case TypeRelation::ConvertTo: case TypeRelation::ConvertTo:
CKind = constraints::ConstraintKind::Conversion; CKind = ConstraintKind::Conversion;
break;
case TypeRelation::SubtypeOf:
CKind = ConstraintKind::Subtype;
break; break;
} }
assert(CKind.has_value()); assert(CKind.has_value());

View File

@@ -0,0 +1,14 @@
// RUN: %empty-directory(%t)
// RUN: %target-swift-ide-test(mock-sdk: %clang-importer-sdk) -batch-code-completion -source-filename %s -filecheck %raw-FileCheck -completion-output-dir %t
// REQUIRES: objc_interop
import Foundation
func foo(_ x: CGFloat) {}
func foo(_ x: Double) {}
// Make sure we suggest completions for both CGFloat and Double.
foo(.#^FOO^#)
// FOO-DAG: Decl[Constructor]/CurrNominal/TypeRelation[Convertible]: init()[#CGFloat#]; name=init()
// FOO-DAG: Decl[Constructor]/CurrNominal/IsSystem/TypeRelation[Convertible]: init()[#Double#]; name=init()

View File

@@ -0,0 +1,22 @@
// RUN: %empty-directory(%t)
// RUN: %swift-ide-test -batch-code-completion -source-filename %s -filecheck %raw-FileCheck -completion-output-dir %t
// rdar://126168123
protocol MyProto {}
protocol MyProto2 {}
struct MyStruct : MyProto {}
extension MyProto where Self == MyStruct {
static var automatic: MyStruct { fatalError() }
}
func use<T: MyProto>(_ someT: T) {}
func use<T: MyProto2>(_ someT: T) {}
func test() {
use(.#^COMPLETE^#)
}
// COMPLETE: Decl[StaticVar]/CurrNominal/TypeRelation[Convertible]: automatic[#MyStruct#]; name=automatic

View File

@@ -0,0 +1,61 @@
// RUN: %empty-directory(%t)
// RUN: %swift-ide-test -batch-code-completion -source-filename %s -filecheck %raw-FileCheck -completion-output-dir %t
class C {
static func cMethod() -> C {}
}
class D : C {
static func dMethod() -> D {}
}
func test1(_ x: C) {}
func test1(_ x: D) {}
// We prefer the subtype here, so we show completions for D.
test1(.#^TEST1^#)
// TEST1-DAG: Decl[StaticMethod]/Super: cMethod()[#C#]; name=cMethod()
// TEST1-DAG: Decl[StaticMethod]/CurrNominal/Flair[ExprSpecific]/TypeRelation[Convertible]: dMethod()[#D#]; name=dMethod()
// TEST1-DAG: Decl[Constructor]/CurrNominal/TypeRelation[Convertible]: init()[#D#]; name=init()
func test2(_ x: C?) {}
func test2(_ x: D?) {}
test2(.#^TEST2^#)
// TEST2-DAG: Decl[StaticMethod]/Super: cMethod()[#C#]; name=cMethod()
// TEST2-DAG: Decl[StaticMethod]/CurrNominal/Flair[ExprSpecific]/TypeRelation[Convertible]: dMethod()[#D#]; name=dMethod()
// TEST2-DAG: Decl[Constructor]/CurrNominal/TypeRelation[Convertible]: init()[#D#]; name=init()
// TEST2-DAG: Decl[EnumElement]/CurrNominal/IsSystem/TypeRelation[Convertible]: none[#Optional<D>#]; name=none
// TEST2-DAG: Decl[EnumElement]/CurrNominal/IsSystem/TypeRelation[Convertible]: some({#D#})[#Optional<D>#]; name=some()
func test3(_ x: C?) {}
func test3(_ x: D) {}
// We can still provide both C and D completions here.
test3(.#^TEST3^#)
// TEST3-DAG: Decl[StaticMethod]/CurrNominal/Flair[ExprSpecific]/TypeRelation[Convertible]: cMethod()[#C#]; name=cMethod()
// TEST3-DAG: Decl[StaticMethod]/CurrNominal/Flair[ExprSpecific]/TypeRelation[Convertible]: dMethod()[#D#]; name=dMethod()
// TEST3-DAG: Decl[Constructor]/CurrNominal/TypeRelation[Convertible]: init()[#D#]; name=init()
// TEST3-DAG: Decl[EnumElement]/CurrNominal/IsSystem/TypeRelation[Convertible]: none[#Optional<C>#]; name=none
// TEST3-DAG: Decl[EnumElement]/CurrNominal/IsSystem/TypeRelation[Convertible]: some({#C#})[#Optional<C>#]; name=some()
func test4(_ x: Int) {}
func test4(_ x: AnyHashable) {}
// Make sure we show Int completions.
test4(.#^TEST4^#)
// TEST4: Decl[StaticVar]/Super/Flair[ExprSpecific]/IsSystem/TypeRelation[Convertible]: zero[#Int#]; name=zero
protocol P {}
extension P {
func pMethod() {}
}
struct S : P {
func sMethod() {}
}
func test5() -> any P {}
func test5() -> S {}
test5().#^TEST5^#
// TEST5-DAG: Decl[InstanceMethod]/CurrNominal: pMethod()[#Void#]; name=pMethod()
// TEST5-DAG: Decl[InstanceMethod]/CurrNominal: sMethod()[#Void#]; name=sMethod()