Skip to content

Commit f785515

Browse files
committed
[CSOptimizer] Introduce a way to preference disjunctions before scores are considered
Some of the disjunctions are not supported by the optimizers but could still be a better choice than an operator. Using a non-score based preference mechanism first allows us to make sure that operator disjunctions are not selected too eagerly in some situations when i.e. a member (supported or not) could be a better choice. `isPreferable` currently targets only operators in result builder contexts but it could be expanded to more uses in the future.
1 parent a8e5162 commit f785515

File tree

4 files changed

+227
-73
lines changed

4 files changed

+227
-73
lines changed

lib/Sema/CSOptimizer.cpp

+121-72
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
#include "swift/AST/ConformanceLookup.h"
1919
#include "swift/AST/ExistentialLayout.h"
2020
#include "swift/AST/GenericSignature.h"
21+
#include "swift/Basic/Defer.h"
2122
#include "swift/Basic/OptionSet.h"
2223
#include "swift/Sema/ConstraintGraph.h"
2324
#include "swift/Sema/ConstraintSystem.h"
@@ -49,6 +50,103 @@ struct DisjunctionInfo {
4950
: Score(score), FavoredChoices(favoredChoices) {}
5051
};
5152

53+
static DeclContext *getDisjunctionDC(Constraint *disjunction) {
54+
auto *choice = disjunction->getNestedConstraints()[0];
55+
switch (choice->getKind()) {
56+
case ConstraintKind::BindOverload:
57+
return choice->getOverloadUseDC();
58+
case ConstraintKind::ValueMember:
59+
case ConstraintKind::UnresolvedValueMember:
60+
case ConstraintKind::ValueWitness:
61+
return choice->getMemberUseDC();
62+
default:
63+
return nullptr;
64+
}
65+
}
66+
67+
/// Determine whether the given disjunction appears in a context
68+
/// transformed by a result builder.
69+
static bool isInResultBuilderContext(ConstraintSystem &cs,
70+
Constraint *disjunction) {
71+
auto *DC = getDisjunctionDC(disjunction);
72+
if (!DC)
73+
return false;
74+
75+
do {
76+
auto fnContext = AnyFunctionRef::fromDeclContext(DC);
77+
if (!fnContext)
78+
return false;
79+
80+
if (cs.getAppliedResultBuilderTransform(*fnContext))
81+
return true;
82+
83+
} while ((DC = DC->getParent()));
84+
85+
return false;
86+
}
87+
88+
/// If the given operator disjunction appears in some position
89+
// inside of a not yet resolved call i.e. `a.b(1 + c(4) - 1)`
90+
// both `+` and `-` are "in" argument context of `b`.
91+
static bool isOperatorPassedToUnresolvedCall(ConstraintSystem &cs,
92+
Constraint *disjunction) {
93+
ASSERT(isOperatorDisjunction(disjunction));
94+
95+
auto *curr = castToExpr(disjunction->getLocator()->getAnchor());
96+
while (auto *parent = cs.getParentExpr(curr)) {
97+
SWIFT_DEFER { curr = parent; };
98+
99+
switch (parent->getKind()) {
100+
case ExprKind::OptionalEvaluation:
101+
case ExprKind::Paren:
102+
case ExprKind::Binary:
103+
case ExprKind::PrefixUnary:
104+
case ExprKind::PostfixUnary:
105+
continue;
106+
107+
// a.b(<<cond>> ? <<operator chain>> : <<...>>)
108+
case ExprKind::Ternary: {
109+
auto *T = cast<TernaryExpr>(parent);
110+
// If the operator is located in the condition it's
111+
// not tied to the context.
112+
if (T->getCondExpr() == curr)
113+
return false;
114+
115+
// But the branches are connected to the context.
116+
continue;
117+
}
118+
119+
// Handles `a(<<operator chain>>), `a[<<operator chain>>]`,
120+
// `.a(<<operator chain>>)` etc.
121+
case ExprKind::Call: {
122+
auto *call = cast<CallExpr>(parent);
123+
124+
// Type(...)
125+
if (isa<TypeExpr>(call->getFn())) {
126+
auto *ctorLoc = cs.getConstraintLocator(
127+
call, {LocatorPathElt::ApplyFunction(),
128+
LocatorPathElt::ConstructorMember()});
129+
return !cs.findSelectedOverloadFor(ctorLoc);
130+
}
131+
132+
// Ignore injected result builder methods like `buildExpression`
133+
// and `buildBlock`.
134+
if (auto *UDE = dyn_cast<UnresolvedDotExpr>(call->getFn())) {
135+
if (isResultBuilderMethodReference(cs.getASTContext(), UDE))
136+
return false;
137+
}
138+
139+
return !cs.findSelectedOverloadFor(call->getFn());
140+
}
141+
142+
default:
143+
return false;
144+
}
145+
}
146+
147+
return false;
148+
}
149+
52150
// TODO: both `isIntegerType` and `isFloatType` should be available on Type
53151
// as `isStdlib{Integer, Float}Type`.
54152

