Skip to content

RequirementMachine: Wire up protocol requirement signature minimization #39629

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 8 commits into from
Oct 10, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions include/swift/Basic/LangOptions.h
Original file line number Diff line number Diff line change
Expand Up @@ -481,6 +481,11 @@ namespace swift {
/// algorithm.
unsigned RequirementMachineDepthLimit = 10;

/// Enable the new experimental protocol requirement signature minimization
/// algorithm.
RequirementMachineMode RequirementMachineProtocolSignatures =
RequirementMachineMode::Disabled;

/// Sets the target we are building for and updates platform conditions
/// to match.
///
Expand Down
4 changes: 4 additions & 0 deletions include/swift/Option/Options.td
Original file line number Diff line number Diff line change
Expand Up @@ -616,6 +616,10 @@ def requirement_machine_EQ : Joined<["-"], "requirement-machine=">,
Flags<[FrontendOption, ModuleInterfaceOption]>,
HelpText<"Control usage of experimental generics implementation: 'on', 'off', or 'verify'">;

def requirement_machine_protocol_signatures_EQ : Joined<["-"], "requirement-machine-protocol-signatures=">,
Flags<[FrontendOption]>,
HelpText<"Control usage of experimental protocol requirement signature minimization: 'on', 'off', or 'verify'">;

// Diagnostic control options
def suppress_warnings : Flag<["-"], "suppress-warnings">,
Flags<[FrontendOption]>,
Expand Down
6 changes: 1 addition & 5 deletions lib/AST/GenericSignature.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1483,11 +1483,7 @@ int Requirement::compare(const Requirement &other) const {
// We should only have multiple conformance requirements.
assert(getKind() == RequirementKind::Conformance);

int compareProtos =
TypeDecl::compare(getProtocolDecl(), other.getProtocolDecl());

assert(compareProtos != 0 && "Duplicate conformance requirement");
return compareProtos;
return TypeDecl::compare(getProtocolDecl(), other.getProtocolDecl());
}

/// Compare two associated types.
Expand Down
159 changes: 101 additions & 58 deletions lib/AST/RequirementMachine/GeneratingConformances.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,26 @@
// decompositions can be found for all "derived" conformance rules, producing
// a minimal set of generating conformances.
//
// There are two small complications to handle implementation details of
// Swift generics:
//
// 1) Inherited witness tables must be derivable by following other protocol
// refinement requirements only, without looking at non-Self associated
// types. This is expressed by saying that the generating conformance
// equations for a protocol refinement can only be written in terms of
// other protocol refinements; conformance paths involving non-Self
// associated types are not considered.
//
// 2) The subject type of each conformance requirement must be derivable at
// runtime as well, so for each generating conformance, it must be
// possible to write down a conformance path for the parent type without
// using any generating conformance recursively in the parent path of
// itself.
//
// The generating conformances finds fewer conformance requirements to be
// redundant than homotopy reduction, which is why homotopy reduction only
// deletes non-protocol conformance requirements.
//
//===----------------------------------------------------------------------===//

#include "swift/AST/Decl.h"
Expand Down Expand Up @@ -283,23 +303,48 @@ void RewriteSystem::computeCandidateConformancePaths(
llvm::dbgs() << "\n";
}

// Two conformance rules in empty context (T.[P] => T) and (T'.[P] => T)
// are interchangeable, and contribute a trivial pair of conformance
// equations expressing that each one can be written in terms of the
// other:
//
// (T.[P] => T) := (T'.[P])
// (T'.[P] => T') := (T.[P])
for (unsigned candidateRuleID : notInContext) {
for (unsigned otherRuleID : notInContext) {
if (otherRuleID == candidateRuleID)
continue;

SmallVector<unsigned, 2> path;
path.push_back(otherRuleID);
conformancePaths[candidateRuleID].push_back(path);
}
}

// Suppose a 3-cell contains a conformance rule (T.[P] => T) in an empty
// context, and a conformance rule (V.[P] => V) with a possibly non-empty
// left context U and empty right context.
// context, and a conformance rule (V.[P] => V) with a non-empty left
// context U.
//
// The 3-cell looks something like this:
//
// ... ⊗ (T.[P] => T) ⊗ ... ⊗ U.(V => V.[P]) ⊗ ...
// ^ ^
// | |
// + basepoint ========================= basepoint +
//
// We can decompose U into a product of conformance rules:
//
// (V1.[P1] => V1)...(Vn.[Pn] => Vn),
//
// Note that (V1)...(Vn) is canonically equivalent to U.
//
// Now, we can record a candidate decomposition of (T.[P] => T) as a
// product of conformance rules:
//
// (T.[P] => T) := (V1.[P1] => V1)...(Vn.[Pn] => Vn).(V.[P] => V)
//
// Now if U is empty, this becomes the trivial candidate:
//
// (T.[P] => T) := (V.[P] => V)
SmallVector<SmallVector<unsigned, 2>, 2> candidatePaths;
// Again, note that (V1)...(Vn).V is canonically equivalent to U.V,
// and therefore T.
for (auto pair : inContext) {
// We have a term U, and a rule V.[P] => V.
SmallVector<unsigned, 2> conformancePath;
Expand All @@ -313,26 +358,10 @@ void RewriteSystem::computeCandidateConformancePaths(
decomposeTermIntoConformanceRuleLeftHandSides(term, pair.second,
conformancePath);

candidatePaths.push_back(conformancePath);
}

for (unsigned candidateRuleID : notInContext) {
// If multiple conformance rules appear in an empty context, each one
// can be replaced with any other conformance rule.
for (unsigned otherRuleID : notInContext) {
if (otherRuleID == candidateRuleID)
continue;

SmallVector<unsigned, 2> path;
path.push_back(otherRuleID);
conformancePaths[candidateRuleID].push_back(path);
}

// If conformance rules appear in non-empty context, they define a
// conformance access path for each conformance rule in empty context.
for (const auto &path : candidatePaths) {
conformancePaths[candidateRuleID].push_back(path);
}
// This decomposition defines a conformance access path for each
// conformance rule we saw in empty context.
for (unsigned otherRuleID : notInContext)
conformancePaths[otherRuleID].push_back(conformancePath);
}
}
}
Expand Down Expand Up @@ -492,6 +521,48 @@ void RewriteSystem::verifyGeneratingConformanceEquations(
#endif
}

