Skip to content

Commit 21a3705

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 705f232 commit 21a3705

File tree

4 files changed

+227
-73
lines changed

4 files changed

+227
-73
lines changed

lib/Sema/CSOptimizer.cpp

Lines changed: 121 additions & 72 deletions
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

@@ -1542,77 +1640,30 @@ static void determineBestChoicesInContext(
15421640
}
15431641
}
15441642

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

1609-
if (auto depth = cs.getExprDepth(member)) {
1610-
if (!best.first || best.second > depth)
1611-
best = std::make_pair(disjunction, depth.value());
1651+
// Prevent operator selection if its passed as an argument
1652+
// to not-yet resolved call. This helps to make sure that
1653+
// in result builder context chained members and other
1654+
// non-operator disjunctions are always selected first,
1655+
// because they provide the context and help to prune the system.
1656+
if (isInResultBuilderContext(cs, disjunctionA)) {
1657+
if (isOperatorDisjunction(disjunctionA)) {
1658+
if (isOperatorPassedToUnresolvedCall(cs, disjunctionA))
1659+
return false;
1660+
} else {
1661+
if (isOperatorPassedToUnresolvedCall(cs, disjunctionB))
1662+
return true;
16121663
}
16131664
}
16141665

1615-
return best.first;
1666+
return std::nullopt;
16161667
}
16171668

16181669
std::optional<std::pair<Constraint *, llvm::TinyPtrVector<Constraint *>>>
@@ -1626,11 +1677,6 @@ ConstraintSystem::selectDisjunction() {
16261677
llvm::DenseMap<Constraint *, DisjunctionInfo> favorings;
16271678
determineBestChoicesInContext(*this, disjunctions, favorings);
16281679

1629-
if (auto *disjunction =
1630-
selectDisjunctionInResultBuilderContext(*this, disjunctions)) {
1631-
return std::make_pair(disjunction, favorings[disjunction].FavoredChoices);
1632-
}
1633-
16341680
// Pick the disjunction with the smallest number of favored, then active
16351681
// choices.
16361682
auto bestDisjunction = std::min_element(
@@ -1642,6 +1688,9 @@ ConstraintSystem::selectDisjunction() {
16421688
if (firstActive == 1 || secondActive == 1)
16431689
return secondActive != 1;
16441690

1691+
if (auto preference = isPreferable(*this, first, second))
1692+
return preference.value();
1693+
16451694
auto &[firstScore, firstFavoredChoices] = favorings[first];
16461695
auto &[secondScore, secondFavoredChoices] = favorings[second];
16471696

Lines changed: 34 additions & 0 deletions
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+
}
Lines changed: 71 additions & 0 deletions
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

Lines changed: 1 addition & 1 deletion
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)