Skip to content

Commit bd5a437

Browse files
authored
Merge pull request #41615 from slavapestov/rqm-protocol-superclass-circular-conformance
RequirementMachine: Fix minimization when protocol is constrained to a class that conforms to the protocol
2 parents 10b5031 + 5281426 commit bd5a437

File tree

7 files changed

+223
-54
lines changed

7 files changed

+223
-54
lines changed

lib/AST/RequirementMachine/ConcreteContraction.cpp

Lines changed: 41 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,8 @@ class ConcreteContraction {
159159

160160
llvm::SmallDenseMap<GenericParamKey, Type> ConcreteTypes;
161161
llvm::SmallDenseMap<GenericParamKey, Type> Superclasses;
162+
llvm::SmallDenseMap<GenericParamKey,
163+
llvm::SmallVector<ProtocolDecl *, 1>> Conformances;
162164

163165
Optional<Type> substTypeParameter(GenericTypeParamType *rootParam,
164166
DependentMemberType *memberType,
@@ -436,8 +438,8 @@ bool ConcreteContraction::performConcreteContraction(
436438
if (Debug) {
437439
llvm::dbgs() << "@ Concrete contraction cannot proceed: "
438440
<< "duplicate concrete type requirements\n";
439-
return false;
440441
}
442+
return false;
441443
}
442444

443445
break;
@@ -454,18 +456,54 @@ bool ConcreteContraction::performConcreteContraction(
454456
if (Debug) {
455457
llvm::dbgs() << "@ Concrete contraction cannot proceed: "
456458
<< "duplicate superclass requirements\n";
457-
return false;
458459
}
460+
return false;
459461
}
460462

461463
break;
462464
}
463-
case RequirementKind::Conformance:
465+
case RequirementKind::Conformance: {
466+
auto *protoDecl = req.req.getProtocolDecl();
467+
Conformances[GenericParamKey(genericParam)].push_back(protoDecl);
468+
469+
break;
470+
}
464471
case RequirementKind::Layout:
465472
break;
466473
}
467474
}
468475

476+
// Block concrete contraction if a generic parameter conforms to a protocol P
477+
// which has a superclass bound C which again conforms to P. This is a really
478+
// silly edge case, but we go to great pains to produce the same minimized
479+
// signature as the GenericSignatureBuilder in this case, <T : P>, and not the
480+
// more logical <T : C>.
481+
for (const auto &pair : Conformances) {
482+
auto subjectType = pair.first;
483+
auto found = Superclasses.find(subjectType);
484+
if (found == Superclasses.end())
485+
continue;
486+
487+
auto superclassTy = found->second;
488+
489+
for (const auto *proto : pair.second) {
490+
if (auto otherSuperclassTy = proto->getSuperclass()) {
491+
if (Debug) {
492+
llvm::dbgs() << "@ Subject type of superclass requirement "
493+
<< "τ_" << subjectType.Depth << "_" << subjectType.Index
494+
<< " : " << superclassTy << " conforms to "
495+
<< proto->getName() << " which has a superclass bound "
496+
<< otherSuperclassTy << "\n";
497+
}
498+
499+
if (superclassTy->isEqual(otherSuperclassTy)) {
500+
Superclasses.erase(subjectType);
501+
break;
502+
}
503+
}
504+
}
505+
}
506+
469507
// If there's nothing to do just return.
470508
if (ConcreteTypes.empty() && Superclasses.empty())
471509
return false;

lib/AST/RequirementMachine/MinimalConformances.cpp

Lines changed: 93 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,13 @@ class MinimalConformances {
195195
MutableTerm term, unsigned ruleID,
196196
SmallVectorImpl<unsigned> &result) const;
197197

198+
bool isConformanceRuleRecoverable(
199+
llvm::SmallDenseSet<unsigned, 4> &visited,
200+
unsigned ruleID) const;
201+
202+
bool isDerivedViaCircularConformanceRule(
203+
const std::vector<SmallVector<unsigned, 2>> &paths) const;
204+
198205
bool isValidConformancePath(
199206
llvm::SmallDenseSet<unsigned, 4> &visited,
200207
const llvm::SmallVectorImpl<unsigned> &path) const;
@@ -601,57 +608,71 @@ void MinimalConformances::computeCandidateConformancePaths() {
601608
}
602609
}
603610

