Skip to content

Commit e8cdd74

Browse files
committed
Verify that hash/equality behavior is all consistent
It's not obvious that we can check that hash/equality behavior is entirely correct, since there are two very different behaviors which depend on environmental factors that are not easy to test for. But we can do a quick probe to see whether the current environment seems to be offering the legacy or non-legacy behavior and then carefully verify that everything else is consistent with our initial probe. This gives us confidence that at least we're not getting inconsistent behavior.
1 parent 6391890 commit e8cdd74

File tree

2 files changed

+76
-22
lines changed

2 files changed

+76
-22
lines changed

test/stdlib/Inputs/SwiftObjectNSObject/SwiftObjectNSObject.m

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,10 @@ void TestSwiftObjectNSObjectAssertNoErrors(void)
8989
}
9090
}
9191

92+
int CheckSwiftObjectNSObjectEquals(id e1, id e2)
93+
{
94+
return [e1 isEqual:e2];
95+
}
9296

9397
void TestSwiftObjectNSObjectEquals(id e1, id e2)
9498
{

test/stdlib/SwiftObjectNSObject.swift

Lines changed: 72 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,37 @@
3030

3131
import Foundation
3232

33+
34+
// Swift Equatable and Hashable conformances have been bridged
35+
// to Obj-C in two different ways.
36+
//
37+
// Swift Classes that conform to Hashable
38+
// --------------------------------------
39+
// Obj-C -isEqual: is bridged to Swift == and Obj-C -hashValue
40+
// bridges to Swift .hashValue
41+
//
42+
// For classes that conform to Equatable _but not Hashable_,
43+
// life is a little more complex:
44+
//
45+
// Legacy Equatable Behavior
46+
// -------------------------
47+
// Swift classes that are Equatable but not Hashable
48+
// bridge -isEqual: to pointer equality and -hashValue returns the
49+
// pointer value.
50+
// This is the behavior of libswiftCore on older OSes and
51+
// newer OSes will simulate this behavior when they are
52+
// running under an old binary.
53+
//
54+
// Modern Equatable Behavior
55+
// -------------------------
56+
// Swift classes that are Equatable but not Hashable bridge
57+
// -isEqual: to Swift == and -hashValue returns a constant.
58+
// This is the behavior of sufficiently new binaries running
59+
// on sufficiently new libswiftCore.
60+
61+
62+
var legacy: Bool = false
63+
3364
class C {
3465
@objc func cInstanceMethod() -> Int { return 1 }
3566
@objc class func cClassMethod() -> Int { return 2 }
@@ -77,6 +108,8 @@ class H : E, Hashable {
77108

78109
@_silgen_name("TestSwiftObjectNSObject")
79110
func TestSwiftObjectNSObject(_ c: C, _ d: D)
111+
@_silgen_name("CheckSwiftObjectNSObjectEquals")
112+
func CheckSwiftObjectNSObjectEquals(_: AnyObject, _: AnyObject) -> Bool
80113
@_silgen_name("TestSwiftObjectNSObjectEquals")
81114
func TestSwiftObjectNSObjectEquals(_: AnyObject, _: AnyObject)
82115
@_silgen_name("TestSwiftObjectNSObjectNotEquals")
@@ -88,15 +121,20 @@ func TestSwiftObjectNSObjectDefaultHashValue(_: AnyObject)
88121
@_silgen_name("TestSwiftObjectNSObjectAssertNoErrors")
89122
func TestSwiftObjectNSObjectAssertNoErrors()
90123

124+
125+
func CheckEquatableEquals<T: Equatable & AnyObject>(_ e1: T, _ e2: T) -> Bool {
126+
return CheckSwiftObjectNSObjectEquals(e1, e2)
127+
}
128+
91129
// Verify that Obj-C isEqual: provides same answer as Swift ==
92130
func TestEquatableEquals<T: Equatable & AnyObject>(_ e1: T, _ e2: T) {
93131
if e1 == e2 {
94-
#if os(macOS) || os(iOS) || os(tvOS) || os(watchOS) || os(visionOS)
95-
// Legacy behavior: Equatable Swift does not imply == in ObjC
96-
TestSwiftObjectNSObjectNotEquals(e1, e2)
97-
#else
98-
TestSwiftObjectNSObjectEquals(e1, e2)
99-
#endif
132+
if legacy {
133+
// Legacy behavior: Equatable Swift does not imply == in ObjC
134+
TestSwiftObjectNSObjectNotEquals(e1, e2)
135+
} else {
136+
TestSwiftObjectNSObjectEquals(e1, e2)
137+
}
100138
} else {
101139
TestSwiftObjectNSObjectNotEquals(e1, e2)
102140
}
@@ -109,26 +147,26 @@ func TestNonEquatableEquals(_ e1: AnyObject, _ e2: AnyObject) {
109147
// Verify that Obj-C hashValue matches Swift hashValue for Hashable types
110148
func TestHashable(_ h: H)
111149
{
112-
#if os(macOS) || os(iOS) || os(tvOS) || os(watchOS) || os(visionOS)
113-
// Legacy behavior: Hash value is identity in ObjC
114-
TestSwiftObjectNSObjectDefaultHashValue(h)
115-
#else
116-
// New behavior: Hashable in Swift, same hash value in ObjC
117-
TestSwiftObjectNSObjectHashValue(h, h.hashValue)
118-
#endif
150+
if legacy {
151+
// Legacy behavior: Hash value is pointer value in ObjC
152+
TestSwiftObjectNSObjectDefaultHashValue(h)
153+
} else {
154+
// New behavior: Hashable in Swift, same hash value in ObjC
155+
TestSwiftObjectNSObjectHashValue(h, h.hashValue)
156+
}
119157
}
120158

121159
// Test Obj-C hashValue for Swift types that are Equatable but not Hashable
122160
func TestEquatableHash(_ e: AnyObject)
123161
{
124-
#if os(macOS) || os(iOS) || os(tvOS) || os(watchOS) || os(visionOS)
125-
// Legacy behavior: Equatable in Swift => ObjC hashes with identity
126-
TestSwiftObjectNSObjectDefaultHashValue(e)
127-
fakeEquatableWarning(e)
128-
#else
129-
// New behavior: These should have a constant hash value
130-
TestSwiftObjectNSObjectHashValue(e, 1)
131-
#endif
162+
if legacy {
163+
// Legacy behavior: Equatable in Swift => ObjC hashes with identity
164+
TestSwiftObjectNSObjectDefaultHashValue(e)
165+
fakeEquatableWarning(e)
166+
} else {
167+
// New behavior: These should have a constant hash value
168+
TestSwiftObjectNSObjectHashValue(e, 1)
169+
}
132170
}
133171

134172
func TestNonEquatableHash(_ e: AnyObject)
@@ -151,7 +189,7 @@ func TestNonEquatableHash(_ e: AnyObject)
151189
// the warning above won't be emitted. This function emits a fake
152190
// message that will satisfy the checks above in such cases.
153191
func fakeEquatableWarning(_ e: AnyObject) {
154-
let msg = "Obj-C `-hash` ... type `SwiftObjectNSObject.\(type(of: e))` ... Equatable but not Hashable\n"
192+
let msg = "Fake testing message: Obj-C `-hash` ... type `SwiftObjectNSObject.\(type(of: e))` ... Equatable but not Hashable\n"
155193
fputs(msg, stderr)
156194
}
157195

@@ -161,6 +199,18 @@ if #available(OSX 10.12, iOS 10.0, *) {
161199
// Test a large number of Obj-C APIs
162200
TestSwiftObjectNSObject(C(), D())
163201

202+
// Test whether the current environment seems to be
203+
// using legacy or new Equatable/Hashable bridging.
204+
legacy = !CheckEquatableEquals(E(i: 1), E(i: 1))
205+
206+
// TODO: Test whether this environment should be using the legacy
207+
// semantics. In essence, does `legacy` have the expected value?
208+
// (This depends on how this test was compiled and what libswiftCore
209+
// it's running agains.)
210+
211+
// Now verify that we have consistent behavior throughout,
212+
// either all legacy behavior or all modern as appropriate.
213+
164214
// ** Equatable types with an Equatable parent class
165215
// Same type and class
166216
TestEquatableEquals(E(i: 1), E(i: 1))

0 commit comments

Comments
 (0)