Skip to content

Modernize hashing in Foundation's Swift-only types #2118

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

Merged
merged 22 commits into from
Apr 16, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
22a7465
Port hashing checks from StdlibUnittest
lorentey Apr 15, 2019
eace6a6
_SwiftNativeFoundationType: Add hash(into:) requirement with a defaul…
lorentey Apr 12, 2019
d89f8f6
__BridgedNSError, _BridgedStoredNSError: Add hash(into:) implementations
lorentey Apr 12, 2019
cabf8d5
Calendar: Match hash encoding with the Cocoa Foundation overlay
lorentey Apr 12, 2019
9e37694
Locale: Match hash encoding with the Cocoa Foundation overlay
lorentey Apr 12, 2019
a876f49
TimeZone: Match hash encoding with the Cocoa Foundation overlay
lorentey Apr 15, 2019
b21face
CGFloat: Forward all hashing entry points to the wrapped type
lorentey Apr 12, 2019
456e7af
CharacterSet: Add hashing tests
lorentey Apr 12, 2019
79631e0
NSCharacterSet: Prevent infinite recursion in hash
lorentey Apr 12, 2019
d22bd38
Notification: Add FIXMEs for Equatable/Hashable issues
lorentey Apr 12, 2019
fc63178
Measurement: Fix and modernize hashing
lorentey Apr 12, 2019
0ab9053
AffineTransform: Modernize hashing
lorentey Apr 12, 2019
c81e113
Date: Modernize hashing
lorentey Apr 12, 2019
86a038f
DateInterval: Modernize hashing
lorentey Apr 12, 2019
fa71c3d
Decimal: Modernize hashing
lorentey Apr 12, 2019
dd402b9
IndexPath: Modernize hashing
lorentey Apr 12, 2019
2a60e2f
CGPoint, CGSize, CGRect, NSEdgeInsets: Modernize hashing
lorentey Apr 15, 2019
a921d69
UUID: Modernize hashing
lorentey Apr 15, 2019
ca19ca8
NSRange, NSSpecialValueCoding: Modernize hashing
lorentey Apr 15, 2019
4175bef
Modernize hashing in Foundation’s RawRepresentable enumerations
lorentey Apr 15, 2019
18e010f
CGFloat: Reinstate hashValue docs
lorentey Apr 15, 2019
d69debd
Add new sources to CMakeLists.txt
lorentey Apr 15, 2019
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: 2 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -390,6 +390,7 @@ if(ENABLE_TESTING)
TestFoundation/TestCodable.swift
TestFoundation/TestDateComponents.swift
TestFoundation/TestDateFormatter.swift
TestFoundation/TestDateInterval.swift
TestFoundation/TestDateIntervalFormatter.swift
TestFoundation/TestDate.swift
TestFoundation/TestDecimal.swift
Expand All @@ -407,6 +408,7 @@ if(ENABLE_TESTING)
TestFoundation/TestJSONSerialization.swift
TestFoundation/TestLengthFormatter.swift
TestFoundation/TestMassFormatter.swift
TestFoundation/TestMeasurement.swift
TestFoundation/TestNotificationCenter.swift
TestFoundation/TestNotificationQueue.swift
TestFoundation/TestNotification.swift
Expand Down
8 changes: 8 additions & 0 deletions Foundation.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -354,6 +354,8 @@
7900433C1CACD33E00ECCBF1 /* TestNSPredicate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7900433A1CACD33E00ECCBF1 /* TestNSPredicate.swift */; };
7D0DE86E211883F500540061 /* TestDateComponents.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7D0DE86C211883F500540061 /* TestDateComponents.swift */; };
7D0DE86F211883F500540061 /* Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7D0DE86D211883F500540061 /* Utilities.swift */; };
7D8BD739225ED1480057CF37 /* TestMeasurement.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7D8BD738225ED1480057CF37 /* TestMeasurement.swift */; };
7D8BD737225EADB80057CF37 /* TestDateInterval.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7D8BD736225EADB80057CF37 /* TestDateInterval.swift */; };
90E645DF1E4C89A400D0D47C /* TestNSCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 90E645DE1E4C89A400D0D47C /* TestNSCache.swift */; };
91B668A32252B3C5001487A1 /* FileManager+POSIX.swift in Sources */ = {isa = PBXBuildFile; fileRef = 91B668A22252B3C5001487A1 /* FileManager+POSIX.swift */; };
91B668A52252B3E7001487A1 /* FileManager+Win32.swift in Sources */ = {isa = PBXBuildFile; fileRef = 91B668A42252B3E7001487A1 /* FileManager+Win32.swift */; };
Expand Down Expand Up @@ -881,6 +883,8 @@
7A7D6FBA1C16439400957E2E /* TestURLResponse.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TestURLResponse.swift; sourceTree = "<group>"; };
7D0DE86C211883F500540061 /* TestDateComponents.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TestDateComponents.swift; sourceTree = "<group>"; };
7D0DE86D211883F500540061 /* Utilities.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Utilities.swift; sourceTree = "<group>"; };
7D8BD738225ED1480057CF37 /* TestMeasurement.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestMeasurement.swift; sourceTree = "<group>"; };
7D8BD736225EADB80057CF37 /* TestDateInterval.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestDateInterval.swift; sourceTree = "<group>"; };
83712C8D1C1684900049AD49 /* TestNSURLRequest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TestNSURLRequest.swift; sourceTree = "<group>"; };
844DC3321C17584F005611F9 /* TestScanner.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TestScanner.swift; sourceTree = "<group>"; };
848A30571C137B3500C83206 /* TestHTTPCookie.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = TestHTTPCookie.swift; path = TestFoundation/TestHTTPCookie.swift; sourceTree = SOURCE_ROOT; };
Expand Down Expand Up @@ -1586,6 +1590,7 @@
7D0DE86D211883F500540061 /* Utilities.swift */,
155D3BBB22401D1100B0D38E /* FixtureValues.swift */,
7D0DE86C211883F500540061 /* TestDateComponents.swift */,
7D8BD736225EADB80057CF37 /* TestDateInterval.swift */,
6E203B8C1C1303BB003B2576 /* TestBundle.swift */,
A5A34B551C18C85D00FD972B /* TestByteCountFormatter.swift */,
2EBE67A31C77BF05006583D5 /* TestDateFormatter.swift */,
Expand Down Expand Up @@ -1668,6 +1673,7 @@
5B6F17961C48631C00935030 /* TestUtils.swift */,
03B6F5831F15F339004F25AF /* TestURLProtocol.swift */,
3E55A2321F52463B00082000 /* TestUnit.swift */,
7D8BD738225ED1480057CF37 /* TestMeasurement.swift */,
);
name = Tests;
sourceTree = "<group>";
Expand Down Expand Up @@ -2661,6 +2667,7 @@
2EBE67A51C77BF0E006583D5 /* TestDateFormatter.swift in Sources */,
5B13B3291C582D4C00651CE2 /* TestCalendar.swift in Sources */,
158BCCAA2220A12600750239 /* TestDateIntervalFormatter.swift in Sources */,
7D8BD737225EADB80057CF37 /* TestDateInterval.swift in Sources */,
5B13B34F1C582D4C00651CE2 /* TestXMLParser.swift in Sources */,
BF85E9D81FBDCC2000A79793 /* TestHost.swift in Sources */,
D5C40F331CDA1D460005690C /* TestOperationQueue.swift in Sources */,
Expand All @@ -2678,6 +2685,7 @@
5B13B3511C582D4C00651CE2 /* TestByteCountFormatter.swift in Sources */,
BDFDF0A71DFF5B3E00C04CC5 /* TestPersonNameComponents.swift in Sources */,
5B13B3501C582D4C00651CE2 /* TestUtils.swift in Sources */,
7D8BD739225ED1480057CF37 /* TestMeasurement.swift in Sources */,
CD1C7F7D1E303B47008E331C /* TestNSError.swift in Sources */,
294E3C1D1CC5E19300E4F44C /* TestNSAttributedString.swift in Sources */,
5B13B3431C582D4C00651CE2 /* TestScanner.swift in Sources */,
Expand Down
10 changes: 7 additions & 3 deletions Foundation/AffineTransform.swift
Original file line number Diff line number Diff line change
Expand Up @@ -253,9 +253,13 @@ public struct AffineTransform : ReferenceConvertible, Hashable, CustomStringConv
return newSize
}