static const ProtocolDecl *getParentConformanceForTerm(Term lhs) {
// The last element is a protocol symbol, because this is the left hand side
// of a conformance rule.
assert(lhs.back().getKind() == Symbol::Kind::Protocol);

// The second to last symbol is either an associated type, protocol or generic
// parameter symbol.
assert(lhs.size() >= 2);

auto parentSymbol = lhs[lhs.size() - 2];

switch (parentSymbol.getKind()) {
case Symbol::Kind::AssociatedType: {
// In a conformance rule of the form [P:T].[Q] => [P:T], the parent type is
// trivial.
if (lhs.size() == 2)
return nullptr;

// If we have a rule of the form X.[P:Y].[Q] => X.[P:Y] wih non-empty X,
// then the parent type is X.[P].
const auto protos = parentSymbol.getProtocols();
assert(protos.size() == 1);

return protos[0];
}

case Symbol::Kind::GenericParam:
case Symbol::Kind::Protocol:
// The parent type is trivial (either a generic parameter, or the protocol
// 'Self' type).
return nullptr;

case Symbol::Kind::Name:
case Symbol::Kind::Layout:
case Symbol::Kind::Superclass:
case Symbol::Kind::ConcreteType:
break;
}

llvm_unreachable("Bad symbol kind");
}