604-
/// Determines if \p path can be expressed without any of the conformance
605-
/// rules appearing in \p redundantConformances, by possibly substituting
606-
/// any occurrences of the redundant rules with alternate definitions
607-
/// appearing in \p conformancePaths.
611+
/// If \p ruleID is redundant, determines if it can be expressed without
612+
/// without any of the conformance rules determined to be redundant so far.
608613
///
609-
/// The \p conformancePaths map sends conformance rules to a list of
610-
/// disjunctions, where each disjunction is a product of other conformance
611-
/// rules.
612-
bool MinimalConformances::isValidConformancePath(
614+
/// If the rule is not redundant, determines if its parent path can
615+
/// also be recovered.
616+
bool MinimalConformances::isConformanceRuleRecoverable(
613617
llvm::SmallDenseSet<unsigned, 4> &visited,
614-
const llvm::SmallVectorImpl<unsigned> &path) const {
618+
unsigned ruleID) const {
619+
if (RedundantConformances.count(ruleID)) {
620+
SWIFT_DEFER {
621+
visited.erase(ruleID);
622+
};
623+
visited.insert(ruleID);
615624

616-
for (unsigned ruleID : path) {
617-
if (visited.count(ruleID) > 0)
625+
auto found = ConformancePaths.find(ruleID);
626+
if (found == ConformancePaths.end())
618627
return false;
619628

620-
if (RedundantConformances.count(ruleID)) {
629+
bool foundValidConformancePath = false;
630+
for (const auto &otherPath : found->second) {
631+
if (isValidConformancePath(visited, otherPath)) {
632+
foundValidConformancePath = true;
633+
break;
634+
}
635+
}
636+
637+
if (!foundValidConformancePath)
638+
return false;
639+
} else {
640+
auto found = ParentPaths.find(ruleID);
641+
if (found != ParentPaths.end()) {
621642
SWIFT_DEFER {
622643
visited.erase(ruleID);
623644
};
624645
visited.insert(ruleID);
625646

626-
auto found = ConformancePaths.find(ruleID);
627-
if (found == ConformancePaths.end())
647+
// If 'req' is based on some other conformance requirement
648+
// `T.[P.]A : Q', we want to make sure that we have a
649+
// non-redundant derivation for 'T : P'.
650+
if (!isValidConformancePath(visited, found->second))
628651
return false;
652+
}
653+
}
629654

630-
bool foundValidConformancePath = false;
631-
for (const auto &otherPath : found->second) {
632-
if (isValidConformancePath(visited, otherPath)) {
633-
foundValidConformancePath = true;
634-
break;
635-
}
636-
}
655+
return true;
656+
}
637657

638-
if (!foundValidConformancePath)
639-
return false;
640-
} else {
641-
auto found = ParentPaths.find(ruleID);
642-
if (found != ParentPaths.end()) {
643-
SWIFT_DEFER {
644-
visited.erase(ruleID);
645-
};
646-
visited.insert(ruleID);
647-
648-
// If 'req' is based on some other conformance requirement
649-
// `T.[P.]A : Q', we want to make sure that we have a
650-
// non-redundant derivation for 'T : P'.
651-
if (!isValidConformancePath(visited, found->second))
652-
return false;
653-
}
654-
}
658+
/// Determines if \p path can be expressed without any of the conformance
659+
/// rules determined to be redundant so far, by possibly substituting
660+
/// any occurrences of the redundant rules with alternate definitions
661+
/// appearing in the set of known conformancePaths.
662+
///
663+
/// The conformance path map sends conformance rules to a list of
664+
/// disjunctions, where each disjunction is a product of other conformance
665+
/// rules.
666+
bool MinimalConformances::isValidConformancePath(
667+
llvm::SmallDenseSet<unsigned, 4> &visited,
668+
const llvm::SmallVectorImpl<unsigned> &path) const {
669+
670+
for (unsigned ruleID : path) {
671+
if (visited.count(ruleID) > 0)
672+
return false;
673+
674+
if (!isConformanceRuleRecoverable(visited, ruleID))
675+
return false;
655676
}
656677

657678
return true;
@@ -786,12 +807,45 @@ void MinimalConformances::verifyMinimalConformanceEquations() const {
786807
#endif
787808
}
788809

810+
bool MinimalConformances::isDerivedViaCircularConformanceRule(
811+
const std::vector<SmallVector<unsigned, 2>> &paths) const {
812+
for (const auto &path : paths) {
813+
if (!path.empty() &&
814+
System.getRule(path.back()).isCircularConformanceRule())
815+
return true;
816+
}
817+
818+
return false;
819+
}
820+
789821
/// Find a set of minimal conformances by marking all non-minimal
790822
/// conformances redundant.
791823
///
792824
/// In the first pass, we only consider conformance requirements that are
793825
/// made redundant by concrete conformances.
794826
void MinimalConformances::computeMinimalConformances() {
827+
// First, mark any concrete conformances derived via a circular
828+
// conformance as redundant upfront. See the comment at the top of
829+
// Rule::isCircularConformanceRule() for an explanation of this.
830+
for (unsigned ruleID : ConformanceRules) {
831+
auto found = ConformancePaths.find(ruleID);
832+
if (found == ConformancePaths.end())
833+
continue;
834+
835+
const auto &paths = found->second;
836+
837+
if (System.getRule(ruleID).isProtocolConformanceRule())
838+
continue;
839+
840+
if (isDerivedViaCircularConformanceRule(paths))
841+
RedundantConformances.insert(ruleID);
842+
}
843+
844+
// Now, visit each conformance rule, trying to make it redundant by
845+
// deriving a path in terms of other non-redundant conformance rules.
846+
//
847+
// Note that the ConformanceRules vector is sorted in descending
848+
// canonical term order, so less canonical rules are eliminated first.
795849
for (unsigned ruleID : ConformanceRules) {
796850
auto found = ConformancePaths.find(ruleID);
797851
if (found == ConformancePaths.end())
@@ -840,10 +894,7 @@ void MinimalConformances::verifyMinimalConformances() const {
840894
// minimal conformances.
841895
llvm::SmallDenseSet<unsigned, 4> visited;
842896

843-
llvm::SmallVector<unsigned, 1> path;
844-
path.push_back(ruleID);
845-
846-
if (!isValidConformancePath(visited, path)) {
897+
if (!isConformanceRuleRecoverable(visited, ruleID)) {
847898
llvm::errs() << "Redundant conformance is not recoverable:\n";
848899
llvm::errs() << rule << "\n\n";
849900
dumpMinimalConformanceEquations(llvm::errs());

lib/AST/RequirementMachine/RewriteSystem.cpp

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,32 @@ bool Rule::isProtocolRefinementRule() const {
120120
return false;
121121
}
122122

123+
/// If this is a rule of the form [P].[concrete: C : Q] => [P] where
124+
/// [P] is a protocol symbol, return true.
125+
///
126+
/// This means that P constrains 'Self' to a concrete type that conforms
127+
/// to some Q with P : Q. We don't consider this to be a valid conformance
128+
/// path element, to ensure compatibility with the GSB in an odd edge
129+
/// case:
130+
///
131+
/// protocol P : C {}
132+
/// class C : P {}
133+
///
134+
/// The GSB minimizes the signature <T where T : P> to <T where T : P>,
135+
/// whereas the minimal conformances algorithm would otherwise minimize
136+
/// it down to <T where T : C> on account of the (T.[P] => T) conformance
137+
/// rule being redundantly expressed via [P].[concrete: C : P].
138+
bool Rule::isCircularConformanceRule() const {
139+
if (LHS.size() != 2 || RHS.size() != 1 || LHS[0] != RHS[0])
140+
return false;
141+
142+
if (LHS[0].getKind() != Symbol::Kind::Protocol ||
143+
LHS[1].getKind() != Symbol::Kind::ConcreteConformance)
144+
return false;
145+
146+
return true;
147+
}
148+
123149
/// A protocol typealias rule takes one of the following two forms,
124150
/// where T is a name symbol:
125151
///

lib/AST/RequirementMachine/RewriteSystem.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,8 @@ class Rule final {
106106

107107
bool isProtocolRefinementRule() const;
108108

109+
bool isCircularConformanceRule() const;
110+
109111
/// See above for an explanation of these predicates.
110112
bool isPermanent() const {
111113
return Permanent;

test/Generics/derived_via_concrete.swift

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
1-
// RUN: %target-typecheck-verify-swift -requirement-machine-inferred-signatures=verify
2-
// RUN: %target-swift-frontend -typecheck %s -debug-generic-signatures -requirement-machine-inferred-signatures=verify 2>&1 | %FileCheck %s
1+
// RUN: %target-typecheck-verify-swift -requirement-machine-inferred-signatures=off
2+
// RUN: not %target-swift-frontend -typecheck %s -debug-generic-signatures -requirement-machine-inferred-signatures=on 2>&1 | %FileCheck %s
3+
4+
// FIXME: Both RUN lines should pass 'on' once diagnostics are implemented.
35

46
protocol P {}
57
class C {}
@@ -38,7 +40,11 @@ func derivedViaConcreteY1<A, B>(_: A, _: B)
3840
// expected-warning@-1 {{redundant conformance constraint 'A' : 'V'}}
3941
// expected-note@-2 {{conformance constraint 'A' : 'V' implied here}}
4042

41-
// CHECK: Generic signature: <A, B where A : Y<B>, B : C>
43+
// CHECK: Generic signature: <A, B where A : Y<B>>
44+
//
45+
// FIXME: Should be <A, B where A : Y<B>, B : C>, but redundant
46+
// superclass requirements currently block concrete contraction.
47+
4248
func derivedViaConcreteY2<A, B>(_: A, _: B)
4349
where A : V, B : C, A : Y<B> {}
4450
// expected-warning@-1 {{redundant conformance constraint 'A' : 'V'}}

test/Generics/protocol_superclass_cycle.swift

Lines changed: 51 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,54 @@
1-
// RUN: %target-typecheck-verify-swift
2-
// RUN: %target-swift-frontend -typecheck -debug-generic-signatures %s 2>&1 | %FileCheck %s
1+
// RUN: %target-swift-frontend -typecheck -debug-generic-signatures %s -requirement-machine-protocol-signatures=verify -requirement-machine-inferred-signatures=verify 2>&1 | %FileCheck %s
2+
// RUN: %target-swift-frontend -typecheck -debug-generic-signatures %s -requirement-machine-protocol-signatures=verify -requirement-machine-inferred-signatures=verify -disable-requirement-machine-concrete-contraction 2>&1 | %FileCheck %s
33

44
protocol P : C {}
5-
final class C : P {}
5+
class C : P {}
6+
7+
// CHECK-LABEL: .SubP@
8+
// CHECK-NEXT: Requirement signature: <Self where Self : P>
9+
protocol SubP : P {}
10+
11+
protocol Q {
12+
associatedtype T
13+
}
14+
15+
// CHECK-LABEL: .foo1@
16+
// CHECK-NEXT: Generic signature: <T where T : P>
17+
func foo1<T : P>(_: T) {}
18+
19+
// CHECK-LABEL: .foo1a@
20+
// CHECK-NEXT: Generic signature: <T where T : P>
21+
func foo1a<T>(_: T) where T : P, T : C {}
22+
23+
// CHECK-LABEL: .foo1b@
24+
// CHECK-NEXT: Generic signature: <T where T : P>
25+
func foo1b<T>(_: T) where T : C, T : P {}
26+
27+
// CHECK-LABEL: .foo2@
28+
// CHECK-NEXT: Generic signature: <T where T : Q, T.[Q]T : P>
29+
func foo2<T : Q>(_: T) where T.T : P {}
30+
31+
// CHECK-LABEL: .foo3@
32+
// CHECK-NEXT: Generic signature: <T where T : SubP>
33+
func foo3<T : SubP>(_: T) {}
34+
35+
// CHECK-LABEL: .foo4@
36+
// CHECK-NEXT: Generic signature: <T where T : Q, T.[Q]T : SubP>
37+
func foo4<T : Q>(_: T) where T.T : SubP {}
38+
39+
protocol SuperP {}
40+
41+
// CHECK-LABEL: .SubSuperP@
42+
// CHECK-NEXT: Requirement signature: <Self where Self : SuperC>
43+
protocol SubSuperP : SuperP, SuperC {}
44+
45+
class SuperC : SubSuperP {}
46+
47+
// CHECK-LABEL: .foo5@
48+
// CHECK-NEXT: Generic signature: <T where T : SubSuperP>
49+
func foo5<T : SubSuperP>(_: T) {}
50+
51+
// CHECK-LABEL: .foo6@
52+
// CHECK-NEXT: Generic signature: <T where T : Q, T.[Q]T : SubSuperP>
53+
func foo6<T : Q>(_: T) where T.T : SubSuperP {}
654

7-
// CHECK: Generic signature: <T where T : P>
8-
func foo<T : P>(_: T) {}

test/SILGen/protocol_superclass_cycle.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// RUN: %target-swift-frontend -emit-silgen %s
1+
// RUN: %target-swift-frontend -emit-silgen -requirement-machine-protocol-signatures=on -requirement-machine-inferred-signatures=on %s
22

33
// When a protocol has a superclass requirement on 'Self' and the class
44
// itself conforms concretely, the forwarding substitution map will have

0 commit comments

Comments
 (0)