Skip to content

Commit d9c166f

Browse files
authored
Merge pull request #23832 from lorentey/foundation-hashing
[Foundation] Modernize hashing in Foundation's Swift-only types
2 parents ed36e08 + 530687f commit d9c166f

33 files changed

+518
-167
lines changed

stdlib/private/StdlibUnittest/StdlibUnittest.swift

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2362,17 +2362,22 @@ internal func _checkEquatableImpl<Instance : Equatable>(
23622362
let isEqualXY = x == y
23632363
expectEqual(
23642364
predictedXY, isEqualXY,
2365-
(predictedXY
2366-
? "expected equal, found not equal\n"
2367-
: "expected not equal, found equal\n") +
2368-
"lhs (at index \(i)): \(String(reflecting: x))\n" +
2369-
"rhs (at index \(j)): \(String(reflecting: y))",
2365+
"""
2366+
\((predictedXY
2367+
? "expected equal, found not equal"
2368+
: "expected not equal, found equal"))
2369+
lhs (at index \(i)): \(String(reflecting: x))
2370+
rhs (at index \(j)): \(String(reflecting: y))
2371+
""",
23702372
stackTrace: stackTrace.pushIf(showFrame, file: file, line: line))
23712373

23722374
// Not-equal is an inverse of equal.
23732375
expectNotEqual(
23742376
isEqualXY, x != y,
2375-
"lhs (at index \(i)): \(String(reflecting: x))\nrhs (at index \(j)): \(String(reflecting: y))",
2377+
"""
2378+
lhs (at index \(i)): \(String(reflecting: x))
2379+
rhs (at index \(j)): \(String(reflecting: y))
2380+
""",
23762381
stackTrace: stackTrace.pushIf(showFrame, file: file, line: line))
23772382

23782383
if !allowBrokenTransitivity {
@@ -2414,6 +2419,10 @@ public func checkEquatable<T : Equatable>(
24142419
showFrame: false)
24152420
}
24162421

2422+
/// Produce an integer hash value for `value` by feeding it to a dedicated
2423+
/// `Hasher`. This is always done by calling the `hash(into:)` method.
2424+
/// If a non-nil `seed` is given, it is used to perturb the hasher state;
2425+
/// this is useful for resolving accidental hash collisions.
24172426
internal func hash<H: Hashable>(_ value: H, seed: Int? = nil) -> Int {
24182427
var hasher = Hasher()
24192428
if let seed = seed {
@@ -2429,6 +2438,7 @@ internal func hash<H: Hashable>(_ value: H, seed: Int? = nil) -> Int {
24292438
public func checkHashableGroups<Groups: Collection>(
24302439
_ groups: Groups,
24312440
_ message: @autoclosure () -> String = "",
2441+
allowIncompleteHashing: Bool = false,
24322442
stackTrace: SourceLocStack = SourceLocStack(),
24332443
showFrame: Bool = true,
24342444
file: String = #file, line: UInt = #line
@@ -2446,6 +2456,7 @@ public func checkHashableGroups<Groups: Collection>(
24462456
equalityOracle: equalityOracle,
24472457
hashEqualityOracle: equalityOracle,
24482458
allowBrokenTransitivity: false,
2459+
allowIncompleteHashing: allowIncompleteHashing,
24492460
stackTrace: stackTrace.pushIf(showFrame, file: file, line: line),
24502461
showFrame: false)
24512462
}
@@ -2457,6 +2468,7 @@ public func checkHashable<Instances: Collection>(
24572468
_ instances: Instances,
24582469
equalityOracle: (Instances.Index, Instances.Index) -> Bool,
24592470
allowBrokenTransitivity: Bool = false,
2471+
allowIncompleteHashing: Bool = false,
24602472
_ message: @autoclosure () -> String = "",
24612473
stackTrace: SourceLocStack = SourceLocStack(),
24622474
showFrame: Bool = true,
@@ -2467,6 +2479,7 @@ public func checkHashable<Instances: Collection>(
24672479
equalityOracle: equalityOracle,
24682480
hashEqualityOracle: equalityOracle,
24692481
allowBrokenTransitivity: allowBrokenTransitivity,
2482+
allowIncompleteHashing: allowIncompleteHashing,
24702483
stackTrace: stackTrace.pushIf(showFrame, file: file, line: line),
24712484
showFrame: false)
24722485
}
@@ -2480,6 +2493,7 @@ public func checkHashable<Instances: Collection>(
24802493
equalityOracle: (Instances.Index, Instances.Index) -> Bool,
24812494
hashEqualityOracle: (Instances.Index, Instances.Index) -> Bool,
24822495
allowBrokenTransitivity: Bool = false,
2496+
allowIncompleteHashing: Bool = false,
24832497
_ message: @autoclosure () -> String = "",
24842498
stackTrace: SourceLocStack = SourceLocStack(),
24852499
showFrame: Bool = true,
@@ -2532,12 +2546,12 @@ public func checkHashable<Instances: Collection>(
25322546
expectEqual(
25332547
x._rawHashValue(seed: 0), y._rawHashValue(seed: 0),
25342548
"""
2535-
_rawHashValue expected to match, found to differ
2549+
_rawHashValue(seed:) expected to match, found to differ
25362550
lhs (at index \(i)): \(x)
25372551
rhs (at index \(j)): \(y)
25382552
""",
25392553
stackTrace: stackTrace.pushIf(showFrame, file: file, line: line))
2540-
} else {
2554+
} else if !allowIncompleteHashing {
25412555
// Try a few different seeds; at least one of them should discriminate
25422556
// between the hashes. It is extremely unlikely this check will fail
25432557
// all ten attempts, unless the type's hash encoding is not unique,

stdlib/public/Darwin/Foundation/AffineTransform.swift

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -276,8 +276,13 @@ public struct AffineTransform : ReferenceConvertible, Hashable, CustomStringConv
276276
return newSize
277277
}
278278

279-
public var hashValue : Int {
280-
return Int(m11 + m12 + m21 + m22 + tX + tY)
279+
public func hash(into hasher: inout Hasher) {
280+
hasher.combine(m11)
281+
hasher.combine(m12)
282+
hasher.combine(m21)
283+
hasher.combine(m22)
284+
hasher.combine(tX)
285+
hasher.combine(tY)
281286
}
282287

283288
public var description: String {

stdlib/public/Darwin/Foundation/Calendar.swift

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -899,15 +899,16 @@ public struct Calendar : Hashable, Equatable, ReferenceConvertible, _MutableBoxi
899899

900900
// MARK: -
901901

902-
public var hashValue : Int {
903-
// We implement hash ourselves, because we need to make sure autoupdating calendars have the same hash
902+
public func hash(into hasher: inout Hasher) {
903+
// We need to make sure autoupdating calendars have the same hash
904904
if _autoupdating {
905-
return 1
905+
hasher.combine(false)
906906
} else {
907-
return _handle.map { $0.hash }
907+
hasher.combine(true)
908+
hasher.combine(_handle.map { $0 })
908909
}
909910
}
910-
911+
911912
// MARK: -
912913
// MARK: Conversion Functions
913914

stdlib/public/Darwin/Foundation/CharacterSet.swift

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -52,15 +52,15 @@ fileprivate final class __CharacterSetStorage : Hashable {
5252

5353
// MARK: -
5454

55-
fileprivate var hashValue : Int {
55+
fileprivate func hash(into hasher: inout Hasher) {
5656
switch _backing {
5757
case .immutable(let cs):
58-
return Int(CFHash(cs))
58+
hasher.combine(CFHash(cs))
5959
case .mutable(let cs):
60-
return Int(CFHash(cs))
60+
hasher.combine(CFHash(cs))
6161
}
6262
}
63-
63+
6464
fileprivate static func ==(lhs : __CharacterSetStorage, rhs : __CharacterSetStorage) -> Bool {
6565
switch (lhs._backing, rhs._backing) {
6666
case (.immutable(let cs1), .immutable(let cs2)):
@@ -754,8 +754,8 @@ public struct CharacterSet : ReferenceConvertible, Equatable, Hashable, SetAlgeb
754754

755755
// MARK: -
756756

757-
public var hashValue: Int {
758-
return _storage.hashValue
757+
public func hash(into hasher: inout Hasher) {
758+
hasher.combine(_storage)
759759
}
760760

761761
/// Returns true if the two `CharacterSet`s are equal.

stdlib/public/Darwin/Foundation/Date.swift

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -142,14 +142,10 @@ public struct Date : ReferenceConvertible, Comparable, Equatable {
142142
*/
143143
public static let distantPast = Date(timeIntervalSinceReferenceDate: -63114076800.0)
144144

145-
public var hashValue: Int {
146-
if #available(macOS 10.12, iOS 10.0, *) {
147-
return Int(bitPattern: __CFHashDouble(_time))
148-
} else { // 10.11 and previous behavior fallback; this must allocate a date to reference the hash value and then throw away the reference
149-
return NSDate(timeIntervalSinceReferenceDate: _time).hash
150-
}
145+
public func hash(into hasher: inout Hasher) {
146+
hasher.combine(_time)
151147
}
152-
148+
153149
/// Compare two `Date` values.
154150
public func compare(_ other: Date) -> ComparisonResult {
155151
if _time < other.timeIntervalSinceReferenceDate {

stdlib/public/Darwin/Foundation/DateComponents.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -263,10 +263,10 @@ public struct DateComponents : ReferenceConvertible, Hashable, Equatable, _Mutab
263263

264264
// MARK: -
265265

266-
public var hashValue : Int {
267-
return _handle.map { $0.hash }
266+
public func hash(into hasher: inout Hasher) {
267+
hasher.combine(_handle._uncopiedReference())
268268
}
269-
269+
270270
// MARK: - Bridging Helpers
271271

272272
fileprivate init(reference: __shared NSDateComponents) {

stdlib/public/Darwin/Foundation/DateInterval.swift

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -155,15 +155,11 @@ public struct DateInterval : ReferenceConvertible, Comparable, Hashable, Codable
155155
return false
156156
}
157157

158-
public var hashValue: Int {
159-
var buf: (UInt, UInt) = (UInt(start.timeIntervalSinceReferenceDate), UInt(end.timeIntervalSinceReferenceDate))
160-
return withUnsafeMutablePointer(to: &buf) {
161-
$0.withMemoryRebound(to: UInt8.self, capacity: 2 * MemoryLayout<UInt>.size / MemoryLayout<UInt8>.size) {
162-
return Int(bitPattern: CFHashBytes($0, CFIndex(MemoryLayout<UInt>.size * 2)))
163-
}
164-
}
158+
public func hash(into hasher: inout Hasher) {
159+
hasher.combine(start)
160+
hasher.combine(duration)
165161
}
166-
162+
167163
@available(macOS 10.12, iOS 10.0, watchOS 3.0, tvOS 10.0, *)
168164
public static func ==(lhs: DateInterval, rhs: DateInterval) -> Bool {
169165
return lhs.start == rhs.start && lhs.duration == rhs.duration

stdlib/public/Darwin/Foundation/Decimal.swift

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -177,8 +177,12 @@ extension Decimal : Hashable, Comparable {
177177
return _isNegative != 0 ? -d : d
178178
}
179179

180-
public var hashValue: Int {
181-
return Int(bitPattern: __CFHashDouble(doubleValue))
180+
public func hash(into hasher: inout Hasher) {
181+
// FIXME: This is a weak hash. We should rather normalize self to a
182+
// canonical member of the exact same equivalence relation that
183+
// NSDecimalCompare implements, then simply feed all components to the
184+
// hasher.
185+
hasher.combine(doubleValue)
182186
}
183187

184188
public static func ==(lhs: Decimal, rhs: Decimal) -> Bool {

stdlib/public/Darwin/Foundation/IndexPath.swift

Lines changed: 22 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -662,25 +662,31 @@ public struct IndexPath : ReferenceConvertible, Equatable, Hashable, MutableColl
662662
return .orderedSame
663663
}
664664

665-
public var hashValue: Int {
666-
func hashIndexes(first: Int, last: Int, count: Int) -> Int {
667-
let totalBits = MemoryLayout<Int>.size * 8
668-
let lengthBits = 8
669-
let firstIndexBits = (totalBits - lengthBits) / 2
670-
return count &+ (first << lengthBits) &+ (last << (lengthBits + firstIndexBits))
671-
}
672-
665+
public func hash(into hasher: inout Hasher) {
666+
// Note: We compare all indices in ==, so for proper hashing, we must
667+
// also feed them all to the hasher.
668+
//
669+
// To ensure we have unique hash encodings in nested hashing contexts,
670+
// we combine the count of indices as well as the indices themselves.
671+
// (This matches what Array does.)
673672
switch _indexes {
674-
case .empty: return 0
675-
case .single(let index): return index.hashValue
676-
case .pair(let first, let second):
677-
return hashIndexes(first: first, last: second, count: 2)
678-
default:
679-
let cnt = _indexes.count
680-
return hashIndexes(first: _indexes[0], last: _indexes[cnt - 1], count: cnt)
673+
case .empty:
674+
hasher.combine(0)
675+
case let .single(index):
676+
hasher.combine(1)
677+
hasher.combine(index)
678+
case let .pair(first, second):
679+
hasher.combine(2)
680+
hasher.combine(first)
681+
hasher.combine(second)
682+
case let .array(indexes):
683+
hasher.combine(indexes.count)
684+
for index in indexes {
685+
hasher.combine(index)
686+
}
681687
}
682688
}
683-
689+
684690
// MARK: - Bridging Helpers
685691

686692
fileprivate init(nsIndexPath: __shared ReferenceType) {

stdlib/public/Darwin/Foundation/IndexSet.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -137,8 +137,8 @@ public struct IndexSet : ReferenceConvertible, Equatable, BidirectionalCollectio
137137
_handle = _MutablePairHandle(NSIndexSet(), copying: false)
138138
}
139139

140-
public var hashValue: Int {
141-
return _handle.map { $0.hash }
140+
public func hash(into hasher: inout Hasher) {
141+
_handle.map { hasher.combine($0) }
142142
}
143143

144144
/// Returns the number of integers in `self`.

stdlib/public/Darwin/Foundation/Locale.swift

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -405,11 +405,12 @@ public struct Locale : Hashable, Equatable, ReferenceConvertible {
405405
// MARK: -
406406
//
407407

408-
public var hashValue : Int {
408+
public func hash(into hasher: inout Hasher) {
409409
if _autoupdating {
410-
return 1
410+
hasher.combine(false)
411411
} else {
412-
return _wrapped.hash
412+
hasher.combine(true)
413+
hasher.combine(_wrapped)
413414
}
414415
}
415416

stdlib/public/Darwin/Foundation/Measurement.swift

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,19 @@ public struct Measurement<UnitType : Unit> : ReferenceConvertible, Comparable, E
3636
self.unit = unit
3737
}
3838

39-
public var hashValue: Int {
40-
return Int(bitPattern: __CFHashDouble(value))
39+
public func hash(into hasher: inout Hasher) {
40+
// Warning: The canonicalization performed here needs to be kept in
41+
// perfect sync with the definition of == below. The floating point
42+
// values that are compared there must match exactly with the values fed
43+
// to the hasher here, or hashing would break.
44+
if let dimension = unit as? Dimension {
45+
// We don't need to feed the base unit to the hasher here; all
46+
// dimensional measurements of the same type share the same unit.
47+
hasher.combine(dimension.converter.baseUnitValue(fromValue: value))
48+
} else {
49+
hasher.combine(unit)
50+
hasher.combine(value)
51+
}
4152
}
4253
}
4354

@@ -170,6 +181,10 @@ extension Measurement {
170181
/// If `lhs.unit == rhs.unit`, returns `lhs.value == rhs.value`. Otherwise, converts `rhs` to the same unit as `lhs` and then compares the resulting values.
171182
/// - returns: `true` if the measurements are equal.
172183
public static func ==<LeftHandSideType, RightHandSideType>(lhs: Measurement<LeftHandSideType>, rhs: Measurement<RightHandSideType>) -> Bool {
184+
// Warning: This defines an equivalence relation that needs to be kept
185+
// in perfect sync with the hash(into:) definition above. The floating
186+
// point values that are fed to the hasher there must match exactly with
187+
// the values compared here, or hashing would break.
173188
if lhs.unit == rhs.unit {
174189
return lhs.value == rhs.value
175190
} else {

stdlib/public/Darwin/Foundation/NSRange.swift

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,9 @@
1313
@_exported import Foundation // Clang module
1414

1515
extension NSRange : Hashable {
16-
public var hashValue: Int {
17-
#if arch(i386) || arch(arm)
18-
return Int(bitPattern: (UInt(bitPattern: location) | (UInt(bitPattern: length) << 16)))
19-
#elseif arch(x86_64) || arch(arm64)
20-
return Int(bitPattern: (UInt(bitPattern: location) | (UInt(bitPattern: length) << 32)))
21-
#endif
16+
public func hash(into hasher: inout Hasher) {
17+
hasher.combine(location)
18+
hasher.combine(length)
2219
}
2320

2421
public static func==(lhs: NSRange, rhs: NSRange) -> Bool {
@@ -230,4 +227,4 @@ extension NSRange : Codable {
230227
try container.encode(self.location)
231228
try container.encode(self.length)
232229
}
233-
}
230+
}

stdlib/public/Darwin/Foundation/NSStringEncodings.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,8 +51,8 @@ extension String {
5151
}
5252

5353
extension String.Encoding : Hashable {
54-
public var hashValue : Int {
55-
return rawValue.hashValue
54+
public func hash(into hasher: inout Hasher) {
55+
hasher.combine(rawValue)
5656
}
5757

5858
public static func ==(lhs: String.Encoding, rhs: String.Encoding) -> Bool {

0 commit comments

Comments
 (0)