/// Computes a minimal set of generating conformances, assuming that homotopy
/// reduction has already eliminated all redundant rewrite rules that are not
/// conformance rules.
Expand Down Expand Up @@ -536,49 +607,21 @@ void RewriteSystem::computeGeneratingConformances(

auto lhs = rule.getLHS();

auto parentSymbol = lhs[lhs.size() - 2];

// The last element is a protocol symbol, because this is a conformance rule.
// The second to last symbol is either an associated type, protocol or generic
// parameter symbol.
switch (parentSymbol.getKind()) {
case Symbol::Kind::AssociatedType: {
// If we have a rule of the form X.[P:Y].[Q] => X.[P:Y] wih non-empty X,
// then the parent type is X.[P].
if (lhs.size() == 2)
continue;

// Record a parent path if the subject type itself requires a non-trivial
// conformance path to derive.
if (auto *parentProto = getParentConformanceForTerm(lhs)) {
MutableTerm mutTerm(lhs.begin(), lhs.end() - 2);
assert(!mutTerm.empty());

const auto protos = parentSymbol.getProtocols();
assert(protos.size() == 1);

bool simplified = simplify(mutTerm);
assert(!simplified || rule.isSimplified());
(void) simplified;

mutTerm.add(Symbol::forProtocol(protos[0], Context));
mutTerm.add(Symbol::forProtocol(parentProto, Context));

// Get a conformance path for X.[P] and record it.
decomposeTermIntoConformanceRuleLeftHandSides(mutTerm, parentPaths[ruleID]);
continue;
}

case Symbol::Kind::GenericParam:
case Symbol::Kind::Protocol:
// Don't record a parent path, since the parent type is trivial (either a
// generic parameter, or the protocol 'Self' type).
continue;

case Symbol::Kind::Name:
case Symbol::Kind::Layout:
case Symbol::Kind::Superclass:
case Symbol::Kind::ConcreteType:
break;
}

llvm_unreachable("Bad symbol kind");
}

computeCandidateConformancePaths(conformancePaths);
Expand Down
2 changes: 1 addition & 1 deletion lib/AST/RequirementMachine/HomotopyReduction.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -751,7 +751,7 @@ void RewriteSystem::performHomotopyReduction(
redundantConformances,
replacementPath);

// If there no redundant rules remain in this pass, stop.
// If no redundant rules remain which can be eliminated by this pass, stop.
if (!optRuleID)
return;

Expand Down
14 changes: 9 additions & 5 deletions lib/AST/RequirementMachine/ProtocolGraph.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ using namespace rewriting;
void ProtocolGraph::visitRequirements(ArrayRef<Requirement> reqs) {
for (auto req : reqs) {
if (req.getKind() == RequirementKind::Conformance) {
addProtocol(req.getProtocolDecl());
addProtocol(req.getProtocolDecl(), /*initialComponent=*/false);
}
}
}
Expand All @@ -32,7 +32,7 @@ void ProtocolGraph::visitRequirements(ArrayRef<Requirement> reqs) {
/// \p protos.
void ProtocolGraph::visitProtocols(ArrayRef<const ProtocolDecl *> protos) {
for (auto proto : protos) {
addProtocol(proto);
addProtocol(proto, /*initialComponent=*/true);
}
}

Expand All @@ -50,13 +50,15 @@ const ProtocolInfo &ProtocolGraph::getProtocolInfo(
}

/// Record information about a protocol if we have no seen it yet.
void ProtocolGraph::addProtocol(const ProtocolDecl *proto) {
void ProtocolGraph::addProtocol(const ProtocolDecl *proto,
bool initialComponent) {
if (Info.count(proto) > 0)
return;

Info[proto] = {proto->getInheritedProtocols(),
proto->getAssociatedTypeMembers(),
proto->getRequirementSignature()};
proto->getProtocolDependencies(),
initialComponent};
Protocols.push_back(proto);
}

Expand All @@ -66,7 +68,9 @@ void ProtocolGraph::computeTransitiveClosure() {
unsigned i = 0;
while (i < Protocols.size()) {
auto *proto = Protocols[i++];
visitRequirements(getProtocolInfo(proto).Requirements);
for (auto *proto : getProtocolInfo(proto).Dependencies) {
addProtocol(proto, /*initialComponent=*/false);
}
}
}

Expand Down
22 changes: 15 additions & 7 deletions lib/AST/RequirementMachine/ProtocolGraph.h
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,8 @@ struct ProtocolInfo {
/// ProtocolGraph::computeInheritedAssociatedTypes().
llvm::TinyPtrVector<AssociatedTypeDecl *> InheritedAssociatedTypes;

/// The protocol's requirement signature.
ArrayRef<Requirement> Requirements;
/// The protocol's dependencies.
ArrayRef<ProtocolDecl *> Dependencies;

/// Used by ProtocolGraph::computeProtocolDepth() to detect circularity.
unsigned Mark : 1;
Expand All @@ -55,27 +55,34 @@ struct ProtocolInfo {

/// Index of the protocol in the linear order. Computed by
/// ProtocolGraph::computeLinearOrder().
unsigned Index : 32;
unsigned Index : 31;

/// When building a protocol requirement signature, the initial set of
/// protocols are marked with this bit.
unsigned InitialComponent : 1;

ProtocolInfo() {
Mark = 0;
Depth = 0;
Index = 0;
InitialComponent = 0;
}

ProtocolInfo(ArrayRef<ProtocolDecl *> inherited,
ArrayRef<AssociatedTypeDecl *> &&types,
ArrayRef<Requirement> reqs)
ArrayRef<ProtocolDecl *> deps,
bool initialComponent)
: Inherited(inherited),
AssociatedTypes(types),
Requirements(reqs) {
Dependencies(deps) {
Mark = 0;
Depth = 0;
Index = 0;
InitialComponent = initialComponent;
}
};

/// Stores cached information about all protocols transtively
/// Stores cached information about all protocols transitively
/// referenced from a set of generic requirements.
///
/// Out-of-line methods are documented in ProtocolGraph.cpp.
Expand All @@ -101,7 +108,8 @@ class ProtocolGraph {
const ProtocolDecl *proto) const;

private:
void addProtocol(const ProtocolDecl *proto);
void addProtocol(const ProtocolDecl *proto,
bool initialComponent);
void computeTransitiveClosure();
void computeLinearOrder();
void computeInheritedAssociatedTypes();
Expand Down
Loading