Skip to content

Commit 705f232

Browse files
committed
[CSOptimizer] Reset the overall score of operator disjunctions that is based on speculative bindings
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 29c259f commit 705f232

9 files changed

+103
-32
lines changed

lib/Sema/CSOptimizer.cpp

Lines changed: 74 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) {
@@ -695,6 +698,29 @@ static void determineBestChoicesInContext(
695698
fromInitializerCall(fromInitializerCall) {}
696699
};
697700

701+
// Determine whether there are any non-speculative choices
702+
// in the given set of candidates. Speculative choices are
703+
// literals or types inferred from initializer calls.
704+
auto anyNonSpeculativeCandidates =
705+
[&](ArrayRef<ArgumentCandidate> candidates) {
706+
// If there is only one (non-CGFloat) candidate inferred from
707+
// an initializer call we don't consider this a speculation.
708+
//
709+
// CGFloat inference is always speculative because of the
710+
// implicit conversion between Double and CGFloat.
711+
if (llvm::count_if(candidates, [&](const auto &candidate) {
712+
return candidate.fromInitializerCall &&
713+
!candidate.type->isCGFloat();
714+
}) == 1)
715+
return true;
716+
717+
// If there are no non-literal and non-initializer-inferred types
718+
// in the list, consider this is a speculation.
719+
return llvm::any_of(candidates, [&](const auto &candidate) {
720+
return !candidate.fromLiteral && !candidate.fromInitializerCall;
721+
});
722+
};
723+
698724
SmallVector<SmallVector<ArgumentCandidate, 2>, 2>
699725
argumentCandidates;
700726
argumentCandidates.resize(argFuncType->getNumParams());
@@ -819,19 +845,19 @@ static void determineBestChoicesInContext(
819845
resultTypes.push_back(resultType);
820846
}
821847

822-
// Determine whether all of the argument candidates are inferred from literals.
823-
// This information is going to be used later on when we need to decide how to
824-
// score a matching choice.
825-
bool onlyLiteralCandidates =
848+
// Determine whether all of the argument candidates are speculative (i.e.
849+
// literals). This information is going to be used later on when we need to
850+
// decide how to score a matching choice.
851+
bool onlySpeculativeArgumentCandidates =
826852
hasArgumentCandidates &&
827853
llvm::none_of(
828854
indices(argFuncType->getParams()), [&](const unsigned argIdx) {
829-
auto &candidates = argumentCandidates[argIdx];
830-
return llvm::any_of(candidates, [&](const auto &candidate) {
831-
return !candidate.fromLiteral;
832-
});
855+
return anyNonSpeculativeCandidates(argumentCandidates[argIdx]);
833856
});
834857

858+
bool canUseContextualResultTypes =
859+
isOperator && !isStandardComparisonOperator(disjunction);
860+
835861
// Match arguments to the given overload choice.
836862
auto matchArguments = [&](OverloadChoice choice, FunctionType *overloadType)
837863
-> std::optional<MatchCallArgumentResult> {
@@ -1229,16 +1255,12 @@ static void determineBestChoicesInContext(
12291255
if (!matchings)
12301256
return;
12311257

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

12431265
// This is important for SIMD operators in particular because
12441266
// a lot of their overloads have same-type requires to a concrete
@@ -1384,7 +1406,7 @@ static void determineBestChoicesInContext(
13841406
// because regular functions/methods/subscripts and
13851407
// especially initializers could end up with a lot of
13861408
// favored overloads because on the result type alone.
1387-
if (canUseContextualResultTypes() &&
1409+
if (canUseContextualResultTypes &&
13881410
(score > 0 || !hasArgumentCandidates)) {
13891411
if (llvm::any_of(resultTypes, [&](const Type candidateResultTy) {
13901412
return scoreCandidateMatch(genericSig, decl,
@@ -1439,6 +1461,33 @@ static void determineBestChoicesInContext(
14391461
info.FavoredChoices.push_back(choice.first);
14401462
}
14411463

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

@@ -1604,8 +1653,13 @@ ConstraintSystem::selectDisjunction() {
16041653
return *firstScore > *secondScore;
16051654
}
16061655

1607-
unsigned numFirstFavored = firstFavoredChoices.size();
1608-
unsigned numSecondFavored = secondFavoredChoices.size();
1656+
// Use favored choices only if disjunction score is higher
1657+
// than zero. This means that we can maintain favoring
1658+
// choices without impacting selection decisions.
1659+
unsigned numFirstFavored =
1660+
firstScore.value_or(0) ? firstFavoredChoices.size() : 0;
1661+
unsigned numSecondFavored =
1662+
secondScore.value_or(0) ? secondFavoredChoices.size() : 0;
16091663

16101664
if (numFirstFavored == numSecondFavored) {
16111665
if (firstActive != secondActive)

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

Lines changed: 0 additions & 8 deletions
This file was deleted.
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-swift-frontend -typecheck %s -solver-scope-threshold=500
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-swift-frontend -typecheck %s -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)