Skip to content

Commit 64d1931

Browse files
committed
RequirementMachine: Sketch out "generating conformances" algorithm
1 parent 5f3d781 commit 64d1931

File tree

6 files changed

+484
-57
lines changed

6 files changed

+484
-57
lines changed

lib/AST/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ add_swift_host_library(swiftAST STATIC
7575
RawComment.cpp
7676
RequirementEnvironment.cpp
7777
RequirementMachine/HomotopyReduction.cpp
78+
RequirementMachine/GeneratingConformances.cpp
7879
RequirementMachine/GenericSignatureQueries.cpp
7980
RequirementMachine/PropertyMap.cpp
8081
RequirementMachine/ProtocolGraph.cpp

lib/AST/RequirementMachine/Debug.h

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,10 @@ enum class DebugFlags : unsigned {
3939
ConcretizeNestedTypes = (1<<5),
4040

4141
/// Print debug output from the homotopy reduction algorithm.
42-
HomotopyReduction = (1<<6)
42+
HomotopyReduction = (1<<6),
43+
44+
/// Print debug output from the generating conformances algorithm.
45+
GeneratingConformances = (1<<7),
4346
};
4447

4548
using DebugOptions = OptionSet<DebugFlags>;
Lines changed: 368 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,368 @@
1+
//===--- GeneratingConformances.cpp - Reasoning about conformance rules ---===//
2+
//
3+
// This source file is part of the Swift.org open source project
4+
//
5+
// Copyright (c) 2021 Apple Inc. and the Swift project authors
6+
// Licensed under Apache License v2.0 with Runtime Library Exception
7+
//
8+
// See https://swift.org/LICENSE.txt for license information
9+
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
10+
//
11+
//===----------------------------------------------------------------------===//
12+
//
13+
// This file implements an algorithm to find a minimal set of "generating
14+
// conformances", which are rules (V1.[P1] => V1), ..., (Vn.[Pn] => Vn) such
15+
// that any valid term of the form T.[P] can be written as a product of terms
16+
// (Vi.[Pi]), where each Vi.[Pi] is a left hand side of a generating
17+
// conformance.
18+
//
19+
//===----------------------------------------------------------------------===//
20+
21+
#include "swift/Basic/Defer.h"
22+
#include "swift/Basic/Range.h"
23+
#include "llvm/ADT/DenseMap.h"
24+
#include "llvm/ADT/DenseSet.h"
25+
#include "llvm/Support/Debug.h"
26+
#include "llvm/Support/raw_ostream.h"
27+
#include <algorithm>
28+
#include "RewriteSystem.h"
29+
30+
using namespace swift;
31+
using namespace rewriting;
32+
33+
void HomotopyGenerator::findProtocolConformanceRules(
34+
SmallVectorImpl<unsigned> &notInContext,
35+
SmallVectorImpl<std::pair<MutableTerm, unsigned>> &inContext,
36+
const RewriteSystem &system) const {
37+
38+
MutableTerm term = Basepoint;
39+
40+
for (const auto &step : Path) {
41+
switch (step.Kind) {
42+
case RewriteStep::ApplyRewriteRule: {
43+
const auto &rule = system.getRule(step.RuleID);
44+
if (!rule.isProtocolConformanceRule())
45+
break;
46+
47+
if (!step.isInContext()) {
48+
assert(std::find(notInContext.begin(),
49+
notInContext.end(),
50+
step.RuleID) == notInContext.end() &&
51+
"A conformance rule appears more than once without context?");
52+
notInContext.push_back(step.RuleID);
53+
} else if (step.EndOffset == 0) {
54+
assert(step.StartOffset > 0);
55+
MutableTerm prefix(term.begin(), term.begin() + step.StartOffset);
56+
inContext.emplace_back(prefix, step.RuleID);
57+
}
58+
break;
59+
}
60+
61+
case RewriteStep::AdjustConcreteType:
62+
break;
63+
}
64+
65+
step.apply(term, system);
66+
}
67+
68+
assert(notInContext.empty() || !inContext.empty() &&
69+
"A conformance rule not based on another conformance rule?");
70+
}
71+
72+
/// Write the term as a product of left hand sides of protocol conformance
73+
/// rules.
74+
///
75+
/// The term should already be simplified, except for a protocol symbol
76+
/// at the end.
77+
void
78+
RewriteSystem::decomposeTermIntoConformanceRuleLeftHandSides(
79+
MutableTerm term, SmallVectorImpl<unsigned> &result) const {
80+
assert(term.back().getKind() == Symbol::Kind::Protocol);
81+
82+
// If T is canonical and T.[P] => T, then by confluence, T.[P]
83+
// reduces to T in a single step, via a rule V.[P] => V, where
84+
// T == U.V.
85+
RewritePath steps;
86+
bool simplified = simplify(term, &steps);
87+
if (!simplified) {
88+
llvm::errs() << "Term does not conform to protocol: " << term << "\n";
89+
abort();
90+
}
91+
92+
assert(steps.size() == 1 &&
93+
"Canonical conformance term should simplify in one step");
94+
95+
const auto &step = *steps.begin();
96+
assert(step.Kind == RewriteStep::ApplyRewriteRule);
97+
assert(step.EndOffset == 0);
98+
assert(!step.Inverse);
99+
100+
const auto &rule = getRule(step.RuleID);
101+
assert(rule.isProtocolConformanceRule());
102+
103+
// If |U| > 0, recurse with the term U.[domain(V)]. Since T is
104+
// canonical, we know that U is canonical as well.
105+
if (step.StartOffset > 0) {
106+
// Build the term U.
107+
MutableTerm prefix(term.begin(), term.begin() + step.StartOffset);
108+
109+
// Compute domain(V).
110+
const auto &lhs = rule.getLHS();
111+
auto protocols = lhs[0].getProtocols();
112+
assert(protocols.size() == 1);
113+
114+
// Build the term U.[domain(V)].
115+
prefix.add(Symbol::forProtocol(protocols[0], Context));
116+
117+
decomposeTermIntoConformanceRuleLeftHandSides(prefix, result);
118+
}
119+
120+
result.push_back(step.RuleID);
121+
}
122+
123+
/// Use homotopy information to discover all ways of writing the left hand side
124+
/// of each conformance rule as a product of left hand sides of other conformance
125+
/// rules.
126+
///
127+
/// Each conformance rule (Vi.[P] => Vi) can always be written in terms of itself,
128+
/// so the first term of each disjunction is always (Vi.[P] => Vi).
129+
///
130+
/// Conformance rules can also be circular, so not every choice of disjunctions
131+
/// produces a valid result; for example, if you have these definitions:
132+
///
133+
/// protocol P {
134+
/// associatedtype T : P
135+
/// }
136+
///
137+
/// struct G<X, Y> where X : P, X.T == Y, Y : P, Y.T == X {}
138+
///
139+
/// We have three conformance rules:
140+
///
141+
/// [P:T].[P] => [P:T]
142+
/// <X>.[P] => <X>
143+
/// <Y>.[P] => <Y>
144+
///
145+
/// The first rule, <X>.[P] => <X> has an alternate conformance path:
146+
///
147+
/// (<Y>.[P]).([P:T].[P])
148+
///
149+
/// The second rule similarly has an alternate conformance path:
150+
///
151+
/// (<X>.[P]).([P:T].[P])
152+
///
153+
/// This gives us the following initial set of candidate conformance paths:
154+
///
155+
/// [P:T].[P] := ([P:T].[P])
156+
/// <X>.[P] := (<X>.[P]) ∨ (<Y>.[P]).([P:T].[P])
157+
/// <Y>.[P] := (<Y>.[P]) ∨ (<X>.[P]).([P:T].[P])
158+
///
159+
/// One valid solution is the following set of assignments:
160+
///
161+
/// [P:T].[P] := ([P:T].[P])
162+
/// <X>.[P] := (<X>.[P])
163+
/// <Y>.[P] := (<X>.[P]).([P:T].[P])
164+
///
165+
/// That is, we can choose to eliminate <X>.[P], but not <Y>.[P], or vice
166+
/// versa; but it is never valid to eliminate both.
167+
void RewriteSystem::computeCandidateConformancePaths(
168+
llvm::MapVector<unsigned,
169+
std::vector<SmallVector<unsigned, 2>>> &conformancePaths) const {
170+
for (const auto &loop : HomotopyGenerators) {
171+
if (loop.isDeleted())
172+
continue;
173+
174+
SmallVector<unsigned, 2> notInContext;
175+
SmallVector<std::pair<MutableTerm, unsigned>, 2> inContext;
176+
177+
loop.findProtocolConformanceRules(notInContext, inContext, *this);
178+
179+
if (notInContext.empty())
180+
continue;
181+
182+
// We must either have multiple conformance rules in empty context, or
183+
// at least one conformance rule in non-empty context. Otherwise, we have
184+
// a conformance rule which is written as a series of same-type rules,
185+
// which doesn't make sense.
186+
assert(inContext.size() > 0 || notInContext.size() > 1);
187+
188+
if (Debug.contains(DebugFlags::GeneratingConformances)) {
189+
llvm::dbgs() << "Candidate homotopy generator: ";
190+
loop.dump(llvm::dbgs(), *this);
191+
llvm::dbgs() << "\n";
192+
193+
llvm::dbgs() << "* Conformance rules not in context:\n";
194+
for (unsigned ruleID : notInContext) {
195+
llvm::dbgs() << "- (#" << ruleID << ") " << getRule(ruleID) << "\n";
196+
}
197+
198+
llvm::dbgs() << "* Conformance rules in context:\n";
199+
for (auto pair : inContext) {
200+
llvm::dbgs() << "- " << pair.first;
201+
unsigned ruleID = pair.second;
202+
llvm::dbgs() << " (#" << ruleID << ") " << getRule(ruleID) << "\n";
203+
}
204+
}
205+
206+
// Suppose a 3-cell contains a conformance rule (T.[P] => T) in an empty
207+
// context, and a conformance rule (V.[P] => V) with a possibly non-empty
208+
// left context U and empty right context.
209+
//
210+
// We can decompose U into a product of conformance rules:
211+
//
212+
// (V1.[P1] => V1)...(Vn.[Pn] => Vn),
213+
//
214+
// Now, we can record a candidate decomposition of (T.[P] => T) as a
215+
// product of conformance rules:
216+
//
217+
// (T.[P] => T) := (V1.[P1] => V1)...(Vn.[Pn] => Vn).(V.[P] => V)
218+
//
219+
// Now if U is empty, this becomes the trivial candidate:
220+
//
221+
// (T.[P] => T) := (V.[P] => V)
222+
SmallVector<SmallVector<unsigned, 2>, 2> candidatePaths;
223+
for (auto pair : inContext) {
224+
// We have a term U, and a rule V.[P] => V.
225+
const auto &rule = getRule(pair.second);
226+
assert(rule.isProtocolConformanceRule());
227+
228+
SmallVector<unsigned, 2> conformancePath;
229+
230+
// Simplify U to get U'.
231+
MutableTerm term = pair.first;
232+
(void) simplify(term);
233+
234+
// Compute domain(V).
235+
const auto &lhs = rule.getLHS();
236+
auto protocols = lhs[0].getProtocols();
237+
assert(protocols.size() == 1);
238+
239+
// Build the term U'.[domain(V)].
240+
term.add(Symbol::forProtocol(protocols[0], Context));
241+
242+
// Write U'.[domain(V)] as a product of left hand sides of protocol
243+
// conformance rules.
244+
decomposeTermIntoConformanceRuleLeftHandSides(term, conformancePath);
245+
246+
// Add the rule V => V.[P].
247+
conformancePath.push_back(pair.second);
248+
249+
candidatePaths.push_back(conformancePath);
250+
}
251+
252+
for (unsigned candidateRuleID : notInContext) {
253+
// If multiple conformance rules appear in an empty context, each one
254+
// can be replaced with any other conformance rule.
255+
for (unsigned otherRuleID : notInContext) {
256+
if (otherRuleID == candidateRuleID)
257+
continue;
258+
259+
SmallVector<unsigned, 2> path;
260+
path.push_back(otherRuleID);
261+
conformancePaths[candidateRuleID].push_back(path);
262+
}
263+
264+
// If conformance rules appear in non-empty context, they define a
265+
// conformance access path for each conformance rule in empty context.
266+
for (const auto &path : candidatePaths) {
267+
conformancePaths[candidateRuleID].push_back(path);
268+
}
269+
}
270+
}
271+
}
272+
273+
bool RewriteSystem::isValidConformancePath(
274+
llvm::SmallDenseSet<unsigned, 4> &visited,
275+
llvm::DenseSet<unsigned> &redundantConformances,
276+
const llvm::SmallVectorImpl<unsigned> &path,
277+
const llvm::MapVector<unsigned,
278+
std::vector<SmallVector<unsigned, 2>>>
279+
&conformancePaths) const {
280+
for (unsigned ruleID : path) {
281+
if (visited.count(ruleID) > 0)
282+
return false;
283+
284+
if (!redundantConformances.count(ruleID))
285+
continue;
286+
287+
SWIFT_DEFER {
288+
visited.erase(ruleID);
289+
};
290+
visited.insert(ruleID);
291+
292+
auto found = conformancePaths.find(ruleID);
293+
assert(found != conformancePaths.end());
294+
295+
bool foundValidConformancePath = false;
296+
for (const auto &otherPath : found->second) {
297+
if (isValidConformancePath(visited, redundantConformances,
298+
otherPath, conformancePaths)) {
299+
foundValidConformancePath = true;
300+
break;
301+
}
302+
}
303+
304+
if (!foundValidConformancePath)
305+
return false;
306+
}
307+
308+
return true;
309+
}
310+
311+
void RewriteSystem::computeGeneratingConformances(
312+
llvm::DenseSet<unsigned> &redundantConformances) {
313+
llvm::MapVector<unsigned, std::vector<SmallVector<unsigned, 2>>> conformancePaths;
314+
315+
for (unsigned ruleID : indices(Rules)) {
316+
const auto &rule = getRule(ruleID);
317+
if (rule.isProtocolConformanceRule()) {
318+
SmallVector<unsigned, 2> path;
319+
path.push_back(ruleID);
320+
conformancePaths[ruleID].push_back(path);
321+
}
322+
}
323+
324+
computeCandidateConformancePaths(conformancePaths);
325+
326+
if (Debug.contains(DebugFlags::GeneratingConformances)) {
327+
llvm::dbgs() << "Initial set of equations:\n";
328+
for (const auto &pair : conformancePaths) {
329+
llvm::dbgs() << "- " << getRule(pair.first).getLHS() << " := ";
330+
331+
bool first = true;
332+
for (const auto &path : pair.second) {
333+
if (!first)
334+
llvm::dbgs() << "";
335+
else
336+
first = false;
337+
for (unsigned ruleID : path)
338+
llvm::dbgs() << "(" << getRule(ruleID).getLHS() << ")";
339+
}
340+
341+
llvm::dbgs() << "\n";
342+
}
343+
}
344+
345+
for (const auto &pair : conformancePaths) {
346+
for (const auto &path : pair.second) {
347+
llvm::SmallDenseSet<unsigned, 4> visited;
348+
visited.insert(pair.first);
349+
350+
if (isValidConformancePath(visited, redundantConformances,
351+
path, conformancePaths)) {
352+
redundantConformances.insert(pair.first);
353+
break;
354+
}
355+
}
356+
}
357+
358+
if (Debug.contains(DebugFlags::GeneratingConformances)) {
359+
llvm::dbgs() << "Generating conformances:\n";
360+
361+
for (const auto &pair : conformancePaths) {
362+
if (redundantConformances.count(pair.first) > 0)
363+
continue;
364+
365+
llvm::dbgs() << "- " << getRule(pair.first) << "\n";
366+
}
367+
}
368+
}

0 commit comments

Comments
 (0)