Skip to content

Commit 713e0a4

Browse files
committed
RequirementMachine: Reduce replacement paths for redundant rules to left-canonical normal form
This brings back the code I deleted in 1bf6102 but repurposes it to simplify the replacement paths recorded for redundant rewrite rules only.
1 parent bc6a38c commit 713e0a4

File tree

6 files changed

+299
-21
lines changed

6 files changed

+299
-21
lines changed

lib/AST/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@ add_swift_host_library(swiftAST STATIC
8383
RequirementMachine/InterfaceType.cpp
8484
RequirementMachine/KnuthBendix.cpp
8585
RequirementMachine/MinimalConformances.cpp
86+
RequirementMachine/NormalizeRewritePath.cpp
8687
RequirementMachine/PropertyMap.cpp
8788
RequirementMachine/PropertyRelations.cpp
8889
RequirementMachine/PropertyUnification.cpp

lib/AST/RequirementMachine/HomotopyReduction.cpp

Lines changed: 38 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@
5555
#include "swift/Basic/Range.h"
5656
#include "llvm/ADT/DenseMap.h"
5757
#include "llvm/ADT/DenseSet.h"
58+
#include "llvm/ADT/SmallVector.h"
5859
#include "llvm/Support/Debug.h"
5960
#include "llvm/Support/raw_ostream.h"
6061
#include <algorithm>
@@ -426,8 +427,15 @@ findRuleToDelete(llvm::function_ref<bool(unsigned)> isRedundantRuleFn) {
426427

427428
// Delete loops that don't contain any rewrite rules in empty context,
428429
// since such loops do not give us useful information.
429-
if (!foundAny)
430+
if (!foundAny) {
431+
if (Debug.contains(DebugFlags::HomotopyReduction)) {
432+
llvm::dbgs() << "** Deleting useless loop #" << loopID << ": ";
433+
loop.dump(llvm::dbgs(), *this);
434+
llvm::dbgs() << "\n";
435+
}
436+
430437
loop.markDeleted();
438+
}
431439
}
432440

433441
Optional<std::pair<unsigned, unsigned>> found;
@@ -658,6 +666,34 @@ void RewriteSystem::performHomotopyReduction(
658666
}
659667
}
660668

669+
void RewriteSystem::normalizeRedundantRules() {
670+
for (auto &pair : RedundantRules) {
671+
pair.second.computeNormalForm(*this);
672+
}
673+
674+
if (Debug.contains(DebugFlags::RedundantRules)) {
675+
llvm::dbgs() << "\nRedundant rules:\n";
676+
for (const auto &pair : RedundantRules) {
677+
const auto &rule = getRule(pair.first);
678+
llvm::dbgs() << "- ("
679+
<< rule.getLHS() << " => "
680+
<< rule.getRHS() << ") ::== ";
681+
682+
MutableTerm lhs(rule.getLHS());
683+
pair.second.dump(llvm::dbgs(), lhs, *this);
684+
685+
llvm::dbgs() << "\n";
686+
687+
if (Debug.contains(DebugFlags::RedundantRulesDetail)) {
688+
llvm::dbgs() << "\n";
689+
pair.second.dumpLong(llvm::dbgs(), lhs, *this);
690+
691+
llvm::dbgs() << "\n\n";
692+
}
693+
}
694+
}
695+
}
696+
661697
/// Use the loops to delete redundant rewrite rules via a series of Tietze
662698
/// transformations, updating and simplifying existing loops as each rule
663699
/// is deleted.
@@ -753,25 +789,7 @@ void RewriteSystem::minimizeRewriteSystem() {
753789
verifyRedundantConformances(redundantConformances);
754790
verifyMinimizedRules(redundantConformances);
755791

756-
if (Debug.contains(DebugFlags::RedundantRules)) {
757-
llvm::dbgs() << "\nRedundant rules:\n";
758-
for (const auto &pair : RedundantRules) {
759-
const auto &rule = getRule(pair.first);
760-
llvm::dbgs() << "- " << rule << " ::== ";
761-
762-
MutableTerm lhs(rule.getLHS());
763-
pair.second.dump(llvm::dbgs(), lhs, *this);
764-
765-
llvm::dbgs() << "\n";
766-
767-
if (Debug.contains(DebugFlags::RedundantRulesDetail)) {
768-
llvm::dbgs() << "\n";
769-
pair.second.dumpLong(llvm::dbgs(), lhs, *this);
770-
771-
llvm::dbgs() << "\n\n";
772-
}
773-
}
774-
}
792+
normalizeRedundantRules();
775793
}
776794