/// The computed hash value for the transform.
public var hashValue : Int {
return Int((m11 + m12 + m21 + m22 + tX + tY).native)
public func hash(into hasher: inout Hasher) {
hasher.combine(m11)
hasher.combine(m12)
hasher.combine(m21)
hasher.combine(m22)
hasher.combine(tX)
hasher.combine(tY)
}

/// A textual description of the transform.
Expand Down
6 changes: 6 additions & 0 deletions Foundation/Boxing.swift
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,9 @@ internal protocol _SwiftNativeFoundationType : class {

func mutableCopy(with zone : NSZone) -> Any

func hash(into hasher: inout Hasher)
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In protocol extensions that forward hashing to some core value, it is generally a good idea to implement both hash(into:) and hashValue, so that hashing always matches the behavior of the underlying value. (But if we have to choose one, I'd go with just hash(into:).

var hashValue: Int { get }

var description: String { get }
var debugDescription: String { get }

Expand Down Expand Up @@ -115,6 +117,10 @@ extension _SwiftNativeFoundationType {
return _mapUnmanaged { ($0 as NSObject).mutableCopy() }
}

func hash(into hasher: inout Hasher) {
_mapUnmanaged { hasher.combine($0) }
}

var hashValue: Int {
return _mapUnmanaged { return $0.hashValue }
}
Expand Down
10 changes: 10 additions & 0 deletions Foundation/CGFloat.swift
Original file line number Diff line number Diff line change
Expand Up @@ -477,6 +477,16 @@ extension CGFloat : Hashable {
public var hashValue: Int {
return native.hashValue
}

@_transparent
public func hash(into hasher: inout Hasher) {
hasher.combine(native)
}

@_transparent
public func _rawHashValue(seed: Int) -> Int {
return native._rawHashValue(seed: seed)
}
}

extension UInt8 {
Expand Down
3 changes: 2 additions & 1 deletion Foundation/Calendar.swift
Original file line number Diff line number Diff line change
Expand Up @@ -862,8 +862,9 @@ public struct Calendar : Hashable, Equatable, ReferenceConvertible, _MutableBoxi
public func hash(into hasher: inout Hasher) {
// We implement hash ourselves, because we need to make sure autoupdating calendars have the same hash
if _autoupdating {
hasher.combine(1 as Int8)
hasher.combine(false)
} else {
hasher.combine(true)
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The original implementation here was ever so slightly wrong -- it had a variable-length hash encoding without discriminators to mark its boundaries, which could (theoretically) lead to collisions when multiple Calendar values are fed into a hasher.

Granted, I doubt this would be a problem in practice; however, this change makes Calendar's hash encoding match that of the Foundation overlay, which is nice. I made similar changes for TimeZone and Locale.

hasher.combine(_handle.map { $0 })
}
}
Expand Down
6 changes: 3 additions & 3 deletions Foundation/Date.swift
Original file line number Diff line number Diff line change
Expand Up @@ -136,9 +136,9 @@ public struct Date : ReferenceConvertible, Comparable, Equatable {
The distant past is in terms of centuries.
*/
public static let distantPast = Date(timeIntervalSinceReferenceDate: -63114076800.0)
public var hashValue: Int {
return Int(bitPattern: __CFHashDouble(_time))

public func hash(into hasher: inout Hasher) {
hasher.combine(_time)
}

/// Compare two `Date` values.
Expand Down
11 changes: 3 additions & 8 deletions Foundation/DateInterval.swift
Original file line number Diff line number Diff line change
Expand Up @@ -150,14 +150,9 @@ public struct DateInterval : ReferenceConvertible, Comparable, Hashable {
return false
}

public var hashValue: Int {
var buf: (UInt, UInt) = (UInt(start.timeIntervalSinceReferenceDate), UInt(end.timeIntervalSinceReferenceDate))
return withUnsafeMutablePointer(to: &buf) { bufferPtr in
let count = MemoryLayout<UInt>.size * 2
return bufferPtr.withMemoryRebound(to: UInt8.self, capacity: count) { bufferBytes in
return Int(bitPattern: CFHashBytes(bufferBytes, CFIndex(count)))
}
}
public func hash(into hasher: inout Hasher) {
hasher.combine(start)
hasher.combine(duration)
}

public static func ==(lhs: DateInterval, rhs: DateInterval) -> Bool {
Expand Down
8 changes: 6 additions & 2 deletions Foundation/Decimal.swift
Original file line number Diff line number Diff line change
Expand Up @@ -286,8 +286,12 @@ extension Decimal : Hashable, Comparable {
return Int64(bitPattern: uint64Value)
}

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

public static func ==(lhs: Decimal, rhs: Decimal) -> Bool {
Expand Down
12 changes: 6 additions & 6 deletions Foundation/FileManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -828,11 +828,11 @@ public struct FileAttributeKey : RawRepresentable, Equatable, Hashable {
public init(rawValue: String) {
self.rawValue = rawValue
}
public var hashValue: Int {
return self.rawValue.hashValue

public func hash(into hasher: inout Hasher) {
hasher.combine(rawValue)
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This hash(into:) and the == below are identical to the definitions that the compiler would synthesize on its own. It may be worth considering removing them here (as well as all other RawRepresentable enums).

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree, esp. given that on Darwin it's the compiler that synthesizes those conformances, right?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I looked into it in more detail: on Darwin the hash implementations come from extensions on _SwiftNewtypeWrapper that are identical to similar extensions provided on RawRepresentable. The stdlib also defines a == that’s generic over RawRepresentable types; this gets picked up for Equality conformance.

So if we removed these, then the same definitions would be found on RawRepresentable instead of compiler synthesis. But the end result is effectively the same; we should do it!

}

public static func ==(_ lhs: FileAttributeKey, _ rhs: FileAttributeKey) -> Bool {
return lhs.rawValue == rhs.rawValue
}
Expand Down Expand Up @@ -873,8 +873,8 @@ public struct FileAttributeType : RawRepresentable, Equatable, Hashable {
self.rawValue = rawValue
}

public var hashValue: Int {
return self.rawValue.hashValue
public func hash(into hasher: inout Hasher) {
hasher.combine(rawValue)
}

public static func ==(_ lhs: FileAttributeType, _ rhs: FileAttributeType) -> Bool {
Expand Down
4 changes: 2 additions & 2 deletions Foundation/HTTPCookie.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@ public struct HTTPCookiePropertyKey : RawRepresentable, Equatable, Hashable {
self.rawValue = rawValue
}

public var hashValue: Int {
return self.rawValue.hashValue
public func hash(into hasher: inout Hasher) {
hasher.combine(rawValue)
}

public static func ==(_ lhs: HTTPCookiePropertyKey, _ rhs: HTTPCookiePropertyKey) -> Bool {
Expand Down
38 changes: 22 additions & 16 deletions Foundation/IndexPath.swift
Original file line number Diff line number Diff line change
Expand Up @@ -675,23 +675,29 @@ public struct IndexPath : ReferenceConvertible, Equatable, Hashable, MutableColl
}
return .orderedSame
}

public var hashValue: Int {
func hashIndexes(first: Int, last: Int, count: Int) -> Int {
let totalBits = MemoryLayout<Int>.size * 8
let lengthBits = 8
let firstIndexBits = (totalBits - lengthBits) / 2
return count &+ (first << lengthBits) &+ (last << (lengthBits + firstIndexBits))
}


public func hash(into hasher: inout Hasher) {
// Note: We compare all indices in ==, so for proper hashing, we must
// also feed them all to the hasher.
//
// To ensure we have unique hash encodings in nested hashing contexts,
// we combine the count of indices as well as the indices themselves.
// (This matches what Array does.)
switch _indexes {
case .empty: return 0
case .single(let index): return index.hashValue
case .pair(let first, let second):
return hashIndexes(first: first, last: second, count: 2)
default:
let cnt = _indexes.count
return hashIndexes(first: _indexes[0], last: _indexes[cnt - 1], count: cnt)
case .empty:
hasher.combine(0)
case let .single(index):
hasher.combine(1)
hasher.combine(index)
case let .pair(first, second):
hasher.combine(2)
hasher.combine(first)
hasher.combine(second)
case let .array(indexes):
hasher.combine(indexes.count)
for index in indexes {
hasher.combine(index)
}
}
}

Expand Down
3 changes: 2 additions & 1 deletion Foundation/Locale.swift
Original file line number Diff line number Diff line change
Expand Up @@ -423,8 +423,9 @@ public struct Locale : CustomStringConvertible, CustomDebugStringConvertible, Ha

public func hash(into hasher: inout Hasher) {
if _autoupdating {
hasher.combine(1 as Int8)
hasher.combine(false)
} else {
hasher.combine(true)
hasher.combine(_wrapped)
}
}
Expand Down
19 changes: 17 additions & 2 deletions Foundation/Measurement.swift
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,19 @@ public struct Measurement<UnitType : Unit> : ReferenceConvertible, Comparable, E
self.unit = unit
}

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

Expand Down Expand Up @@ -170,6 +181,10 @@ extension Measurement {
/// 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.
/// - returns: `true` if the measurements are equal.
public static func ==<LeftHandSideType, RightHandSideType>(_ lhs: Measurement<LeftHandSideType>, _ rhs: Measurement<RightHandSideType>) -> Bool {
// Warning: This defines an equivalence relation that needs to be kept
// in perfect sync with the hash(into:) definition above. The floating
// point values that are fed to the hasher there must match exactly with
// the values compared here, or hashing would break.
if lhs.unit == rhs.unit {
return lhs.value == rhs.value
} else {
Expand Down
8 changes: 6 additions & 2 deletions Foundation/NSAttributedString.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,12 @@ extension NSAttributedString {
self.rawValue = rawValue
}

public var hashValue: Int {
return rawValue.hashValue
public func hash(into hasher: inout Hasher) {
hasher.combine(rawValue)
}

public static func ==(left: NSAttributedString.Key, right: NSAttributedString.Key) -> Bool {
return left.rawValue == right.rawValue
}
}
}
Expand Down
6 changes: 3 additions & 3 deletions Foundation/NSCalendar.swift
Original file line number Diff line number Diff line change
Expand Up @@ -65,9 +65,9 @@ extension NSCalendar {
public init(rawValue: String) {
self.rawValue = rawValue
}
public var hashValue: Int {
return rawValue.hashValue

public func hash(into hasher: inout Hasher) {
hasher.combine(rawValue)
}

public static let gregorian = NSCalendar.Identifier("gregorian")
Expand Down
2 changes: 1 addition & 1 deletion Foundation/NSCharacterSet.swift
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ open class NSCharacterSet : NSObject, NSCopying, NSMutableCopying, NSCoding {
}

open override var hash: Int {
return Int(bitPattern: CFHash(_cfObject))
return Int(bitPattern: _CFNonObjCHash(_cfObject))
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This fixes an infinite recursion in NSCharacterSet.hash.

}

open override func isEqual(_ value: Any?) -> Bool {
Expand Down
6 changes: 3 additions & 3 deletions Foundation/NSDecimalNumber.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,9 @@ public struct NSExceptionName : RawRepresentable, Equatable, Hashable {
public init(rawValue: String) {
self.rawValue = rawValue
}
public var hashValue: Int {
return self.rawValue.hashValue

public func hash(into hasher: inout Hasher) {
hasher.combine(rawValue)
}

public static func ==(_ lhs: NSExceptionName, _ rhs: NSExceptionName) -> Bool {
Expand Down
10 changes: 9 additions & 1 deletion Foundation/NSError.swift
Original file line number Diff line number Diff line change
Expand Up @@ -455,6 +455,10 @@ extension __BridgedNSError where Self: RawRepresentable, Self.RawValue: FixedWid
}

public var hashValue: Int { return _code }

public func hash(into hasher: inout Hasher) {
hasher.combine(rawValue)
}
}

/// Describes a raw representable type that is bridged to a particular
Expand Down Expand Up @@ -533,7 +537,11 @@ public extension _BridgedStoredNSError {
/// Implementation of Hashable for all _BridgedStoredNSErrors.
public extension _BridgedStoredNSError {
var hashValue: Int {
return _nsError.hashValue
return _nsError.hash
}

func hash(into hasher: inout Hasher) {
hasher.combine(_nsError)
}
}

Expand Down
Loading