Skip to content

Commit a8e5162

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 a8e5162

9 files changed

+86
-23
lines changed

lib/Sema/CSOptimizer.cpp

Lines changed: 57 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -695,6 +695,29 @@ static void determineBestChoicesInContext(
695695
fromInitializerCall(fromInitializerCall) {}
696696
};
697697

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

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 =
845+
// Determine whether all of the argument candidates are speculative (i.e.
846+
// literals). This information is going to be used later on when we need to
847+
// decide how to score a matching choice.
848+
bool onlySpeculativeCandidates =
826849
hasArgumentCandidates &&
827850
llvm::none_of(
828851
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-
});
852+
return anyNonSpeculativeCandidates(argumentCandidates[argIdx]);
833853
});
834854

835855
// Match arguments to the given overload choice.
@@ -1237,7 +1257,7 @@ static void determineBestChoicesInContext(
12371257
// are literals and there are no usable contextual result
12381258
// types that could help narrow favored choices.
12391259
bool favorExactMatchesOnly =
1240-
onlyLiteralCandidates &&
1260+
onlySpeculativeCandidates &&
12411261
(!canUseContextualResultTypes() || resultTypes.empty());
12421262

12431263
// This is important for SIMD operators in particular because
@@ -1439,6 +1459,27 @@ static void determineBestChoicesInContext(
14391459
info.FavoredChoices.push_back(choice.first);
14401460
}
14411461

1462+
// Reset operator disjunction score but keep favoring
1463+
// choices only available candidates where speculative.
1464+
// This helps to prevent over-eager selection of the
1465+
// operators over unsupported non-operator declarations.
1466+
if (isOperator && onlySpeculativeCandidates) {
1467+
if (cs.isDebugMode()) {
1468+
PrintOptions PO;
1469+
PO.PrintTypesForDebugging = true;
1470+
1471+
llvm::errs().indent(cs.solverState->getCurrentIndent())
1472+
<< "<<< Disjunction "
1473+
<< disjunction->getNestedConstraints()[0]
1474+
->getFirstType()
1475+
->getString(PO)
1476+
<< " score " << bestScore
1477+
<< " was reset due to having only speculative candidates\n";
1478+
}
1479+
1480+
info.Score = 0;
1481+
}
1482+
14421483
recordResult(disjunction, std::move(info));
14431484
}
14441485

@@ -1604,8 +1645,13 @@ ConstraintSystem::selectDisjunction() {
16041645
return *firstScore > *secondScore;
16051646
}
16061647

1607-
unsigned numFirstFavored = firstFavoredChoices.size();
1608-
unsigned numSecondFavored = secondFavoredChoices.size();
1648+
// Use favored choices only if disjunction score is higher
1649+
// than zero. This means that we can maintain favoring
1650+
// choices without impacting selection decisions.
1651+
unsigned numFirstFavored =
1652+
firstScore.value_or(0) ? firstFavoredChoices.size() : 0;
1653+
unsigned numSecondFavored =
1654+
secondScore.value_or(0) ? secondFavoredChoices.size() : 0;
16091655

16101656
if (numFirstFavored == numSecondFavored) {
16111657
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)