Skip to content

Commit ff91f36

Browse files
committed
[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
1 parent 6fc4cef commit ff91f36

13 files changed

+957
-240
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: 46 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -39,18 +39,36 @@ public protocol _HasCustomAnyHashableRepresentation {
3939

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

4444
/// Determine whether values in the boxes are equivalent.
4545
///
46+
/// - Precondition: `self` and `box` are in canonical form.
4647
/// - Returns: `nil` to indicate that the boxes store different types, so
4748
/// no comparison is possible. Otherwise, contains the result of `==`.
48-
func _isEqual(to: _AnyHashableBox) -> Bool?
49+
func _isEqual(to box: _AnyHashableBox) -> Bool?
4950
var _hashValue: Int { get }
5051
func _hash(into hasher: inout Hasher)
52+
func _rawHashValue(_seed: (UInt64, UInt64)) -> Int
5153

5254
var _base: Any { get }
55+
func _unbox<T: Hashable>() -> T?
5356
func _downCastConditional<T>(into result: UnsafeMutablePointer<T>) -> Bool
57+
58+
func _asSet() -> Set<AnyHashable>?
59+
func _asDictionary() -> Dictionary<AnyHashable, AnyHashable>?
60+
}
61+
62+
extension _AnyHashableBox {
63+
var _canonicalBox: _AnyHashableBox {
64+
return self
65+
}
66+
func _asSet() -> Set<AnyHashable>? {
67+
return nil
68+
}
69+
func _asDictionary() -> Dictionary<AnyHashable, AnyHashable>? {
70+
return nil
71+
}
5472
}
5573

5674
@_fixed_layout // FIXME(sil-serialize-all)
@@ -87,6 +105,11 @@ internal struct _ConcreteHashableBox<Base : Hashable> : _AnyHashableBox {
87105
_baseHashable.hash(into: &hasher)
88106
}
89107

108+
@inlinable // FIXME(sil-serialize-all)
109+
func _rawHashValue(_seed: (UInt64, UInt64)) -> Int {
110+
return _baseHashable._rawHashValue(seed: _seed)
111+
}
112+
90113
@inlinable // FIXME(sil-serialize-all)
91114
internal var _base: Any {
92115
return _baseHashable
@@ -101,19 +124,6 @@ internal struct _ConcreteHashableBox<Base : Hashable> : _AnyHashableBox {
101124
}
102125
}
103126

104-
#if _runtime(_ObjC)
105-
// Retrieve the custom AnyHashable representation of the value after it
106-
// has been bridged to Objective-C. This mapping to Objective-C and back
107-
// turns a non-custom representation into a custom one, which is used as
108-
// the lowest-common-denominator for comparisons.
109-
@inlinable // FIXME(sil-serialize-all)
110-
internal func _getBridgedCustomAnyHashable<T>(_ value: T) -> AnyHashable? {
111-
let bridgedValue = _bridgeAnythingToObjectiveC(value)
112-
return (bridgedValue as?
113-
_HasCustomAnyHashableRepresentation)?._toCustomAnyHashable()
114-
}
115-
#endif
116-
117127
/// A type-erased hashable value.
118128
///
119129
/// The `AnyHashable` type forwards equality comparisons and hashing operations
@@ -137,8 +147,11 @@ internal func _getBridgedCustomAnyHashable<T>(_ value: T) -> AnyHashable? {
137147
public struct AnyHashable {
138148
@usableFromInline // FIXME(sil-serialize-all)
139149
internal var _box: _AnyHashableBox
140-
@usableFromInline // FIXME(sil-serialize-all)
141-
internal var _usedCustomRepresentation: Bool
150+
151+
@inlinable // FIXME(sil-serialize-all)
152+
internal init(_box box: _AnyHashableBox) {
153+
self._box = box
154+
}
142155

143156
/// Creates a type-erased hashable value that wraps the given instance.
144157
///
@@ -160,15 +173,13 @@ public struct AnyHashable {
160173
/// - Parameter base: A hashable value to wrap.
161174
@inlinable // FIXME(sil-serialize-all)
162175
public init<H : Hashable>(_ base: H) {
163-
if let customRepresentation =
176+
if let custom =
164177
(base as? _HasCustomAnyHashableRepresentation)?._toCustomAnyHashable() {
165-
self = customRepresentation
166-
self._usedCustomRepresentation = true
178+
self = custom
167179
return
168180
}
169181

170-
self._box = _ConcreteHashableBox(0 as Int)
171-
self._usedCustomRepresentation = false
182+
self.init(_box: _ConcreteHashableBox(false)) // Dummy value
172183
_makeAnyHashableUpcastingToHashableBaseType(
173184
base,
174185
storingResultInto: &self)
@@ -177,7 +188,6 @@ public struct AnyHashable {
177188
@inlinable // FIXME(sil-serialize-all)
178189
internal init<H : Hashable>(_usingDefaultRepresentationOf base: H) {
179190
self._box = _ConcreteHashableBox(base)
180-
self._usedCustomRepresentation = false
181191
}
182192

183193
/// The value wrapped by this instance.
@@ -206,13 +216,11 @@ public struct AnyHashable {
206216
if _box._downCastConditional(into: result) { return true }
207217

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

@@ -248,42 +256,15 @@ extension AnyHashable : Equatable {
248256
/// - rhs: Another type-erased hashable value.
249257
@inlinable // FIXME(sil-serialize-all)
250258
public static func == (lhs: AnyHashable, rhs: AnyHashable) -> Bool {
251-
// If they're equal, we're done.
252-
if let result = lhs._box._isEqual(to: rhs._box) { return result }
253-
254-
#if _runtime(_ObjC)
255-
// If one used a custom representation but the other did not, bridge
256-
// the one that did *not* use the custom representation to Objective-C:
257-
// if the bridged result has a custom representation, compare those custom
258-
// custom representations.
259-
if lhs._usedCustomRepresentation != rhs._usedCustomRepresentation {
260-
// If the lhs used a custom representation, try comparing against the
261-
// custom representation of the bridged rhs (if there is one).
262-
if lhs._usedCustomRepresentation {
263-
if let customRHS = _getBridgedCustomAnyHashable(rhs._box._base) {
264-
return lhs._box._isEqual(to: customRHS._box) ?? false
265-
}
266-
return false
267-
}
268-
269-
// Otherwise, try comparing the rhs against the custom representation of
270-
// the bridged lhs (if there is one).
271-
if let customLHS = _getBridgedCustomAnyHashable(lhs._box._base) {
272-
return customLHS._box._isEqual(to: rhs._box) ?? false
273-
}
274-
return false
275-
}
276-
#endif
277-
278-
return false
259+
return lhs._box._canonicalBox._isEqual(to: rhs._box._canonicalBox) ?? false
279260
}
280261
}
281262

282263
extension AnyHashable : Hashable {
283264
/// The hash value.
284265
@inlinable
285266
public var hashValue: Int {
286-
return _box._hashValue
267+
return _box._canonicalBox._hashValue
287268
}
288269

289270
/// Hashes the essential components of this value by feeding them into the
@@ -293,7 +274,12 @@ extension AnyHashable : Hashable {
293274
/// of this instance.
294275
@inlinable
295276
public func hash(into hasher: inout Hasher) {
296-
_box._hash(into: &hasher)
277+
_box._canonicalBox._hash(into: &hasher)
278+
}
279+
280+
@inlinable // FIXME(sil-serialize-all)
281+
public func _rawHashValue(seed: (UInt64, UInt64)) -> Int {
282+
return _box._canonicalBox._rawHashValue(_seed: seed)
297283
}
298284
}
299285

stdlib/public/core/Array.swift

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1758,3 +1758,76 @@ extension Array {
17581758
}
17591759
}
17601760
#endif
1761+
1762+
extension Array: _HasCustomAnyHashableRepresentation
1763+
where Element: Hashable {
1764+
public func _toCustomAnyHashable() -> AnyHashable? {
1765+
return AnyHashable(_box: _ArrayAnyHashableBox(self))
1766+
}
1767+
}
1768+
1769+
internal protocol _ArrayAnyHashableProtocol: _AnyHashableBox {
1770+
var count: Int { get }
1771+
subscript(index: Int) -> AnyHashable { get }
1772+
}
1773+
1774+
internal struct _ArrayAnyHashableBox<Element: Hashable>
1775+
: _ArrayAnyHashableProtocol {
1776+
internal let _value: [Element]
1777+
1778+
internal init(_ value: [Element]) {
1779+
self._value = value
1780+
}
1781+
1782+
internal var _base: Any {
1783+
return _value
1784+
}
1785+
1786+
internal var count: Int {
1787+
return _value.count
1788+
}
1789+
1790+
internal subscript(index: Int) -> AnyHashable {
1791+
return _value[index] as AnyHashable
1792+
}
1793+
1794+
func _isEqual(to other: _AnyHashableBox) -> Bool? {
1795+
guard let other = other as? _ArrayAnyHashableProtocol else { return nil }
1796+
guard _value.count == other.count else { return false }
1797+
for i in 0 ..< _value.count {
1798+
if self[i] != other[i] { return false }
1799+
}
1800+
return true
1801+
}
1802+
1803+
var _hashValue: Int {
1804+
var hasher = Hasher()
1805+
_hash(into: &hasher)
1806+
return hasher.finalize()
1807+
}
1808+
1809+
func _hash(into hasher: inout Hasher) {
1810+
hasher.combine(_value.count) // discriminator
1811+
for i in 0 ..< _value.count {
1812+
hasher.combine(self[i])
1813+
}
1814+
}
1815+
1816+
func _rawHashValue(_seed: (UInt64, UInt64)) -> Int {
1817+
var hasher = Hasher(_seed: _seed)
1818+
self._hash(into: &hasher)
1819+
return hasher._finalize()
1820+
}
1821+
1822+
internal func _unbox<T : Hashable>() -> T? {
1823+
return _value as? T
1824+
}
1825+
1826+
internal func _downCastConditional<T>(
1827+
into result: UnsafeMutablePointer<T>
1828+
) -> Bool {
1829+
guard let value = _value as? T else { return false }
1830+
result.initialize(to: value)
1831+
return true
1832+
}
1833+
}

stdlib/public/core/Dictionary.swift

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1520,6 +1520,61 @@ extension Dictionary: Hashable where Value: Hashable {
15201520
}
15211521
}
15221522

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

0 commit comments

Comments
 (0)