Skip to content

Synthesize ==/hashValue for tuple fields/payloads #12598

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

Closed
wants to merge 3 commits into from
Closed
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
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ Swift 4.1

* [SE-0185][]

Structs and enums that declare a conformance to `Equatable`/`Hashable` now get an automatically synthesized implementation of `==`/`hashValue`. For structs, all stored properties must be `Equatable`/`Hashable`. For enums, all enum cases with associated values must be `Equatable`/`Hashable`.
Structs and enums that declare a conformance to `Equatable`/`Hashable` now get an automatically synthesized implementation of `==`/`hashValue`. For structs, all stored properties must either be `Equatable`/`Hashable` or tuples where all elements are `Equatable`/`Hashable`. For enums, all enum cases with associated values must be `Equatable`/`Hashable` or tuples where all elements are `Equatable`/`Hashable`.

```swift
public struct Point: Hashable {
Expand Down
135 changes: 100 additions & 35 deletions lib/Sema/DerivedConformanceEquatableHashable.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -32,13 +32,39 @@
using namespace swift;
using namespace DerivedConformance;

/// Returns true if we can synthesize the protocol's requirements for the given
/// type. This is trivially true if the type directly conforms to the protocol,
/// but we also support tuples where all elements conform to the protocol. We do
/// not attempt to do so recursively (for example, tuples of tuples), due to the
/// complexity involved in synthesizing such requirements.
static bool typeCanSynthesizeProtocol(TypeChecker &tc, Type type,
ProtocolDecl *protocol,
DeclContext *declContext) {
// If the type is a tuple, then we can synthesize P if all of its elements
// conform to P.
if (auto tupleType = type->getAs<TupleType>()) {
for (auto tupleElementType : tupleType->getElementTypes()) {
if (!tc.conformsToProtocol(tupleElementType, protocol, declContext,
ConformanceCheckFlags::Used)) {
return false;
}
}
return true;
}

// Otherwise, return whether or not the type conforms to the protocol.
return (bool) tc.conformsToProtocol(type, protocol, declContext,
ConformanceCheckFlags::Used);
}

/// Returns true if, for every element of the given enum, it either has no
/// associated values or all of them conform to a protocol.
/// \p theEnum The enum whose elements and associated values should be checked.
/// \p protocol The protocol being requested.
/// \return True if all associated values of all elements of the enum conform.
bool allAssociatedValuesConformToProtocol(TypeChecker &tc, EnumDecl *theEnum,
ProtocolDecl *protocol) {
static bool allAssociatedValuesConformToProtocol(TypeChecker &tc,
EnumDecl *theEnum,
ProtocolDecl *protocol) {
auto declContext = theEnum->getDeclContext();

for (auto elt : theEnum->getAllElements()) {
Expand All @@ -53,16 +79,15 @@ bool allAssociatedValuesConformToProtocol(TypeChecker &tc, EnumDecl *theEnum,
// One associated value with a label or multiple associated values
// (labeled or unlabeled) are tuple types.
for (auto tupleElementType : tupleType->getElementTypes()) {
if (!tc.conformsToProtocol(tupleElementType, protocol, declContext,
ConformanceCheckFlags::Used)) {
if (!typeCanSynthesizeProtocol(tc, tupleElementType, protocol,
declContext)) {
return false;
}
}
} else {
// One associated value with no label is represented as a paren type.
auto actualType = argumentType->getWithoutParens();
if (!tc.conformsToProtocol(actualType, protocol, declContext,
ConformanceCheckFlags::Used)) {
if (!typeCanSynthesizeProtocol(tc, actualType, protocol, declContext)) {
return false;
}
}
Expand All @@ -75,9 +100,9 @@ bool allAssociatedValuesConformToProtocol(TypeChecker &tc, EnumDecl *theEnum,
/// \p theStruct The struct whose stored properties should be checked.
/// \p protocol The protocol being requested.
/// \return True if all stored properties of the struct conform.
bool allStoredPropertiesConformToProtocol(TypeChecker &tc,
StructDecl *theStruct,
ProtocolDecl *protocol) {
static bool allStoredPropertiesConformToProtocol(TypeChecker &tc,
StructDecl *theStruct,
ProtocolDecl *protocol) {
auto declContext = theStruct->getDeclContext();

auto storedProperties =
Expand All @@ -87,8 +112,8 @@ bool allStoredPropertiesConformToProtocol(TypeChecker &tc,
tc.validateDecl(propertyDecl);

if (!propertyDecl->hasType() ||
!tc.conformsToProtocol(propertyDecl->getType(), protocol, declContext,
ConformanceCheckFlags::Used)) {
!typeCanSynthesizeProtocol(tc, propertyDecl->getType(), protocol,
declContext)) {
return false;
}
}
Expand Down Expand Up @@ -283,24 +308,44 @@ static DeclRefExpr *convertEnumToIndex(SmallVectorImpl<ASTNode> &stmts,
}

/// Generates a guard statement that checks whether the given lhs and rhs
/// variables are equal; if they are not, then the isEqual variable is set to
/// false and a break statement is executed.
/// variables are equal; if they are not, then the body of the guard returns
/// false.
/// \p C The AST context.
/// \p lhsVar The first variable to test for equality.
/// \p rhsVar The second variable to test for equality.
/// \p isEqualVar The variable to set to false if the guard condition fails.
static GuardStmt *returnIfNotEqualGuard(ASTContext &C,
Expr *lhsExpr,
Expr *rhsExpr) {
static void appendReturnIfNotEqualGuard(
ASTContext &C, Expr *lhsExpr, Expr *rhsExpr, Type type,
SmallVectorImpl<ASTNode> &statements) {
// We can generate a single equality testing statement for types conforming to
// Equatable and for tuples of arity <= 6 where all elements conform to
// Equatable (because the standard library provides hand-coded == impls for
// these). For larger tuples, we must manually generate tests for each
// element.
if (auto tupleType = type->getAs<TupleType>()) {
if (tupleType->getNumElements() > 6) {
for (unsigned i = 0; i < tupleType->getNumElements(); ++i) {
auto tupleElt = tupleType->getElement(i);
auto tupleEltType = tupleElt.getType();
auto lhsEltExpr = new (C) TupleElementExpr(lhsExpr, SourceLoc(), i,
SourceLoc(), tupleEltType);
lhsEltExpr->setImplicit(true);
auto rhsEltExpr = new (C) TupleElementExpr(rhsExpr, SourceLoc(), i,
SourceLoc(), tupleEltType);
rhsEltExpr->setImplicit(true);
appendReturnIfNotEqualGuard(C, lhsEltExpr, rhsEltExpr, tupleEltType,
statements);
}
return;
}
}

SmallVector<StmtConditionElement, 1> conditions;
SmallVector<ASTNode, 2> statements;

// First, generate the statements for the body of the guard.
// return false
auto falseExpr = new (C) BooleanLiteralExpr(false, SourceLoc(),
/*Implicit*/true);
auto returnStmt = new (C) ReturnStmt(SourceLoc(), falseExpr);
statements.emplace_back(ASTNode(returnStmt));

// Next, generate the condition being checked.
// lhs == rhs
Expand All @@ -316,10 +361,12 @@ static GuardStmt *returnIfNotEqualGuard(ASTContext &C,
/*Implicit*/true);
conditions.emplace_back(cmpExpr);

// Build and return the complete guard statement.
// Build and append the complete guard statement.
// guard lhs == rhs else { return false }
auto body = BraceStmt::create(C, SourceLoc(), statements, SourceLoc());
return new (C) GuardStmt(SourceLoc(), C.AllocateCopy(conditions), body);
auto body = BraceStmt::create(C, SourceLoc(), { returnStmt }, SourceLoc());
auto guardStmt =
new (C) GuardStmt(SourceLoc(), C.AllocateCopy(conditions), body);
statements.emplace_back(guardStmt);
}

/// Derive the body for an '==' operator for an enum that has no associated
Expand Down Expand Up @@ -445,8 +492,8 @@ deriveBodyEquatable_enum_hasAssociatedValues_eq(AbstractFunctionDecl *eqDecl) {
auto rhsVar = rhsPayloadVars[varIdx];
auto rhsExpr = new (C) DeclRefExpr(rhsVar, DeclNameLoc(),
/*Implicit*/true);
auto guardStmt = returnIfNotEqualGuard(C, lhsExpr, rhsExpr);
statementsInCase.emplace_back(guardStmt);
appendReturnIfNotEqualGuard(
C, lhsExpr, rhsExpr, lhsVar->getType(), statementsInCase);
}

// If none of the guard statements caused an early exit, then all the pairs
Expand Down Expand Up @@ -529,8 +576,8 @@ static void deriveBodyEquatable_struct_eq(AbstractFunctionDecl *eqDecl) {
auto bPropertyExpr = new (C) DotSyntaxCallExpr(bPropertyRef, SourceLoc(),
bParamRef);

auto guardStmt = returnIfNotEqualGuard(C, aPropertyExpr, bPropertyExpr);
statements.emplace_back(guardStmt);
appendReturnIfNotEqualGuard(C, aPropertyExpr, bPropertyExpr,
propertyDecl->getType(), statements);
}

// If none of the guard statements caused an early exit, then all the pairs
Expand Down Expand Up @@ -750,10 +797,28 @@ static Expr* integerLiteralExpr(ASTContext &C, int64_t value) {
/// \p C The AST context.
/// \p resultVar The variable into which the hash value will be mixed.
/// \p exprToHash The expression whose hash value should be mixed in.
/// \return The expression that mixes the hash value into the result variable.
static Expr* mixInHashExpr_hashValue(ASTContext &C,
VarDecl* resultVar,
Expr *exprToHash) {
/// \p type The type of the expression.
/// \p expressions A vector to which the function will append the expression
/// that mixes the hash value into the result variable.
static void appendMixInHashExpr_hashValue(
ASTContext &C, VarDecl *resultVar, Expr *exprToHash, Type type,
SmallVectorImpl<ASTNode> &expressions) {
// We can generate a single hashValue expression for anything that directly
// conforms to Hashable. For tuples, we have to loop over the elements and mix
// in their hash values.
if (auto tupleType = type->getAs<TupleType>()) {
for (unsigned i = 0; i < tupleType->getNumElements(); ++i) {
auto tupleElt = tupleType->getElement(i);
auto tupleEltType = tupleElt.getType();
auto tupleEltExpr = new (C) TupleElementExpr(exprToHash, SourceLoc(), i,
SourceLoc(), tupleEltType);
tupleEltExpr->setImplicit(true);
appendMixInHashExpr_hashValue(C, resultVar, tupleEltExpr, tupleEltType,
expressions);
}
return;
}

// <exprToHash>.hashValue
auto hashValueExpr = new (C) UnresolvedDotExpr(exprToHash, SourceLoc(),
C.Id_hashValue, DeclNameLoc(),
Expand All @@ -773,7 +838,7 @@ static Expr* mixInHashExpr_hashValue(ASTContext &C,
/*implicit*/ true);
auto assignExpr = new (C) AssignExpr(lhsResultExpr, SourceLoc(),
mixinResultExpr, /*implicit*/ true);
return assignExpr;
expressions.emplace_back(assignExpr);
}

/// Returns a new assignment expression that invokes _mixInt on a variable and
Expand Down Expand Up @@ -874,8 +939,8 @@ deriveBodyHashable_enum_hashValue(AbstractFunctionDecl *hashValueDecl) {
auto payloadVarRef = new (C) DeclRefExpr(payloadVar, DeclNameLoc(),
/*implicit*/ true);
// result = _mixForSynthesizedHashValue(result, <payloadVar>.hashValue)
auto mixExpr = mixInHashExpr_hashValue(C, resultVar, payloadVarRef);
mixExpressions.emplace_back(ASTNode(mixExpr));
appendMixInHashExpr_hashValue(C, resultVar, payloadVarRef,
payloadVar->getType(), mixExpressions);
}

// result = _mixInt(result)
Expand Down Expand Up @@ -963,8 +1028,8 @@ deriveBodyHashable_struct_hashValue(AbstractFunctionDecl *hashValueDecl) {
auto selfPropertyExpr = new (C) DotSyntaxCallExpr(propertyRef, SourceLoc(),
selfRef);
// result = _mixForSynthesizedHashValue(result, <property>.hashValue)
auto mixExpr = mixInHashExpr_hashValue(C, resultVar, selfPropertyExpr);
statements.emplace_back(ASTNode(mixExpr));
appendMixInHashExpr_hashValue(C, resultVar, selfPropertyExpr,
propertyDecl->getType(), statements);
}

{
Expand Down
32 changes: 32 additions & 0 deletions test/Interpreter/enum_equatable_hashable_correctness.swift
Original file line number Diff line number Diff line change
Expand Up @@ -93,4 +93,36 @@ EnumSynthesisTests.test("IndirectEquatability/Hashability") {
checkHashable([one, two, three, four], equalityOracle: { $0 == $1 })
}

// Test the synthesized members when the enum contains tuples of various
// arities.
enum HasTuple6: Hashable {
case v((Int, Int, Int, Int, Int, Int))
}
enum HasTuple7: Hashable {
case v((a: Int, b: Int, c: Int, d: Int, e: Int, f: Int, g: Int))
}

EnumSynthesisTests.test("TupleEquatability/Hashability") {
checkHashable([
HasTuple6.v((1, 2, 3, 4, 5, 6)), .v((1, 2, 3, 4, 5, 7)),
], equalityOracle: { $0 == $1 })

checkHashable([
HasTuple7.v((a: 1, b: 2, c: 3, d: 4, e: 5, f: 6, g: 7)),
.v((a: 1, b: 2, c: 3, d: 4, e: 5, f: 6, g: 8)),
], equalityOracle: { $0 == $1 })
}

EnumSynthesisTests.test("CloseTupleValuesDoNotCollide") {
expectNotEqual(
HasTuple6.v((1, 2, 3, 4, 5, 6)).hashValue,
HasTuple6.v((1, 2, 3, 4, 5, 7)).hashValue
)

expectNotEqual(
HasTuple7.v((a: 1, b: 2, c: 3, d: 4, e: 5, f: 6, g: 7)).hashValue,
HasTuple7.v((a: 1, b: 2, c: 3, d: 4, e: 5, f: 6, g: 8)).hashValue
)
}

runAllTests()
33 changes: 33 additions & 0 deletions test/Interpreter/struct_equatable_hashable_correctness.swift
Original file line number Diff line number Diff line change
Expand Up @@ -67,4 +67,37 @@ StructSynthesisTests.test("ExplicitOverridesSynthesizedInExtension") {
expectEqual(OverridesInExtension(a: 4).hashValue, 2)
}

// Test the synthesized members when the struct contains tuples of various
// arities.
struct HasTuple6: Hashable {
let v: (Int, Int, Int, Int, Int, Int)
}
struct HasTuple7: Hashable {
let v: (a: Int, b: Int, c: Int, d: Int, e: Int, f: Int, g: Int)
}

StructSynthesisTests.test("TupleEquatability/Hashability") {
checkHashable([
HasTuple6(v: (1, 2, 3, 4, 5, 6)),
HasTuple6(v: (1, 2, 3, 4, 5, 7)),
], equalityOracle: { $0 == $1 })

checkHashable([
HasTuple7(v: (a: 1, b: 2, c: 3, d: 4, e: 5, f: 6, g: 7)),
HasTuple7(v: (a: 1, b: 2, c: 3, d: 4, e: 5, f: 6, g: 8)),
], equalityOracle: { $0 == $1 })
}

StructSynthesisTests.test("CloseTupleValuesDoNotCollide") {
expectNotEqual(
HasTuple6(v: (1, 2, 3, 4, 5, 6)).hashValue,
HasTuple6(v: (1, 2, 3, 4, 5, 7)).hashValue
)

expectNotEqual(
HasTuple7(v: (a: 1, b: 2, c: 3, d: 4, e: 5, f: 6, g: 7)).hashValue,
HasTuple7(v: (a: 1, b: 2, c: 3, d: 4, e: 5, f: 6, g: 8)).hashValue
)
}

runAllTests()
24 changes: 20 additions & 4 deletions test/Sema/enum_equatable_hashable.swift
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ enum CustomHashable {

var hashValue: Int { return 0 }
}
func ==(x: CustomHashable, y: CustomHashable) -> Bool { // expected-note 3 {{non-matching type}}
func ==(x: CustomHashable, y: CustomHashable) -> Bool { // expected-note 4 {{non-matching type}}
return true
}

Expand All @@ -50,7 +50,7 @@ enum InvalidCustomHashable {

var hashValue: String { return "" } // expected-note{{previously declared here}}
}
func ==(x: InvalidCustomHashable, y: InvalidCustomHashable) -> String { // expected-note 3 {{non-matching type}}
func ==(x: InvalidCustomHashable, y: InvalidCustomHashable) -> String { // expected-note 4 {{non-matching type}}
return ""
}
if InvalidCustomHashable.A == .B { }
Expand Down Expand Up @@ -172,7 +172,7 @@ public enum Medicine {

extension Medicine : Equatable {}

public func ==(lhs: Medicine, rhs: Medicine) -> Bool { // expected-note 2 {{non-matching type}}
public func ==(lhs: Medicine, rhs: Medicine) -> Bool { // expected-note 3 {{non-matching type}}
return true
}

Expand All @@ -189,7 +189,7 @@ extension NotExplicitlyHashableAndCannotDerive : Hashable {} // expected-error 2
// Verify that conformance (albeit manually implemented) can still be added to
// a type in a different file.
extension OtherFileNonconforming: Hashable {
static func ==(lhs: OtherFileNonconforming, rhs: OtherFileNonconforming) -> Bool { // expected-note 2 {{non-matching type}}
static func ==(lhs: OtherFileNonconforming, rhs: OtherFileNonconforming) -> Bool { // expected-note 3 {{non-matching type}}
return true
}
var hashValue: Int { return 0 }
Expand Down Expand Up @@ -226,6 +226,22 @@ indirect enum TotallyIndirect: Hashable {
case end(Int)
}

// Verify the expected results for a few tuple cases. The arity-7 case is
// important because the stdlib's built-in == operators end at 6 and we want to
// make sure we can handle higher ones.
enum SatisfyingTuple0: Hashable {
case value(payload: ())
}
enum SatisfyingTuple6: Hashable {
case value(payload: (Int, Int, Int, Int, Int, Int))
}
enum SatisfyingTuple7: Hashable {
case value(payload: (Int, Int, Int, Int, Int, Int, Int))
}
enum NotSatisfyingTuple: Hashable { // expected-error 2 {{does not conform}}
case value(payload: (Int, Int, NotHashable, Int, Int, Int))
}

// FIXME: Remove -verify-ignore-unknown.
// <unknown>:0: error: unexpected error produced: invalid redeclaration of 'hashValue'
// <unknown>:0: error: unexpected note produced: candidate has non-matching type '(Foo, Foo) -> Bool'
Expand Down
Loading