Skip to content

Commit b415588

Browse files
lorenteyairspeedswift
authored andcommitted
[4.2][stdlib] Fix AnyHashable's Equatable/Hashable conformance (#17518)
* [stdlib] Fix AnyHashable's Equatable/Hashable conformance AnyHashable has numerous edge cases where two AnyHashable values compare equal but produce different hashes. This breaks Set and Dictionary invariants and can cause unexpected behavior and/or traps. This change overhauls AnyHashable's implementation to fix these edge cases, hopefully without introducing new issues. - Fix transitivity of ==. Previously, comparisons involving AnyHashable values with Objective-C provenance were handled specially, breaking Equatable: let a = (42 as Int as AnyHashable) let b = (42 as NSNumber as AnyHashable) let c = (42 as Double as AnyHashable) a == b // true b == c // true a == c // was false(!), now true let d = ("foo" as AnyHashable) let e = ("foo" as NSString as AnyHashable) let f = ("foo" as NSString as NSAttributedStringKey as AnyHashable) d == e // true e == f // true d == f // was false(!), now true - Fix Hashable conformance for numeric types boxed into AnyHashable: b == c // true b.hashValue == c.hashValue // was false(!), now true Fixing this required adding a custom AnyHashable box for all standard integer and floating point types. The custom box was needed to ensure that two AnyHashables containing the same number compare equal and hash the same way, no matter what their original type was. (This behavior is required to ensure consistency with NSNumber, which has not been preserving types since SE-0170. - Add custom AnyHashable representations for Arrays, Sets and Dictionaries, so that when they contain numeric types, they hash correctly under the new rules above. - Remove AnyHashable._usedCustomRepresentation. The provenance of a value should not affect its behavior. - Allow AnyHashable values to be downcasted into compatible types more often. - Forward _rawHashValue(seed:) to AnyHashable box. This fixes AnyHashable hashing for types that customize single-shot hashing. https://bugs.swift.org/browse/SR-7496 rdar://problem/39648819 (cherry picked from commit ff91f36) # Conflicts: # stdlib/public/core/AnyHashable.swift # stdlib/public/core/Arrays.swift.gyb # stdlib/public/core/Hasher.swift # stdlib/public/core/Integers.swift.gyb * [stdlib][SE-0206] Use distinct hash encodings for standard integer types Fix Hashable conformance of standard integer types so that the number of bits they feed into hasher is exactly Self.bitWidth. This was intended to be part of SE-0206. However, it would have introduced additional issues with AnyHashable. The custom AnyHashable representations introduced in the previous commit unify hashing for numeric types, eliminating the problem. (cherry picked from commit bf872ec) # Conflicts: # stdlib/public/core/Integers.swift.gyb * [stdlib] Eliminate _AnyHashableBox._asSet() & ._asDictionary() _canonicalBox can perform essentially the same job, so there is no reason to have these requirements. (cherry picked from commit 2f4ad79)
1 parent d7ff0a3 commit b415588

13 files changed

+909
-266
lines changed

stdlib/private/StdlibUnittest/StdlibUnittest.swift

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2410,6 +2410,33 @@ internal func hash<H: Hashable>(_ value: H, seed: Int? = nil) -> Int {
24102410
return hasher.finalize()
24112411
}
24122412

2413+
/// Test that the elements of `groups` consist of instances that satisfy the
2414+
/// semantic requirements of `Hashable`, with each group defining a distinct
2415+
/// equivalence class under `==`.
2416+
public func checkHashableGroups<Groups: Collection>(
2417+
_ groups: Groups,
2418+
_ message: @autoclosure () -> String = "",
2419+
stackTrace: SourceLocStack = SourceLocStack(),
2420+
showFrame: Bool = true,
2421+
file: String = #file, line: UInt = #line
2422+
) where Groups.Element: Collection, Groups.Element.Element: Hashable {
2423+
let instances = groups.flatMap { $0 }
2424+
// groupIndices[i] is the index of the element in groups that contains
2425+
// instances[i].
2426+
let groupIndices =
2427+
zip(0..., groups).flatMap { i, group in group.map { _ in i } }
2428+
func equalityOracle(_ lhs: Int, _ rhs: Int) -> Bool {
2429+
return groupIndices[lhs] == groupIndices[rhs]
2430+
}
2431+
checkHashable(
2432+
instances,
2433+
equalityOracle: equalityOracle,
2434+
hashEqualityOracle: equalityOracle,
2435+
allowBrokenTransitivity: false,
2436+
stackTrace: stackTrace.pushIf(showFrame, file: file, line: line),
2437+
showFrame: false)
2438+
}
2439+
24132440
/// Test that the elements of `instances` satisfy the semantic requirements of
24142441
/// `Hashable`, using `equalityOracle` to generate equality and hashing
24152442
/// expectations from pairs of positions in `instances`.