777795
/// In a conformance-valid rewrite system, any rule with unresolved symbols on
Lines changed: 241 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,241 @@
1+
//===--- LeftCanonicalForm.cpp - Left canonical form of a rewrite path ----===//
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+
// Algorithm for reducing a rewrite path to left-canonical form:
14+
//
15+
// - Adjacent steps that are inverses of each other cancel out. for example
16+
// these two steps will be eliminated:
17+
//
18+
// (A => B) ⊗ (B => A)
19+
//
20+
// - Interchange law moves rewrites "to the left", for example
21+
//
22+
// X.(U => V) ⊗ (X => Y).V
23+
//
24+
// becomes
25+
//
26+
// (X => Y).U ⊗ Y.(U => V)
27+
//
28+
// These two transformations are iterated until fixed point to produce a
29+
// equivalent rewrite path that is simpler.
30+
//
31+
// From "Homotopy reduction systems for monoid presentations",
32+
// https://www.sciencedirect.com/science/article/pii/S0022404997000959
33+
//
34+
//===----------------------------------------------------------------------===//
35+
36+
#include "RewriteLoop.h"
37+
#include "RewriteSystem.h"
38+
#include "llvm/ADT/SmallVector.h"
39+
#include <utility>
40+
41+
using namespace swift;
42+
using namespace rewriting;
43+
44+
/// Returns true if this rewrite step is an inverse of \p other
45+
/// (and vice versa).
46+
bool RewriteStep::isInverseOf(const RewriteStep &other) const {
47+
if (Kind != other.Kind)
48+
return false;
49+
50+
if (StartOffset != other.StartOffset)
51+
return false;
52+
53+
if (Inverse != !other.Inverse)
54+
return false;
55+
56+
switch (Kind) {
57+
case RewriteStep::Rule:
58+
return Arg == other.Arg;
59+
60+
default:
61+
return false;
62+
}
63+
64+
assert(EndOffset == other.EndOffset && "Bad whiskering?");
65+
return true;
66+
}
67+
68+
bool RewriteStep::maybeSwapRewriteSteps(RewriteStep &other,
69+
const RewriteSystem &system) {
70+
if (Kind != RewriteStep::Rule ||
71+
other.Kind != RewriteStep::Rule)
72+
return false;
73+
74+
// Two rewrite steps are _orthogonal_ if they rewrite disjoint subterms
75+
// in context. Orthogonal rewrite steps commute, so we can canonicalize
76+
// a path by placing the left-most step first.
77+
//
78+
// Eg, A.U.B.(X => Y).C ⊗ A.(U => V).B.Y == A.(U => V).B.X ⊗ A.V.B.(X => Y).
79+
//
80+
// Or, in diagram form. We want to turn this:
81+
//
82+
// ----- time ----->
83+
// +---------+---------+
84+
// | A | A |
85+
// +---------+---------+
86+
// | U | U ==> V |
87+
// +---------+---------+
88+
// | B | B |
89+
// +---------+---------+
90+
// | X ==> Y | Y |
91+
// +---------+---------+
92+
// | C | C |
93+
// +---------+---------+
94+
//
95+
// Into this:
96+
//
97+
// +---------+---------+
98+
// | A | A |
99+
// +---------+---------+
100+
// | U ==> V | V |
101+
// +---------+---------+
102+
// | B | B |
103+
// +---------+---------+
104+
// | X | X ==> Y |
105+
// +---------+---------+
106+
// | C | C |
107+
// +---------+---------+
108+
//
109+
// Note that
110+
//
111+
// StartOffset == |A|+|U|+|B|
112+
// EndOffset = |C|
113+
//
114+
// other.StartOffset = |A|
115+
// other.EndOffset = |B|+|Y|+|C|
116+
//
117+
// After interchange, we adjust:
118+
//
119+
// StartOffset = |A|
120+
// EndOffset = |B|+|X|+|C|
121+
//
122+
// other.StartOffset = |A|+|V|+|B|
123+
// other.EndOffset = |C|
124+
125+
const auto &rule = system.getRule(Arg);
126+
auto lhs = (Inverse ? rule.getRHS() : rule.getLHS());
127+
auto rhs = (Inverse ? rule.getLHS() : rule.getRHS());
128+
129+
const auto &otherRule = system.getRule(other.Arg);
130+
auto otherLHS = (other.Inverse ? otherRule.getRHS() : otherRule.getLHS());
131+
auto otherRHS = (other.Inverse ? otherRule.getLHS() : otherRule.getRHS());
132+
133+
if (StartOffset < other.StartOffset + otherLHS.size())
134+
return false;
135+
136+
std::swap(*this, other);
137+
EndOffset += (lhs.size() - rhs.size());
138+
other.StartOffset += (otherRHS.size() - otherLHS.size());
139+
140+
return true;
141+
}
142+
143+
/// Cancels out adjacent rewrite steps that are inverses of each other.
144+
/// This does not change either endpoint of the path, and the path does
145+
/// not necessarily need to be a loop.
146+
bool RewritePath::computeFreelyReducedForm() {
147+
SmallVector<RewriteStep, 4> newSteps;
148+
bool changed = false;
149+
150+
for (const auto &step : Steps) {
151+
if (!newSteps.empty() &&
152+
newSteps.back().isInverseOf(step)) {
153+
changed = true;
154+
newSteps.pop_back();
155+
continue;
156+
}
157+
158+
newSteps.push_back(step);
159+
}
160+
161+
if (!changed)
162+
return false;
163+
std::swap(newSteps, Steps);
164+
return changed;
165+
}
166+
167+
/// Apply the interchange rule until fixed point (see maybeSwapRewriteSteps()).
168+
bool RewritePath::computeLeftCanonicalForm(const RewriteSystem &system) {
169+
bool changed = false;
170+
171+
for (unsigned i = 1, e = Steps.size(); i < e; ++i) {
172+
auto &prevStep = Steps[i - 1];
173+
auto &step = Steps[i];
174+
175+
if (prevStep.maybeSwapRewriteSteps(step, system))
176+
changed = true;
177+
}
178+
179+
return changed;
180+
}
181+
182+
/// Compute freely-reduced left-canonical normal form of a path.
183+
void RewritePath::computeNormalForm(const RewriteSystem &system) {
184+
// FIXME: This can be more efficient.
185+
bool changed;
186+
do {
187+
changed = false;
188+
changed |= computeFreelyReducedForm();
189+
changed |= computeLeftCanonicalForm(system);
190+
} while (changed);
191+
}
192+
193+
/// Given a path that is a loop around the given basepoint, cancels out
194+
/// pairs of terms from the ends that are inverses of each other, applying
195+
/// the corresponding translation to the basepoint.
196+
///
197+
/// For example, consider this loop with basepoint 'X':
198+
///
199+
/// (X => Y.A) * (Y.A => Y.B) * Y.(B => A) * (Y.A => X)
200+
///
201+
/// The first step is the inverse of the last step, so the cyclic
202+
/// reduction is the loop (Y.A => Y.B) * Y.(B => A), with a new
203+
/// basepoint 'Y.A'.
204+
bool RewritePath::computeCyclicallyReducedForm(MutableTerm &basepoint,
205+
const RewriteSystem &system) {
206+
RewritePathEvaluator evaluator(basepoint);
207+
unsigned count = 0;
208+
209+
while (2 * count + 1 < size()) {
210+
auto left = Steps[count];
211+
auto right = Steps[Steps.size() - count - 1];
212+
if (!left.isInverseOf(right))
213+
break;
214+
215+
// Update the basepoint by applying the first step in the path.
216+
evaluator.apply(left, system);
217+
218+
++count;
219+
}
220+
221+
std::rotate(Steps.begin(), Steps.begin() + count, Steps.end() - count);
222+
Steps.erase(Steps.end() - 2 * count, Steps.end());
223+
224+
basepoint = evaluator.getCurrentTerm();
225+
return count > 0;
226+
}
227+
228+
/// Compute cyclically-reduced left-canonical normal form of a loop.
229+
void RewriteLoop::computeNormalForm(const RewriteSystem &system) {
230+
// FIXME: This can be more efficient.
231+
bool changed;
232+
do {
233+
changed = false;
234+
changed |= Path.computeFreelyReducedForm();
235+
changed |= Path.computeCyclicallyReducedForm(Basepoint, system);
236+
changed |= Path.computeLeftCanonicalForm(system);
237+
238+
if (changed)
239+
markDirty();
240+
} while (changed);
241+
}

