Skip to content

Commit bcc749f

Browse files
committed
[CSOptimizer] Reset the overall score of operator disjunctions that is based speculation
New ranking + selection algorithm suffered from over-eagerly selecting operator disjunctions vs. unsupported non-operator ones even if the ranking was based purely on literal candidates. This change introduces a notion of a speculative candidate - one which has a type inferred from a literal or an initializer call that has failable overloads and/or implicit conversions (i.e. Double/CGFloat). `determineBestChoicesInContext` would reset the score of an operator disjunction which was computed based on speculative candidates alone but would preserve favoring information. This way selection algorithm would not be skewed towards operators and at the same time if there is no no choice by to select one we'd still have favoring information available which is important for operator chains that consist purely of literals.
1 parent df4ae0a commit bcc749f

File tree

8 files changed

+113
-24
lines changed

8 files changed

+113
-24
lines changed

lib/Sema/CSOptimizer.cpp

Lines changed: 84 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -108,9 +108,12 @@ static bool isSupportedSpecialConstructor(ConstructorDecl *ctor) {
108108
return false;
109109
}
110110

111-
static bool isStandardComparisonOperator(ValueDecl *decl) {
112-
return decl->isOperator() &&
113-
decl->getBaseIdentifier().isStandardComparisonOperator();
111+
static bool isStandardComparisonOperator(Constraint *disjunction) {
112+
auto *choice = disjunction->getNestedConstraints()[0];
113+
if (auto *decl = getOverloadChoiceDecl(choice))
114+
return decl->isOperator() &&
115+
decl->getBaseIdentifier().isStandardComparisonOperator();
116+
return false;
114117
}
115118

116119
static bool isOperatorNamed(Constraint *disjunction, StringRef name) {
@@ -694,6 +697,38 @@ static void determineBestChoicesInContext(
694697
fromInitializerCall(fromInitializerCall) {}
695698
};
696699

700+
// Determine whether there are any non-speculative choices
701+
// in the given set of candidates. Speculative choices are
702+
// literals or types inferred from initializer calls.
703+
auto anyNonSpeculativeCandidates =
704+
[&](ArrayRef<ArgumentCandidate> candidates) {
705+
// If there is only one (non-CGFloat) candidate inferred from
706+
// an initializer call we don't consider this a speculation.
707+
//
708+
// CGFloat inference is always speculative because of the
709+
// implicit conversion between Double and CGFloat.
710+
if (llvm::count_if(candidates, [&](const auto &candidate) {
711+
return candidate.fromInitializerCall &&
712+
!candidate.type->isCGFloat();
713+
}) == 1)
714+
return true;
715+
716+
// If there are no non-literal and non-initializer-inferred types
717+
// in the list, consider this is a speculation.
718+
return llvm::any_of(candidates, [&](const auto &candidate) {
719+
return !candidate.fromLiteral && !candidate.fromInitializerCall;
720+
});
721+
};
722+
723+
auto anyNonSpeculativeResultTypes = [](ArrayRef<Type> results) {
724+
return llvm::any_of(results, [](Type resultTy) {
725+
// Double and CGFloat are considered speculative because
726+
// there exists an implicit conversion between them and
727+
// preference is based on score impact in the overall solution.
728+
return !(resultTy->isDouble() || resultTy->isCGFloat());
729+
});
730+
};
731+
697732
SmallVector<SmallVector<ArgumentCandidate, 2>, 2>
698733
argumentCandidates;
699734
argumentCandidates.resize(argFuncType->getNumParams());
@@ -818,19 +853,19 @@ static void determineBestChoicesInContext(
818853
resultTypes.push_back(resultType);
819854
}
820855

821-
// Determine whether all of the argument candidates are inferred from literals.
822-
// This information is going to be used later on when we need to decide how to
823-
// score a matching choice.
824-
bool onlyLiteralCandidates =
856+
// Determine whether all of the argument candidates are speculative (i.e.
857+
// literals). This information is going to be used later on when we need to
858+
// decide how to score a matching choice.
859+
bool onlySpeculativeArgumentCandidates =
825860
hasArgumentCandidates &&
826861
llvm::none_of(
827862
indices(argFuncType->getParams()), [&](const unsigned argIdx) {
828-
auto &candidates = argumentCandidates[argIdx];
829-
return llvm::any_of(candidates, [&](const auto &candidate) {
830-
return !candidate.fromLiteral;
831-
});
863+
return anyNonSpeculativeCandidates(argumentCandidates[argIdx]);
832864
});
833865

866+
bool canUseContextualResultTypes =
867+
isOperator && !isStandardComparisonOperator(disjunction);
868+
834869
// Match arguments to the given overload choice.
835870
auto matchArguments = [&](OverloadChoice choice, FunctionType *overloadType)
836871
-> std::optional<MatchCallArgumentResult> {
@@ -1229,16 +1264,12 @@ static void determineBestChoicesInContext(
12291264
if (!matchings)
12301265
return;
12311266

1232-
auto canUseContextualResultTypes = [&decl]() {
1233-
return decl->isOperator() && !isStandardComparisonOperator(decl);
1234-
};
1235-
12361267
// Require exact matches only if all of the arguments
12371268
// are literals and there are no usable contextual result
12381269
// types that could help narrow favored choices.
12391270
bool favorExactMatchesOnly =
1240-
onlyLiteralCandidates &&
1241-
(!canUseContextualResultTypes() || resultTypes.empty());
1271+
onlySpeculativeArgumentCandidates &&
1272+
(!canUseContextualResultTypes || resultTypes.empty());
12421273

12431274
// This is important for SIMD operators in particular because
12441275
// a lot of their overloads have same-type requires to a concrete
@@ -1384,7 +1415,7 @@ static void determineBestChoicesInContext(
13841415
// because regular functions/methods/subscripts and
13851416
// especially initializers could end up with a lot of
13861417
// favored overloads because on the result type alone.
1387-
if (canUseContextualResultTypes() &&
1418+
if (canUseContextualResultTypes &&
13881419
(score > 0 || !hasArgumentCandidates)) {
13891420
if (llvm::any_of(resultTypes, [&](const Type candidateResultTy) {
13901421
return scoreCandidateMatch(genericSig, decl,
@@ -1439,6 +1470,34 @@ static void determineBestChoicesInContext(
14391470
info.FavoredChoices.push_back(choice.first);
14401471
}
14411472

1473+
// Reset operator disjunction score but keep favoring
1474+
// choices only available candidates where speculative
1475+
// with no contextual information available, standard
1476+
// comparison operators are a special cases because
1477+
// their return type is fixed to `Bool` unlike that of
1478+
// bitwise, arithmetic, and other operators.
1479+
//
1480+
// This helps to prevent over-eager selection of the
1481+
// operators over unsupported non-operator declarations.
1482+
if (isOperator && onlySpeculativeArgumentCandidates &&
1483+
(!canUseContextualResultTypes ||
1484+
!anyNonSpeculativeResultTypes(resultTypes))) {
1485+
if (cs.isDebugMode()) {
1486+
PrintOptions PO;
1487+
PO.PrintTypesForDebugging = true;
1488+
1489+
llvm::errs().indent(cs.solverState->getCurrentIndent())
1490+
<< "<<< Disjunction "
1491+
<< disjunction->getNestedConstraints()[0]
1492+
->getFirstType()
1493+
->getString(PO)
1494+
<< " score " << bestScore
1495+
<< " was reset due to having only speculative candidates\n";
1496+
}
1497+
1498+
info.Score = 0;
1499+
}
1500+
14421501
recordResult(disjunction, std::move(info));
14431502
}
14441503

@@ -1610,8 +1669,13 @@ ConstraintSystem::selectDisjunction() {
16101669
return *firstScore > *secondScore;
16111670
}
16121671

1613-
unsigned numFirstFavored = firstFavoredChoices.size();
1614-
unsigned numSecondFavored = secondFavoredChoices.size();
1672+
// Use favored choices only if disjunction score is higher
1673+
// than zero. This means that we can maintain favoring
1674+
// choices without impacting selection decisions.
1675+
unsigned numFirstFavored =
1676+
firstScore.value_or(0) ? firstFavoredChoices.size() : 0;
1677+
unsigned numSecondFavored =
1678+
secondScore.value_or(0) ? secondFavoredChoices.size() : 0;
16151679

16161680
if (numFirstFavored == numSecondFavored) {
16171681
if (firstActive != secondActive)
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
// RUN: %target-swift-frontend -typecheck %s -solver-scope-threshold=10000
2+
// REQUIRES: tools-release,no_asan
3+
4+
// Selecting operators from the closure before arguments to `zip` makes this "too complex"
5+
func compute(_ ids: [UInt64]) {
6+
let _ = zip(ids[ids.indices.dropLast()], ids[ids.indices.dropFirst()]).map { pair in
7+
((pair.0 % 2 == 0) && (pair.1 % 2 == 1)) ? UInt64(pair.1 - pair.0) : 42
8+
}
9+
}

validation-test/Sema/type_checker_perf/fast/rdar47492691.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,7 @@ import simd
77
func test(foo: CGFloat, bar: CGFloat) {
88
_ = CGRect(x: 0.0 + 1.0, y: 0.0 + foo, width: 3.0 - 1 - 1 - 1.0, height: bar)
99
}
10+
11+
func test_with_generic_func_and_literals(bounds: CGRect) {
12+
_ = CGRect(x: 0, y: 0, width: 1, height: bounds.height - 2 + bounds.height / 2 + max(bounds.height / 2, bounds.height / 2))
13+
}
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
1-
// RUN: %target-swift-frontend -typecheck %s -solver-scope-threshold=2000
1+
// RUN: %target-swift-frontend -typecheck %s -solver-scope-threshold=4000
22

33
func f896() { let _ = ((0 >> ((0 >> 0) + ((0 / 0) & 0))) >> (0 << ((0 << 0) >> (0 << (0 << 0))))) }

validation-test/Sema/type_checker_perf/fast/swift_package_index_1.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// RUN: %target-typecheck-verify-swift -solver-scope-threshold=1000
1+
// RUN: %target-swift-frontend -typecheck %s -solver-scope-threshold=700
22
// REQUIRES: tools-release,no_asan
33

44
public class Cookie {
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
// RUN: %target-typecheck-verify-swift -solver-scope-threshold=5000
2+
// REQUIRES: tools-release,no_asan
3+
// REQUIRES: OS=macosx
4+
5+
// FIXME: Array literals are considered "speculative" bindings at the moment but they cannot actually
6+
// assume different types unlike integer and floating-pointer literals.
7+
func f(n: Int, a: [Int]) {
8+
// expected-error@+1 {{the compiler is unable to type-check this expression in reasonable time}}
9+
let _ = [(0 ..< n + a.count).map { Int8($0) }] +
10+
[(0 ..< n + a.count).map { Int8($0) }.reversed()] // Ok
11+
}

validation-test/Sema/type_checker_perf/fast/rdar23682605.swift renamed to validation-test/Sema/type_checker_perf/slow/rdar23682605.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ func memoize<T: Hashable, U>( body: @escaping ((T)->U, T)->U ) -> (T)->U {
1414
}
1515

1616
let fibonacci = memoize {
17+
// expected-error@-1 {{reasonable time}}
1718
fibonacci, n in
1819
n < 2 ? Double(n) : fibonacci(n - 1) + fibonacci(n - 2)
1920
}

validation-test/Sema/type_checker_perf/fast/swift_package_index_3.swift renamed to validation-test/Sema/type_checker_perf/slow/swift_package_index_3.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// RUN: %target-typecheck-verify-swift -solver-scope-threshold=50000
1+
// RUN: %target-typecheck-verify-swift %s -solver-scope-threshold=50000
22
// REQUIRES: tools-release,no_asan
33

44
extension String {
@@ -9,7 +9,7 @@ extension String {
99
func getProperties(
1010
from ics: String
1111
) -> [(name: String, value: String)] {
12-
return ics
12+
return ics // expected-error {{the compiler is unable to type-check this expression in reasonable time}}
1313
.replacingOccurrences(of: "\r\n ", with: "")
1414
.components(separatedBy: "\r\n")
1515
.map { $0.split(separator: ":", maxSplits: 1, omittingEmptySubsequences: true) }

0 commit comments

Comments
 (0)