stdlib/public/core/AnyHashable.swift

Lines changed: 26 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -39,21 +39,28 @@ public protocol _HasCustomAnyHashableRepresentation {
3939

4040
@usableFromInline // FIXME(sil-serialize-all)
4141
internal protocol _AnyHashableBox {
42-
var _typeID: ObjectIdentifier { get }
43-
func _unbox<T : Hashable>() -> T?
42+
var _canonicalBox: _AnyHashableBox { get }
4443

4544
/// Determine whether values in the boxes are equivalent.
4645
///
46+
/// - Precondition: `self` and `box` are in canonical form.
4747
/// - Returns: `nil` to indicate that the boxes store different types, so
4848
/// no comparison is possible. Otherwise, contains the result of `==`.
49-
func _isEqual(to: _AnyHashableBox) -> Bool?
49+
func _isEqual(to box: _AnyHashableBox) -> Bool?
5050
var _hashValue: Int { get }
5151
func _hash(into hasher: inout Hasher)
5252

5353
var _base: Any { get }
54+
func _unbox<T: Hashable>() -> T?
5455
func _downCastConditional<T>(into result: UnsafeMutablePointer<T>) -> Bool
5556
}
5657

58+
extension _AnyHashableBox {
59+
var _canonicalBox: _AnyHashableBox {
60+
return self
61+
}
62+
}
63+
5764
@_fixed_layout // FIXME(sil-serialize-all)
5865
@usableFromInline // FIXME(sil-serialize-all)
5966
internal struct _ConcreteHashableBox<Base : Hashable> : _AnyHashableBox {
@@ -108,19 +115,6 @@ internal struct _ConcreteHashableBox<Base : Hashable> : _AnyHashableBox {
108115
}
109116
}
110117

111-
#if _runtime(_ObjC)
112-
// Retrieve the custom AnyHashable representation of the value after it
113-
// has been bridged to Objective-C. This mapping to Objective-C and back
114-
// turns a non-custom representation into a custom one, which is used as
115-
// the lowest-common-denominator for comparisons.
116-
@inlinable // FIXME(sil-serialize-all)
117-
internal func _getBridgedCustomAnyHashable<T>(_ value: T) -> AnyHashable? {
118-
let bridgedValue = _bridgeAnythingToObjectiveC(value)
119-
return (bridgedValue as?
120-
_HasCustomAnyHashableRepresentation)?._toCustomAnyHashable()
121-
}
122-
#endif
123-
124118
/// A type-erased hashable value.
125119
///
126120
/// The `AnyHashable` type forwards equality comparisons and hashing operations
@@ -144,8 +138,11 @@ internal func _getBridgedCustomAnyHashable<T>(_ value: T) -> AnyHashable? {
144138
public struct AnyHashable {
145139
@usableFromInline // FIXME(sil-serialize-all)
146140
internal var _box: _AnyHashableBox
147-
@usableFromInline // FIXME(sil-serialize-all)
148-
internal var _usedCustomRepresentation: Bool
141+
142+
@inlinable // FIXME(sil-serialize-all)
143+
internal init(_box box: _AnyHashableBox) {
144+
self._box = box
145+
}
149146

150147
/// Creates a type-erased hashable value that wraps the given instance.
151148
///
@@ -167,15 +164,13 @@ public struct AnyHashable {
167164
/// - Parameter base: A hashable value to wrap.
168165
@inlinable // FIXME(sil-serialize-all)
169166
public init<H : Hashable>(_ base: H) {
170-
if let customRepresentation =
167+
if let custom =
171168
(base as? _HasCustomAnyHashableRepresentation)?._toCustomAnyHashable() {
172-
self = customRepresentation
173-
self._usedCustomRepresentation = true
169+
self = custom
174170
return
175171
}
176172

177-
self._box = _ConcreteHashableBox(0 as Int)
178-
self._usedCustomRepresentation = false
173+
self.init(_box: _ConcreteHashableBox(false)) // Dummy value
179174
_makeAnyHashableUpcastingToHashableBaseType(
180175
base,
181176
storingResultInto: &self)
@@ -184,7 +179,6 @@ public struct AnyHashable {
184179
@inlinable // FIXME(sil-serialize-all)
185180
internal init<H : Hashable>(_usingDefaultRepresentationOf base: H) {
186181
self._box = _ConcreteHashableBox(base)
187-
self._usedCustomRepresentation = false
188182
}
189183

190184
/// The value wrapped by this instance.
@@ -213,13 +207,11 @@ public struct AnyHashable {
213207
if _box._downCastConditional(into: result) { return true }
214208

215209
#if _runtime(_ObjC)
216-
// If we used a custom representation, bridge to Objective-C and then
217-
// attempt the cast from there.
218-
if _usedCustomRepresentation {
219-
if let value = _bridgeAnythingToObjectiveC(_box._base) as? T {
220-
result.initialize(to: value)
221-
return true
222-
}
210+
// Bridge to Objective-C and then attempt the cast from there.
211+
// FIXME: This should also work without the Objective-C runtime.
212+
if let value = _bridgeAnythingToObjectiveC(_box._base) as? T {
213+
result.initialize(to: value)
214+
return true
223215
}
224216
#endif
225217

@@ -255,42 +247,15 @@ extension AnyHashable : Equatable {
255247
/// - rhs: Another type-erased hashable value.
256248
@inlinable // FIXME(sil-serialize-all)
257249
public static func == (lhs: AnyHashable, rhs: AnyHashable) -> Bool {
258-
// If they're equal, we're done.
259-
if let result = lhs._box._isEqual(to: rhs._box) { return result }
260-
261-
#if _runtime(_ObjC)
262-
// If one used a custom representation but the other did not, bridge
263-
// the one that did *not* use the custom representation to Objective-C:
264-
// if the bridged result has a custom representation, compare those custom
265-
// custom representations.
266-
if lhs._usedCustomRepresentation != rhs._usedCustomRepresentation {
267-
// If the lhs used a custom representation, try comparing against the
268-
// custom representation of the bridged rhs (if there is one).
269-
if lhs._usedCustomRepresentation {
270-
if let customRHS = _getBridgedCustomAnyHashable(rhs._box._base) {
271-
return lhs._box._isEqual(to: customRHS._box) ?? false
272-
}
273-
return false
274-
}
275-
276-
// Otherwise, try comparing the rhs against the custom representation of
277-
// the bridged lhs (if there is one).
278-
if let customLHS = _getBridgedCustomAnyHashable(lhs._box._base) {
279-
return customLHS._box._isEqual(to: rhs._box) ?? false
280-
}
281-
return false
282-
}
283-
#endif
284-
285-
return false
250+
return lhs._box._canonicalBox._isEqual(to: rhs._box._canonicalBox) ?? false
286251
}
287252
}
288253

289254
extension AnyHashable : Hashable {
290255
/// The hash value.
291256
@inlinable // FIXME(sil-serialize-all)
292257
public var hashValue: Int {
293-
return _box._hashValue
258+
return _box._canonicalBox._hashValue
294259
}
295260

296261
/// Hashes the essential components of this value by feeding them into the
@@ -300,7 +265,7 @@ extension AnyHashable : Hashable {
300265
/// of this instance.
301266
@inlinable // FIXME(sil-serialize-all)
302267
public func hash(into hasher: inout Hasher) {
303-
_box._hash(into: &hasher)
268+
_box._canonicalBox._hash(into: &hasher)
304269
}
305270
}
306271

stdlib/public/core/Arrays.swift.gyb

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2420,6 +2420,73 @@ extension ArraySlice {
24202420
}
24212421
}
24222422

2423+
extension Array: _HasCustomAnyHashableRepresentation
2424+
where Element: Hashable {
2425+
public func _toCustomAnyHashable() -> AnyHashable? {
2426+
return AnyHashable(_box: _ArrayAnyHashableBox(self))
2427+
}
2428+
}
2429+
2430+
internal protocol _ArrayAnyHashableProtocol: _AnyHashableBox {
2431+
var count: Int { get }
2432+
subscript(index: Int) -> AnyHashable { get }
2433+
}
2434+
2435+
internal struct _ArrayAnyHashableBox<Element: Hashable>
2436+
: _ArrayAnyHashableProtocol {
2437+
internal let _value: [Element]
2438+
2439+
internal init(_ value: [Element]) {
2440+
self._value = value
2441+
}
2442+
2443+
internal var _base: Any {
2444+
return _value
2445+
}
2446+
2447+
internal var count: Int {
2448+
return _value.count
2449+
}
2450+
2451+
internal subscript(index: Int) -> AnyHashable {
2452+
return _value[index] as AnyHashable
2453+
}
2454+
2455+
func _isEqual(to other: _AnyHashableBox) -> Bool? {
2456+
guard let other = other as? _ArrayAnyHashableProtocol else { return nil }
2457+
guard _value.count == other.count else { return false }
2458+
for i in 0 ..< _value.count {
2459+
if self[i] != other[i] { return false }
2460+
}
2461+
return true
2462+
}
2463+
2464+
var _hashValue: Int {
2465+
var hasher = Hasher()
2466+
_hash(into: &hasher)
2467+
return hasher.finalize()
2468+
}
2469+
2470+
func _hash(into hasher: inout Hasher) {
2471+
hasher.combine(_value.count) // discriminator
2472+
for i in 0 ..< _value.count {
2473+
hasher.combine(self[i])
2474+
}
2475+
}
2476+
2477+
internal func _unbox<T : Hashable>() -> T? {
2478+
return _value as? T
2479+
}
2480+
2481+
internal func _downCastConditional<T>(
2482+
into result: UnsafeMutablePointer<T>
2483+
) -> Bool {
2484+
guard let value = _value as? T else { return false }
2485+
result.initialize(to: value)
2486+
return true
2487+
}
2488+
}
2489+
24232490
// ${'Local Variables'}:
24242491
// eval: (read-only-mode 1)
24252492
// End:

stdlib/public/core/Dictionary.swift

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1515,6 +1515,61 @@ extension Dictionary: Hashable where Value: Hashable {
15151515
}
15161516
}
15171517

1518+
extension Dictionary: _HasCustomAnyHashableRepresentation
1519+
where Value: Hashable {
1520+
public func _toCustomAnyHashable() -> AnyHashable? {
1521+
return AnyHashable(_box: _DictionaryAnyHashableBox(self))
1522+
}
1523+
}
1524+
1525+
internal struct _DictionaryAnyHashableBox<Key: Hashable, Value: Hashable>
1526+
: _AnyHashableBox {
1527+
internal let _value: Dictionary<Key, Value>
1528+
internal let _canonical: Dictionary<AnyHashable, AnyHashable>
1529+
1530+
internal init(_ value: Dictionary<Key, Value>) {
1531+
self._value = value
1532+
self._canonical = value as Dictionary<AnyHashable, AnyHashable>
1533+
}
1534+
1535+
internal var _base: Any {
1536+
return _value
1537+
}
1538+
1539+
internal var _canonicalBox: _AnyHashableBox {
1540+
return _DictionaryAnyHashableBox<AnyHashable, AnyHashable>(_canonical)
1541+
}
1542+
1543+
internal func _isEqual(to other: _AnyHashableBox) -> Bool? {
1544+
guard
1545+
let other = other as? _DictionaryAnyHashableBox<AnyHashable, AnyHashable>
1546+
else {
1547+
return nil
1548+
}
1549+
return _canonical == other._value
1550+
}
1551+
1552+
internal var _hashValue: Int {
1553+
return _canonical.hashValue
1554+
}
1555+
1556+
internal func _hash(into hasher: inout Hasher) {
1557+
_canonical.hash(into: &hasher)
1558+
}
1559+
1560+
internal func _unbox<T: Hashable>() -> T? {
1561+
return _value as? T
1562+
}
1563+
1564+
internal func _downCastConditional<T>(
1565+
into result: UnsafeMutablePointer<T>
1566+
) -> Bool {
1567+
guard let value = _value as? T else { return false }
1568+
result.initialize(to: value)
1569+
return true
1570+
}
1571+
}
1572+
15181573
extension Dictionary: CustomStringConvertible, CustomDebugStringConvertible {
15191574
@inlinable // FIXME(sil-serialize-all)
15201575
internal func _makeDescription() -> String {

0 commit comments

Comments
 (0)