lib/AST/RequirementMachine/RewriteLoop.h

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -355,6 +355,11 @@ struct RewriteStep {
355355
RewritePathEvaluator &evaluator,
356356
const RewriteSystem &system) const;
357357

358+
bool isInverseOf(const RewriteStep &other) const;
359+
360+
bool maybeSwapRewriteSteps(RewriteStep &other,
361+
const RewriteSystem &system);
362+
358363
private:
359364
static unsigned getConcreteProjectionArg(unsigned differenceID,
360365
unsigned substitutionIndex) {
@@ -406,6 +411,15 @@ class RewritePath {
406411

407412
void invert();
408413

414+
bool computeFreelyReducedForm();
415+
416+
bool computeCyclicallyReducedForm(MutableTerm &basepoint,
417+
const RewriteSystem &system);
418+
419+
bool computeLeftCanonicalForm(const RewriteSystem &system);
420+
421+
void computeNormalForm(const RewriteSystem &system);
422+
409423
void dump(llvm::raw_ostream &out,
410424
MutableTerm term,
411425
const RewriteSystem &system) const;
@@ -491,6 +505,8 @@ class RewriteLoop {
491505
ProtocolConformanceRules, 2> &result,
492506
const RewriteSystem &system) const;
493507

508+
void computeNormalForm(const RewriteSystem &system);
509+
494510
void verify(const RewriteSystem &system) const;
495511

496512
void dump(llvm::raw_ostream &out, const RewriteSystem &system) const;

lib/AST/RequirementMachine/RewriteSystem.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -627,7 +627,7 @@ void RewriteSystem::recordRewriteLoop(MutableTerm basepoint,
627627
if (!RecordLoops)
628628
return;
629629

630-
// Ignore the rewrite rule if it is not part of our minimization domain.
630+
// Ignore the rewrite loop if it is not part of our minimization domain.
631631
if (!isInMinimizationDomain(basepoint.getRootProtocol()))
632632
return;
633633

lib/AST/RequirementMachine/RewriteSystem.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -486,6 +486,8 @@ class RewriteSystem final {
486486
void computeMinimalConformances(
487487
llvm::DenseSet<unsigned> &redundantConformances);
488488

489+
void normalizeRedundantRules();
490+
489491
public:
490492
void recordRewriteLoop(MutableTerm basepoint,
491493
RewritePath path);

0 commit comments

Comments
 (0)