@@ -1534,77 +1632,30 @@ static void determineBestChoicesInContext(
15341632
}
15351633
}
15361634

1537-
/// Prioritize `build{Block, Expression, ...}` and any chained
1538-
/// members that are connected to individual builder elements
1539-
/// i.e. `ForEach(...) { ... }.padding(...)`, once `ForEach`
1540-
/// is resolved, `padding` should be prioritized because its
1541-
/// requirements can help prune the solution space before the
1542-
/// body is checked.
1543-
static Constraint *
1544-
selectDisjunctionInResultBuilderContext(ConstraintSystem &cs,
1545-
ArrayRef<Constraint *> disjunctions) {
1546-
auto context = AnyFunctionRef::fromDeclContext(cs.DC);
1547-
if (!context)
1548-
return nullptr;
1549-
1550-
if (!cs.getAppliedResultBuilderTransform(context.value()))
1551-
return nullptr;
1552-
1553-
std::pair<Constraint *, unsigned> best{nullptr, 0};
1554-
for (auto *disjunction : disjunctions) {
1555-
auto *member =
1556-
getAsExpr<UnresolvedDotExpr>(disjunction->getLocator()->getAnchor());
1557-
if (!member)
1558-
continue;
1559-
1560-
// Attempt `build{Block, Expression, ...} first because they
1561-
// provide contextual information for the inner calls.
1562-
if (isResultBuilderMethodReference(cs.getASTContext(), member))
1563-
return disjunction;
1564-
1565-
Expr *curr = member;
1566-
bool disqualified = false;
1567-
// Walk up the parent expression chain and check whether this
1568-
// disjunction represents one of the members in a chain that
1569-
// leads up to `buildExpression` (if defined by the builder)
1570-
// or to a pattern binding for `$__builderN` (the walk won't
1571-
// find any argument position locations in that case).
1572-
while (auto parent = cs.getParentExpr(curr)) {
1573-
if (!(isExpr<CallExpr>(parent) || isExpr<UnresolvedDotExpr>(parent))) {
1574-
disqualified = true;
1575-
break;
1576-
}
1577-
1578-
if (auto *call = getAsExpr<CallExpr>(parent)) {
1579-
// The current parent appears in an argument position.
1580-
if (call->getFn() != curr) {
1581-
// Allow expressions that appear in a argument position to
1582-
// `build{Expression, Block, ...} methods.
1583-
if (auto *UDE = getAsExpr<UnresolvedDotExpr>(call->getFn())) {
1584-
disqualified =
1585-
!isResultBuilderMethodReference(cs.getASTContext(), UDE);
1586-
} else {
1587-
disqualified = true;
1588-
}
1589-
}
1590-
}
1591-
1592-
if (disqualified)
1593-
break;
1594-
1595-
curr = parent;
1596-
}
1597-
1598-
if (disqualified)
1599-
continue;
1635+
static std::optional<bool> isPreferable(ConstraintSystem &cs,
1636+
Constraint *disjunctionA,
1637+
Constraint *disjunctionB) {
1638+
// Consider only operator vs. non-operator situations.
1639+
if (isOperatorDisjunction(disjunctionA) ==
1640+
isOperatorDisjunction(disjunctionB))
1641+
return std::nullopt;
16001642

1601-
if (auto depth = cs.getExprDepth(member)) {
1602-
if (!best.first || best.second > depth)
1603-
best = std::make_pair(disjunction, depth.value());
1643+
// Prevent operator selection if its passed as an argument
1644+
// to not-yet resolved call. This helps to make sure that
1645+
// in result builder context chained members and other
1646+
// non-operator disjunctions are always selected first,
1647+
// because they provide the context and help to prune the system.
1648+
if (isInResultBuilderContext(cs, disjunctionA)) {
1649+
if (isOperatorDisjunction(disjunctionA)) {
1650+
if (isOperatorPassedToUnresolvedCall(cs, disjunctionA))
1651+
return false;
1652+
} else {
1653+
if (isOperatorPassedToUnresolvedCall(cs, disjunctionB))
1654+
return true;
16041655
}
16051656
}
16061657

1607-
return best.first;
1658+
return std::nullopt;
16081659
}
16091660

16101661
std::optional<std::pair<Constraint *, llvm::TinyPtrVector<Constraint *>>>
@@ -1618,11 +1669,6 @@ ConstraintSystem::selectDisjunction() {
16181669
llvm::DenseMap<Constraint *, DisjunctionInfo> favorings;
16191670
determineBestChoicesInContext(*this, disjunctions, favorings);
16201671

1621-
if (auto *disjunction =
1622-
selectDisjunctionInResultBuilderContext(*this, disjunctions)) {
1623-
return std::make_pair(disjunction, favorings[disjunction].FavoredChoices);
1624-
}
1625-
16261672
// Pick the disjunction with the smallest number of favored, then active
16271673
// choices.
16281674
auto bestDisjunction = std::min_element(
@@ -1634,6 +1680,9 @@ ConstraintSystem::selectDisjunction() {
16341680
if (firstActive == 1 || secondActive == 1)
16351681
return secondActive != 1;
16361682

1683+
if (auto preference = isPreferable(*this, first, second))
1684+
return preference.value();
1685+
16371686
auto &[firstScore, firstFavoredChoices] = favorings[first];
16381687
auto &[secondScore, secondFavoredChoices] = favorings[second];
16391688

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
// RUN: %target-typecheck-verify-swift -target %target-cpu-apple-macosx12 -solver-scope-threshold=10000
2+
3+
// REQUIRES: OS=macosx
4+
// REQUIRES: objc_interop
5+
6+
import Foundation
7+
import SwiftUI
8+
9+
struct MyView: View {
10+
public enum Style {
11+
case focusRing(platterSize: CGSize, stroke: CGFloat, offset: CGFloat)
12+
}
13+
14+
var style: Style
15+
var isFocused: Bool
16+
var focusColor: Color
17+
18+
var body: some View {
19+
Group {
20+
switch style {
21+
case let .focusRing(platterSize: platterSize, stroke: focusRingStroke, offset: focusRingOffset):
22+
Circle()
23+
.overlay {
24+
Circle()
25+
.stroke(isFocused ? focusColor : Color.clear, lineWidth: focusRingStroke)
26+
.frame(
27+
width: platterSize.width + (2 * focusRingOffset) + focusRingStroke,
28+
height: platterSize.height + (2 * focusRingOffset) + focusRingStroke
29+
)
30+
}
31+
}
32+
}
33+
}
34+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
// RUN: %target-typecheck-verify-swift -solver-scope-threshold=200
2+
// REQUIRES: tools-release,no_asan
3+
// REQUIRES: OS=macosx
4+
5+
struct Time {
6+
static func +(_: Time, _: Double) -> Time {
7+
Time()
8+
}
9+
10+
static func now() -> Time { Time() }
11+
}
12+
13+
struct Queue {
14+
static let current = Queue()
15+
16+
func after(deadline: Time, execute: () -> Void) {}
17+
func after(deadline: Time, execute: (Int) -> Void) {}
18+
}
19+
20+
func compute(_: () -> Void) {}
21+
22+
protocol P {
23+
}
24+
25+
@resultBuilder
26+
struct Builder {
27+
static func buildExpression<T: P>(_ v: T) -> T {
28+
v
29+
}
30+
31+
static func buildBlock<T: P>(_ v: T) -> T {
32+
v
33+
}
34+
35+
static func buildBlokc<T0: P, T1: P>(_ v0: T0, _ v1: T1) -> (T0, T1) {
36+
(v0, v1)
37+
}
38+
}
39+
40+
struct MyP: P {
41+
func onAction(_: () -> Void) -> some P { self }
42+
}
43+
44+
class Test {
45+
var value = 0.0
46+
47+
@Builder func test() -> some P {
48+
MyP().onAction {
49+
Queue.current.after(deadline: .now() + 0.1) {
50+
compute {
51+
value = 0.3
52+
Queue.current.after(deadline: .now() + 0.2) {
53+
compute {
54+
value = 1.0
55+
Queue.current.after(deadline: .now() + 0.2) {
56+
compute {
57+
value = 0.3
58+
Queue.current.after(deadline: .now() + 0.2) {
59+
compute {
60+
value = 1.0
61+
}
62+
}
63+
}
64+
}
65+
}
66+
}
67+
}
68+
}
69+
}
70+
}
71+
}

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// RUN: %target-typecheck-verify-swift -target %target-cpu-apple-macosx10.15 -solver-scope-threshold=1000
1+
// RUN: %target-typecheck-verify-swift -target %target-cpu-apple-macosx10.15 -solver-scope-threshold=5500
22
// REQUIRES: OS=macosx
33

44
import SwiftUI

0 commit comments

Comments
 (0)