From ff91f36a9db8eb1e58b3dab25962248eec1142d6 Mon Sep 17 00:00:00 2001 From: Karoy Lorentey Date: Thu, 3 May 2018 18:23:59 +0100 Subject: [PATCH 1/3] [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 --- .../StdlibUnittest/StdlibUnittest.swift | 27 ++ stdlib/public/core/AnyHashable.swift | 106 +++--- stdlib/public/core/Array.swift | 73 ++++ stdlib/public/core/Dictionary.swift | 55 +++ .../public/core/FloatingPointTypes.swift.gyb | 88 +++++ stdlib/public/core/Hasher.swift | 1 + stdlib/public/core/Integers.swift.gyb | 74 ++++ stdlib/public/core/NewtypeWrapper.swift | 77 ++++- stdlib/public/core/Set.swift | 53 +++ test/stdlib/AnyHashableCasts.swift.gyb | 123 ++++++- validation-test/stdlib/AnyHashable.swift.gyb | 315 ++++++++++++------ .../DictionaryAnyHashableExtensions.swift | 129 ++++--- .../stdlib/SetAnyHashableExtensions.swift | 76 ++++- 13 files changed, 957 insertions(+), 240 deletions(-) diff --git a/stdlib/private/StdlibUnittest/StdlibUnittest.swift b/stdlib/private/StdlibUnittest/StdlibUnittest.swift index 99798f3ab1289..71b4f60a2bd2b 100644 --- a/stdlib/private/StdlibUnittest/StdlibUnittest.swift +++ b/stdlib/private/StdlibUnittest/StdlibUnittest.swift @@ -2410,6 +2410,33 @@ internal func hash(_ value: H, seed: Int? = nil) -> Int { return hasher.finalize() } +/// Test that the elements of `groups` consist of instances that satisfy the +/// semantic requirements of `Hashable`, with each group defining a distinct +/// equivalence class under `==`. +public func checkHashableGroups( + _ groups: Groups, + _ message: @autoclosure () -> String = "", + stackTrace: SourceLocStack = SourceLocStack(), + showFrame: Bool = true, + file: String = #file, line: UInt = #line +) where Groups.Element: Collection, Groups.Element.Element: Hashable { + let instances = groups.flatMap { $0 } + // groupIndices[i] is the index of the element in groups that contains + // instances[i]. + let groupIndices = + zip(0..., groups).flatMap { i, group in group.map { _ in i } } + func equalityOracle(_ lhs: Int, _ rhs: Int) -> Bool { + return groupIndices[lhs] == groupIndices[rhs] + } + checkHashable( + instances, + equalityOracle: equalityOracle, + hashEqualityOracle: equalityOracle, + allowBrokenTransitivity: false, + stackTrace: stackTrace.pushIf(showFrame, file: file, line: line), + showFrame: false) +} + /// Test that the elements of `instances` satisfy the semantic requirements of /// `Hashable`, using `equalityOracle` to generate equality and hashing /// expectations from pairs of positions in `instances`. diff --git a/stdlib/public/core/AnyHashable.swift b/stdlib/public/core/AnyHashable.swift index 56dc92d87c44c..436018750301f 100644 --- a/stdlib/public/core/AnyHashable.swift +++ b/stdlib/public/core/AnyHashable.swift @@ -39,18 +39,36 @@ public protocol _HasCustomAnyHashableRepresentation { @usableFromInline // FIXME(sil-serialize-all) internal protocol _AnyHashableBox { - func _unbox() -> T? + var _canonicalBox: _AnyHashableBox { get } /// Determine whether values in the boxes are equivalent. /// + /// - Precondition: `self` and `box` are in canonical form. /// - Returns: `nil` to indicate that the boxes store different types, so /// no comparison is possible. Otherwise, contains the result of `==`. - func _isEqual(to: _AnyHashableBox) -> Bool? + func _isEqual(to box: _AnyHashableBox) -> Bool? var _hashValue: Int { get } func _hash(into hasher: inout Hasher) + func _rawHashValue(_seed: (UInt64, UInt64)) -> Int var _base: Any { get } + func _unbox() -> T? func _downCastConditional(into result: UnsafeMutablePointer) -> Bool + + func _asSet() -> Set? + func _asDictionary() -> Dictionary? +} + +extension _AnyHashableBox { + var _canonicalBox: _AnyHashableBox { + return self + } + func _asSet() -> Set? { + return nil + } + func _asDictionary() -> Dictionary? { + return nil + } } @_fixed_layout // FIXME(sil-serialize-all) @@ -87,6 +105,11 @@ internal struct _ConcreteHashableBox : _AnyHashableBox { _baseHashable.hash(into: &hasher) } + @inlinable // FIXME(sil-serialize-all) + func _rawHashValue(_seed: (UInt64, UInt64)) -> Int { + return _baseHashable._rawHashValue(seed: _seed) + } + @inlinable // FIXME(sil-serialize-all) internal var _base: Any { return _baseHashable @@ -101,19 +124,6 @@ internal struct _ConcreteHashableBox : _AnyHashableBox { } } -#if _runtime(_ObjC) -// Retrieve the custom AnyHashable representation of the value after it -// has been bridged to Objective-C. This mapping to Objective-C and back -// turns a non-custom representation into a custom one, which is used as -// the lowest-common-denominator for comparisons. -@inlinable // FIXME(sil-serialize-all) -internal func _getBridgedCustomAnyHashable(_ value: T) -> AnyHashable? { - let bridgedValue = _bridgeAnythingToObjectiveC(value) - return (bridgedValue as? - _HasCustomAnyHashableRepresentation)?._toCustomAnyHashable() -} -#endif - /// A type-erased hashable value. /// /// The `AnyHashable` type forwards equality comparisons and hashing operations @@ -137,8 +147,11 @@ internal func _getBridgedCustomAnyHashable(_ value: T) -> AnyHashable? { public struct AnyHashable { @usableFromInline // FIXME(sil-serialize-all) internal var _box: _AnyHashableBox - @usableFromInline // FIXME(sil-serialize-all) - internal var _usedCustomRepresentation: Bool + + @inlinable // FIXME(sil-serialize-all) + internal init(_box box: _AnyHashableBox) { + self._box = box + } /// Creates a type-erased hashable value that wraps the given instance. /// @@ -160,15 +173,13 @@ public struct AnyHashable { /// - Parameter base: A hashable value to wrap. @inlinable // FIXME(sil-serialize-all) public init(_ base: H) { - if let customRepresentation = + if let custom = (base as? _HasCustomAnyHashableRepresentation)?._toCustomAnyHashable() { - self = customRepresentation - self._usedCustomRepresentation = true + self = custom return } - self._box = _ConcreteHashableBox(0 as Int) - self._usedCustomRepresentation = false + self.init(_box: _ConcreteHashableBox(false)) // Dummy value _makeAnyHashableUpcastingToHashableBaseType( base, storingResultInto: &self) @@ -177,7 +188,6 @@ public struct AnyHashable { @inlinable // FIXME(sil-serialize-all) internal init(_usingDefaultRepresentationOf base: H) { self._box = _ConcreteHashableBox(base) - self._usedCustomRepresentation = false } /// The value wrapped by this instance. @@ -206,13 +216,11 @@ public struct AnyHashable { if _box._downCastConditional(into: result) { return true } #if _runtime(_ObjC) - // If we used a custom representation, bridge to Objective-C and then - // attempt the cast from there. - if _usedCustomRepresentation { - if let value = _bridgeAnythingToObjectiveC(_box._base) as? T { - result.initialize(to: value) - return true - } + // Bridge to Objective-C and then attempt the cast from there. + // FIXME: This should also work without the Objective-C runtime. + if let value = _bridgeAnythingToObjectiveC(_box._base) as? T { + result.initialize(to: value) + return true } #endif @@ -248,34 +256,7 @@ extension AnyHashable : Equatable { /// - rhs: Another type-erased hashable value. @inlinable // FIXME(sil-serialize-all) public static func == (lhs: AnyHashable, rhs: AnyHashable) -> Bool { - // If they're equal, we're done. - if let result = lhs._box._isEqual(to: rhs._box) { return result } - - #if _runtime(_ObjC) - // If one used a custom representation but the other did not, bridge - // the one that did *not* use the custom representation to Objective-C: - // if the bridged result has a custom representation, compare those custom - // custom representations. - if lhs._usedCustomRepresentation != rhs._usedCustomRepresentation { - // If the lhs used a custom representation, try comparing against the - // custom representation of the bridged rhs (if there is one). - if lhs._usedCustomRepresentation { - if let customRHS = _getBridgedCustomAnyHashable(rhs._box._base) { - return lhs._box._isEqual(to: customRHS._box) ?? false - } - return false - } - - // Otherwise, try comparing the rhs against the custom representation of - // the bridged lhs (if there is one). - if let customLHS = _getBridgedCustomAnyHashable(lhs._box._base) { - return customLHS._box._isEqual(to: rhs._box) ?? false - } - return false - } - #endif - - return false + return lhs._box._canonicalBox._isEqual(to: rhs._box._canonicalBox) ?? false } } @@ -283,7 +264,7 @@ extension AnyHashable : Hashable { /// The hash value. @inlinable public var hashValue: Int { - return _box._hashValue + return _box._canonicalBox._hashValue } /// Hashes the essential components of this value by feeding them into the @@ -293,7 +274,12 @@ extension AnyHashable : Hashable { /// of this instance. @inlinable public func hash(into hasher: inout Hasher) { - _box._hash(into: &hasher) + _box._canonicalBox._hash(into: &hasher) + } + + @inlinable // FIXME(sil-serialize-all) + public func _rawHashValue(seed: (UInt64, UInt64)) -> Int { + return _box._canonicalBox._rawHashValue(_seed: seed) } } diff --git a/stdlib/public/core/Array.swift b/stdlib/public/core/Array.swift index 2fd105716dccb..a23c6d165b105 100644 --- a/stdlib/public/core/Array.swift +++ b/stdlib/public/core/Array.swift @@ -1758,3 +1758,76 @@ extension Array { } } #endif + +extension Array: _HasCustomAnyHashableRepresentation + where Element: Hashable { + public func _toCustomAnyHashable() -> AnyHashable? { + return AnyHashable(_box: _ArrayAnyHashableBox(self)) + } +} + +internal protocol _ArrayAnyHashableProtocol: _AnyHashableBox { + var count: Int { get } + subscript(index: Int) -> AnyHashable { get } +} + +internal struct _ArrayAnyHashableBox + : _ArrayAnyHashableProtocol { + internal let _value: [Element] + + internal init(_ value: [Element]) { + self._value = value + } + + internal var _base: Any { + return _value + } + + internal var count: Int { + return _value.count + } + + internal subscript(index: Int) -> AnyHashable { + return _value[index] as AnyHashable + } + + func _isEqual(to other: _AnyHashableBox) -> Bool? { + guard let other = other as? _ArrayAnyHashableProtocol else { return nil } + guard _value.count == other.count else { return false } + for i in 0 ..< _value.count { + if self[i] != other[i] { return false } + } + return true + } + + var _hashValue: Int { + var hasher = Hasher() + _hash(into: &hasher) + return hasher.finalize() + } + + func _hash(into hasher: inout Hasher) { + hasher.combine(_value.count) // discriminator + for i in 0 ..< _value.count { + hasher.combine(self[i]) + } + } + + func _rawHashValue(_seed: (UInt64, UInt64)) -> Int { + var hasher = Hasher(_seed: _seed) + self._hash(into: &hasher) + return hasher._finalize() + } + + internal func _unbox() -> T? { + return _value as? T + } + + internal func _downCastConditional( + into result: UnsafeMutablePointer + ) -> Bool { + guard let value = _value as? T else { return false } + result.initialize(to: value) + return true + } +} diff --git a/stdlib/public/core/Dictionary.swift b/stdlib/public/core/Dictionary.swift index dd71afe2ccbcc..335f97a1dfa6c 100644 --- a/stdlib/public/core/Dictionary.swift +++ b/stdlib/public/core/Dictionary.swift @@ -1520,6 +1520,61 @@ extension Dictionary: Hashable where Value: Hashable { } } +extension Dictionary: _HasCustomAnyHashableRepresentation +where Value: Hashable { + public func _toCustomAnyHashable() -> AnyHashable? { + return AnyHashable(_box: _DictionaryAnyHashableBox(self)) + } +} + +internal struct _DictionaryAnyHashableBox + : _AnyHashableBox { + internal let _value: Dictionary + internal let _canonical: Dictionary + + internal init(_ value: Dictionary) { + self._value = value + self._canonical = value as Dictionary + } + + internal var _base: Any { + return _value + } + + internal func _isEqual(to other: _AnyHashableBox) -> Bool? { + guard let other = other._asDictionary() else { return nil } + return _canonical == other + } + + internal var _hashValue: Int { + return _canonical.hashValue + } + + internal func _hash(into hasher: inout Hasher) { + _canonical.hash(into: &hasher) + } + + internal func _rawHashValue(_seed: (UInt64, UInt64)) -> Int { + return _canonical._rawHashValue(seed: _seed) + } + + internal func _unbox() -> T? { + return _value as? T + } + + internal func _downCastConditional( + into result: UnsafeMutablePointer + ) -> Bool { + guard let value = _value as? T else { return false } + result.initialize(to: value) + return true + } + + internal func _asDictionary() -> Dictionary? { + return _canonical + } +} + extension Dictionary: CustomStringConvertible, CustomDebugStringConvertible { @inlinable // FIXME(sil-serialize-all) internal func _makeDescription() -> String { diff --git a/stdlib/public/core/FloatingPointTypes.swift.gyb b/stdlib/public/core/FloatingPointTypes.swift.gyb index 35311baf34a3d..5936bf2a02b7b 100644 --- a/stdlib/public/core/FloatingPointTypes.swift.gyb +++ b/stdlib/public/core/FloatingPointTypes.swift.gyb @@ -1560,6 +1560,13 @@ extension ${Self} : Hashable { } } +extension ${Self}: _HasCustomAnyHashableRepresentation { + // Not @inlinable + public func _toCustomAnyHashable() -> AnyHashable? { + return AnyHashable(_box: _${Self}AnyHashableBox(self)) + } +} + extension ${Self} { /// The magnitude of this value. /// @@ -1810,6 +1817,87 @@ extension ${Self} : Strideable { } } +//===----------------------------------------------------------------------===// +// AnyHashable +//===----------------------------------------------------------------------===// + +internal struct _${Self}AnyHashableBox: _AnyHashableBox { + internal typealias Base = ${Self} + internal let _value: Base + + internal init(_ value: Base) { + self._value = value + } + + internal var _canonicalBox: _AnyHashableBox { + // Float and Double are bridged with NSNumber, so we have to follow + // NSNumber's rules for equality. I.e., we need to make sure equal + // numerical values end up in identical boxes after canonicalization, so + // that _isEqual will consider them equal and they're hashed the same way. + // + // Note that these AnyHashable boxes don't currently feed discriminator bits + // to the hasher, so we allow repeatable collisions. E.g., -1 will always + // collide with UInt64.max. + if _value < 0 { + if let i = Int64(exactly: _value) { + return _IntegerAnyHashableBox(i) + } + } else { + if let i = UInt64(exactly: _value) { + return _IntegerAnyHashableBox(i) + } + } + if let d = Double(exactly: _value) { + return _DoubleAnyHashableBox(d) + } + // If a value can't be represented by a Double, keep it in its original + // representation so that it won't compare equal to approximations. (So that + // we don't round off Float80 values.) + return self + } + + internal func _isEqual(to box: _AnyHashableBox) -> Bool? { + _sanityCheck(Int64(exactly: _value) == nil, "self isn't canonical") + _sanityCheck(UInt64(exactly: _value) == nil, "self isn't canonical") + if let box = box as? _${Self}AnyHashableBox { + return _value == box._value + } + return nil + } + + internal var _hashValue: Int { + return _rawHashValue(_seed: Hasher._seed) + } + + internal func _hash(into hasher: inout Hasher) { + _sanityCheck(Int64(exactly: _value) == nil, "self isn't canonical") + _sanityCheck(UInt64(exactly: _value) == nil, "self isn't canonical") + hasher.combine(_value) + } + + internal func _rawHashValue(_seed: (UInt64, UInt64)) -> Int { + var hasher = Hasher(_seed: _seed) + _hash(into: &hasher) + return hasher.finalize() + } + + internal var _base: Any { + return _value + } + + internal func _unbox() -> T? { + return _value as? T + } + + internal func _downCastConditional( + into result: UnsafeMutablePointer + ) -> Bool { + guard let value = _value as? T else { return false } + result.initialize(to: value) + return true + } +} + //===----------------------------------------------------------------------===// // Deprecated operators //===----------------------------------------------------------------------===// diff --git a/stdlib/public/core/Hasher.swift b/stdlib/public/core/Hasher.swift index 0aaf1a146fdba..5e48f11eda87b 100644 --- a/stdlib/public/core/Hasher.swift +++ b/stdlib/public/core/Hasher.swift @@ -437,6 +437,7 @@ public struct Hasher { seed: (UInt64, UInt64), bytes value: UInt64, count: Int) -> Int { + _sanityCheck(count >= 0 && count < 8) var core = RawCore(seed: seed) let tbc = _HasherTailBuffer(tail: value, byteCount: count) return Int(truncatingIfNeeded: core.finalize(tailAndByteCount: tbc.value)) diff --git a/stdlib/public/core/Integers.swift.gyb b/stdlib/public/core/Integers.swift.gyb index 784a5fdb5cf47..50e728faeb3d3 100644 --- a/stdlib/public/core/Integers.swift.gyb +++ b/stdlib/public/core/Integers.swift.gyb @@ -3920,6 +3920,13 @@ extension ${Self} : Hashable { } } +extension ${Self} : _HasCustomAnyHashableRepresentation { + // Not @inlinable + public func _toCustomAnyHashable() -> AnyHashable? { + return AnyHashable(_box: _IntegerAnyHashableBox(self)) + } +} + // Create an ambiguity when indexing or slicing // Range[OfStrideable]<${Self}> outside a generic context. See @@ -4270,6 +4277,73 @@ extension SignedInteger where Self : FixedWidthInteger { % end } +internal struct _IntegerAnyHashableBox< + Base: FixedWidthInteger +>: _AnyHashableBox { + internal let _value: Base + + internal init(_ value: Base) { + self._value = value + } + + internal var _canonicalBox: _AnyHashableBox { + // We need to follow NSNumber semantics here; the AnyHashable forms of + // integer types holding the same mathematical value should compare equal. + // Sign-extend value to a 64-bit integer. This will generate hash conflicts + // between, say -1 and UInt.max, but that's fine. + if _value < 0 { + return _IntegerAnyHashableBox(Int64(truncatingIfNeeded: _value)) + } + return _IntegerAnyHashableBox(UInt64(truncatingIfNeeded: _value)) + } + + internal func _isEqual(to box: _AnyHashableBox) -> Bool? { + if Base.self == UInt64.self { + guard let box = box as? _IntegerAnyHashableBox else { return nil } + return _value == box._value + } + if Base.self == Int64.self { + guard let box = box as? _IntegerAnyHashableBox else { return nil } + return _value == box._value + } + _preconditionFailure("self isn't canonical") + } + + internal var _hashValue: Int { + _sanityCheck(Base.self == UInt64.self || Base.self == Int64.self, + "self isn't canonical") + return _value.hashValue + } + + internal func _hash(into hasher: inout Hasher) { + _sanityCheck(Base.self == UInt64.self || Base.self == Int64.self, + "self isn't canonical") + _value.hash(into: &hasher) + } + + internal func _rawHashValue(_seed: (UInt64, UInt64)) -> Int { + _sanityCheck(Base.self == UInt64.self || Base.self == Int64.self, + "self isn't canonical") + return _value._rawHashValue(seed: _seed) + } + + internal var _base: Any { + return _value + } + + internal func _unbox() -> T? { + return _value as? T + } + + internal func _downCastConditional( + into result: UnsafeMutablePointer + ) -> Bool { + guard let value = _value as? T else { return false } + result.initialize(to: value) + return true + } +} + // ${'Local Variables'}: // eval: (read-only-mode 1) // End: diff --git a/stdlib/public/core/NewtypeWrapper.swift b/stdlib/public/core/NewtypeWrapper.swift index 5024c9d17d9e6..d8bd13e150de5 100644 --- a/stdlib/public/core/NewtypeWrapper.swift +++ b/stdlib/public/core/NewtypeWrapper.swift @@ -13,9 +13,10 @@ /// An implementation detail used to implement support importing /// (Objective-)C entities marked with the swift_newtype Clang /// attribute. -public protocol _SwiftNewtypeWrapper : RawRepresentable { } +public protocol _SwiftNewtypeWrapper +: RawRepresentable, _HasCustomAnyHashableRepresentation { } -extension _SwiftNewtypeWrapper where Self: Hashable, Self.RawValue : Hashable { +extension _SwiftNewtypeWrapper where Self: Hashable, Self.RawValue: Hashable { /// The hash value. @inlinable public var hashValue: Int { @@ -31,6 +32,78 @@ extension _SwiftNewtypeWrapper where Self: Hashable, Self.RawValue : Hashable { public func hash(into hasher: inout Hasher) { hasher.combine(rawValue) } + + @inlinable // FIXME(sil-serialize-all) + public func _rawHashValue(seed: (UInt64, UInt64)) -> Int { + return rawValue._rawHashValue(seed: seed) + } +} + +extension _SwiftNewtypeWrapper { + public func _toCustomAnyHashable() -> AnyHashable? { + return nil + } +} + +extension _SwiftNewtypeWrapper where Self: Hashable, Self.RawValue: Hashable { + public func _toCustomAnyHashable() -> AnyHashable? { + return AnyHashable(_box: _NewtypeWrapperAnyHashableBox(self)) + } +} + +internal struct _NewtypeWrapperAnyHashableBox: _AnyHashableBox +where Base: _SwiftNewtypeWrapper & Hashable, Base.RawValue: Hashable { + var _value: Base + + init(_ value: Base) { + self._value = value + } + + var _canonicalBox: _AnyHashableBox { + return (_value.rawValue as AnyHashable)._box._canonicalBox + } + + func _isEqual(to other: _AnyHashableBox) -> Bool? { + _preconditionFailure("_isEqual called on non-canonical AnyHashable box") + } + + var _hashValue: Int { + _preconditionFailure("_hashValue called on non-canonical AnyHashable box") + } + + func _hash(into hasher: inout Hasher) { + _preconditionFailure("_hash(into:) called on non-canonical AnyHashable box") + } + + func _rawHashValue(_seed: (UInt64, UInt64)) -> Int { + _preconditionFailure("_rawHashValue(_seed:) called on non-canonical AnyHashable box") + } + + var _base: Any { return _value } + + func _unbox() -> T? { + return _value as? T ?? _value.rawValue as? T + } + + func _downCastConditional(into result: UnsafeMutablePointer) -> Bool { + if let value = _value as? T { + result.initialize(to: value) + return true + } + if let value = _value.rawValue as? T { + result.initialize(to: value) + return true + } + return false + } + + func _asSet() -> Set? { + return _canonicalBox._asSet() + } + + func _asDictionary() -> Dictionary? { + return _canonicalBox._asDictionary() + } } #if _runtime(_ObjC) diff --git a/stdlib/public/core/Set.swift b/stdlib/public/core/Set.swift index 1441f79980a2a..83899832bd9a6 100644 --- a/stdlib/public/core/Set.swift +++ b/stdlib/public/core/Set.swift @@ -503,6 +503,59 @@ extension Set: Hashable { } } +extension Set: _HasCustomAnyHashableRepresentation { + public func _toCustomAnyHashable() -> AnyHashable? { + return AnyHashable(_box: _SetAnyHashableBox(self)) + } +} + +internal struct _SetAnyHashableBox: _AnyHashableBox { + internal let _value: Set + internal let _canonical: Set + + internal init(_ value: Set) { + self._value = value + self._canonical = value as Set + } + + internal var _base: Any { + return _value + } + + internal func _isEqual(to other: _AnyHashableBox) -> Bool? { + guard let other = other._asSet() else { return nil } + return _canonical == other + } + + internal var _hashValue: Int { + return _canonical.hashValue + } + + internal func _hash(into hasher: inout Hasher) { + _canonical.hash(into: &hasher) + } + + func _rawHashValue(_seed: (UInt64, UInt64)) -> Int { + return _canonical._rawHashValue(seed: _seed) + } + + internal func _unbox() -> T? { + return _value as? T + } + + internal func _downCastConditional( + into result: UnsafeMutablePointer + ) -> Bool { + guard let value = _value as? T else { return false } + result.initialize(to: value) + return true + } + + internal func _asSet() -> Set? { + return _canonical + } +} + extension Set: SetAlgebra { /// Inserts the given element in the set if it is not already present. diff --git a/test/stdlib/AnyHashableCasts.swift.gyb b/test/stdlib/AnyHashableCasts.swift.gyb index 4eaaf507c427e..3a6f81fe27392 100644 --- a/test/stdlib/AnyHashableCasts.swift.gyb +++ b/test/stdlib/AnyHashableCasts.swift.gyb @@ -1,10 +1,10 @@ // RUN: %empty-directory(%t) // // RUN: %gyb %s -o %t/AnyHashableCasts.swift -// RUN: %target-build-swift -g -module-name a %t/AnyHashableCasts.swift -o %t.out -// RUN: %target-run %t.out -// RUN: %target-build-swift -g -O -module-name a %t/AnyHashableCasts.swift -o %t.out.optimized -// RUN: %target-run %t.out.optimized +// RUN: %line-directive %t/AnyHashableCasts.swift -- %target-build-swift -g -module-name a %t/AnyHashableCasts.swift -o %t.out +// RUN: %line-directive %t/AnyHashableCasts.swift -- %target-run %t.out +// RUN: %line-directive %t/AnyHashableCasts.swift -- %target-build-swift -g -O -module-name a %t/AnyHashableCasts.swift -o %t.out.optimized +// RUN: %line-directive %t/AnyHashableCasts.swift -- %target-run %t.out.optimized // REQUIRES: executable_test import StdlibUnittest @@ -117,34 +117,129 @@ AnyHashableCasts.test("${valueExpr} as ${coercedType} as? ${castType}") { % end #if _runtime(_ObjC) +// A wrapper type around Int that bridges to NSNumber. +struct IntWrapper1: _SwiftNewtypeWrapper, Hashable, _ObjectiveCBridgeable { + let rawValue: Int +} + +// A wrapper type around Int that bridges to NSNumber. +struct IntWrapper2: _SwiftNewtypeWrapper, Hashable, _ObjectiveCBridgeable { + let rawValue: Int +} + +AnyHashableCasts.test("Wrappers around bridged integers") { + let wrapper1: AnyHashable = IntWrapper1(rawValue: 42) + let wrapper2: AnyHashable = IntWrapper2(rawValue: 42) + let integer: AnyHashable = 42 as Int + let byte: AnyHashable = 42 as UInt8 + let double: AnyHashable = 42.0 as Double + let number: AnyHashable = 42 as NSNumber + + // Wrappers compare equal to their wrapped value as AnyHashable. + expectEqual(wrapper1, wrapper2) + expectEqual(wrapper1, integer) + expectEqual(wrapper1, byte) + expectEqual(wrapper1, double) + expectEqual(wrapper1, number) + + // Original types are preserved in the base property. + expectTrue(wrapper1.base is IntWrapper1) + expectTrue(wrapper2.base is IntWrapper2) + expectTrue(integer.base is Int) + expectTrue(byte.base is UInt8) + expectTrue(double.base is Double) + expectTrue(number.base is NSNumber) // Through bridging + + // AnyHashable forms can be casted to any standard numeric type that can hold + // their value. + expectNotNil(wrapper1 as? IntWrapper1) + expectNotNil(wrapper1 as? IntWrapper2) + expectNotNil(wrapper1 as? Int) + expectNotNil(wrapper1 as? UInt8) + expectNotNil(wrapper1 as? Double) + expectNotNil(wrapper1 as? NSNumber) + + expectNotNil(byte as? IntWrapper1) + expectNotNil(byte as? IntWrapper2) + expectNotNil(byte as? Int) + expectNotNil(byte as? UInt8) + expectNotNil(byte as? Double) + expectNotNil(byte as? NSNumber) + + expectNotNil(integer as? IntWrapper1) + expectNotNil(integer as? IntWrapper2) + expectNotNil(integer as? Int) + expectNotNil(integer as? UInt8) + expectNotNil(integer as? Double) + expectNotNil(integer as? NSNumber) + + expectNotNil(double as? IntWrapper1) + expectNotNil(double as? IntWrapper2) + expectNotNil(double as? Int) + expectNotNil(double as? UInt8) + expectNotNil(double as? Double) + expectNotNil(double as? NSNumber) + + expectNotNil(number as? IntWrapper1) + expectNotNil(number as? IntWrapper2) + expectNotNil(number as? Int) + expectNotNil(number as? UInt8) + expectNotNil(number as? Double) + expectNotNil(number as? NSNumber) + + // We can't cast to a numeric type that can't hold the value. + let big: AnyHashable = Int32.max + expectNotNil(big as? IntWrapper1) + expectNotNil(big as? IntWrapper2) + expectNotNil(big as? Int) + expectNil(big as? UInt8) // <-- + expectNotNil(big as? Double) + expectNotNil(big as? NSNumber) +} + // A wrapper type around a String that bridges to NSString. -struct StringWrapper1 : _SwiftNewtypeWrapper, Hashable, _ObjectiveCBridgeable { +struct StringWrapper1: _SwiftNewtypeWrapper, Hashable, _ObjectiveCBridgeable { let rawValue: String } // A wrapper type around a String that bridges to NSString. -struct StringWrapper2 : _SwiftNewtypeWrapper, Hashable, _ObjectiveCBridgeable { +struct StringWrapper2: _SwiftNewtypeWrapper, Hashable, _ObjectiveCBridgeable { let rawValue: String } -AnyHashableCasts.test("Wrappers around bridged types") { +AnyHashableCasts.test("Wrappers around bridged strings") { let wrapper1Hello: AnyHashable = StringWrapper1(rawValue: "hello") + let wrapper2Hello: AnyHashable = StringWrapper2(rawValue: "hello") let stringHello: AnyHashable = "hello" as String let nsStringHello: AnyHashable = "hello" as NSString - // Casting from Swift wrapper maintains type identity + // Wrappers compare equal to their wrapped value as AnyHashable. + expectEqual(wrapper1Hello, wrapper2Hello) + expectEqual(wrapper1Hello, stringHello) + expectEqual(wrapper1Hello, nsStringHello) + expectEqual(wrapper2Hello, stringHello) + expectEqual(wrapper2Hello, nsStringHello) + expectEqual(stringHello, nsStringHello) + + // Type identity is maintained through the base property. + expectTrue(wrapper1Hello.base is StringWrapper1) + expectTrue(wrapper2Hello.base is StringWrapper2) + expectTrue(stringHello.base is String) + expectTrue(nsStringHello.base is NSString) // Through bridging + + // Swift wrapper's AnyHashable form doesn't enfore type identity. expectNotNil(wrapper1Hello as? StringWrapper1) - expectNil(wrapper1Hello as? StringWrapper2) - expectNil(wrapper1Hello as? String) + expectNotNil(wrapper1Hello as? StringWrapper2) + expectNotNil(wrapper1Hello as? String) expectNotNil(wrapper1Hello as? NSString) - // Casting from String maintains type identity - expectNil(stringHello as? StringWrapper1) - expectNil(stringHello as? StringWrapper2) + // String's AnyHashable form doesn't enfore type identity. + expectNotNil(stringHello as? StringWrapper1) + expectNotNil(stringHello as? StringWrapper2) expectNotNil(stringHello as? String) expectNotNil(stringHello as? NSString) - // Casting form NSString works with anything. + // NSString's AnyHashable form doesn't enfore type identity. expectNotNil(nsStringHello as? StringWrapper1) expectNotNil(nsStringHello as? StringWrapper2) expectNotNil(nsStringHello as? String) diff --git a/validation-test/stdlib/AnyHashable.swift.gyb b/validation-test/stdlib/AnyHashable.swift.gyb index 05084ad4bdd8e..c6fa6cfd2650d 100644 --- a/validation-test/stdlib/AnyHashable.swift.gyb +++ b/validation-test/stdlib/AnyHashable.swift.gyb @@ -767,6 +767,132 @@ AnyHashableTests.test("AnyHashable(MinimalHashableRCSwiftError).base") { expectEqual(MinimalHashableRCSwiftError.self, type(of: ah.base)) } +AnyHashableTests.test("AnyHashable(NumericTypes)/Hashable") { + // Numeric types holding mathematically equal values must compare equal and + // hash the same way when converted to AnyHashable. + let groups: [[AnyHashable]] = [ + [ + 1 as Int, + 1 as UInt, + 1 as Int8, + 1 as UInt8, + 1 as Int16, + 1 as UInt16, + 1 as Int32, + 1 as UInt32, + 1 as Int64, + 1 as UInt64, + 1 as Float, + 1 as Double, + ], + [ + 42 as Int, + 42 as UInt, + 42 as Int8, + 42 as UInt8, + 42 as Int16, + 42 as UInt16, + 42 as Int32, + 42 as UInt32, + 42 as Int64, + 42 as UInt64, + 42 as Float, + 42 as Double, + ], + [ + Int(Int32.max), + UInt(Int32.max), + Int32.max, + UInt32(Int32.max), + Int64(Int32.max), + UInt64(Int32.max), + Double(Int32.max), + ], + [ + Float.infinity, + Double.infinity, + ], + [ + 0x1.aP1 as Float, // 3.25 + 0x1.aP1 as Double, + ], + [ + 0x1.a000000000001P1, // 3.25.nextUp, not representable by a Float + ] + ] + checkHashableGroups(groups) +} + +#if !os(Windows) && (arch(i386) || arch(x86_64)) +AnyHashableTests.test("AnyHashable(Float80)/Hashable") { + let groups: [[AnyHashable]] = [ + [ + 42 as Int, + 42 as Float, + 42 as Double, + 42 as Float80, + ], + [ + Float.infinity, + Double.infinity, + Float80.infinity, + ], + [ + 3.25 as Float, + 3.25 as Double, + 3.25 as Float80, + ], + [ + 0x1.a000000000001P1 as Double, // 3.25.nextUp + 0x1.a000000000001P1 as Float80, + ], + [ + 0x1.a000000000000002p1 as Float80, // (3.25 as Float80).nextUp + ], + ] + checkHashableGroups(groups) +} +#endif + +#if _runtime(_ObjC) +// A wrapper type around an Int that bridges to NSNumber. +struct IntWrapper1 : _SwiftNewtypeWrapper, Hashable, _ObjectiveCBridgeable { + let rawValue: Int +} + +// A wrapper type around an Int that bridges to NSNumber. +struct IntWrapper2 : _SwiftNewtypeWrapper, Hashable, _ObjectiveCBridgeable { + let rawValue: Int +} + +// A wrapper type around an Int that bridges to NSNumber. +struct Int8Wrapper : _SwiftNewtypeWrapper, Hashable, _ObjectiveCBridgeable { + let rawValue: Int8 +} + +AnyHashableTests.test("AnyHashable(IntWrappers)/Hashable") { + let groups: [[AnyHashable]] = [ + [ + IntWrapper1(rawValue: 42), + IntWrapper2(rawValue: 42), + Int8Wrapper(rawValue: 42), + 42, + 42 as Double, + 42 as NSNumber, + ], + [ + IntWrapper1(rawValue: -23), + IntWrapper2(rawValue: -23), + Int8Wrapper(rawValue: -23), + -23, + -23 as Double, + -23 as NSNumber, + ], + ] + checkHashableGroups(groups) +} +#endif + #if _runtime(_ObjC) // A wrapper type around a String that bridges to NSString. struct StringWrapper1 : _SwiftNewtypeWrapper, Hashable, _ObjectiveCBridgeable { @@ -778,126 +904,101 @@ struct StringWrapper2 : _SwiftNewtypeWrapper, Hashable, _ObjectiveCBridgeable { let rawValue: String } -AnyHashableTests.test("AnyHashable(Wrappers)/Hashable") { - let values: [AnyHashable] = [ - StringWrapper1(rawValue: "hello"), - StringWrapper2(rawValue: "hello"), - "hello" as String, - "hello" as NSString, - StringWrapper1(rawValue: "world"), - StringWrapper2(rawValue: "world"), - "world" as String, - "world" as NSString, +AnyHashableTests.test("AnyHashable(StringWrappers)/Hashable") { + let groups: [[AnyHashable]] = [ + [ + StringWrapper1(rawValue: "hello"), + StringWrapper2(rawValue: "hello"), + "hello" as String, + "hello" as NSString, + ], + [ + StringWrapper1(rawValue: "world"), + StringWrapper2(rawValue: "world"), + "world" as String, + "world" as NSString, + ] ] - - func equalityOracle(_ lhs: Int, _ rhs: Int) -> Bool { - // Elements in [0, 3] match 3. - if lhs == 3 { return rhs >= 0 && rhs <= 3 } - if rhs == 3 { return lhs >= 0 && lhs <= 3 } - - // Elements in [4, 7] match 7. - if lhs == 7 { return rhs >= 4 && rhs <= 7 } - if rhs == 7 { return lhs >= 4 && lhs <= 7 } - - return lhs == rhs - } - - func hashEqualityOracle(_ lhs: Int, _ rhs: Int) -> Bool { - // Elements in [0, 3] hash the same, as do elements in [4, 7]. - return lhs / 4 == rhs / 4 - } - - checkHashable( - values, - equalityOracle: equalityOracle, - hashEqualityOracle: hashEqualityOracle, - allowBrokenTransitivity: true) + checkHashableGroups(groups) } AnyHashableTests.test("AnyHashable(Set)/Hashable") { - let values: [AnyHashable] = [ - Set([1, 2, 3]), - NSSet(set: [1, 2, 3]), - Set([2, 3, 4]), - NSSet(set: [2, 3, 4]), - Set([Set([1, 2]), Set([3, 4])]), - NSSet(set: [NSSet(set: [1, 2]), NSSet(set: [3, 4])]), - Set([Set([1, 3]), Set([2, 4])]), - NSSet(set: [NSSet(set: [1, 3]), NSSet(set: [2, 4])]), + let groups: [[AnyHashable]] = [ + [ + Set([1, 2, 3]), + Set([1, 2, 3] as [Int8]), + Set([1, 2, 3] as [Float]), + NSSet(set: [1, 2, 3]), + ], + [ + Set([2, 3, 4]), + NSSet(set: [2, 3, 4]), + ], + [ + Set([Set([1, 2]), Set([3, 4])]), + NSSet(set: [NSSet(set: [1, 2]), NSSet(set: [3, 4])]), + ], + [ + Set([Set([1, 3]), Set([2, 4])]), + NSSet(set: [NSSet(set: [1, 3]), NSSet(set: [2, 4])]), + ], ] - - func equalityOracle(_ lhs: Int, _ rhs: Int) -> Bool { - switch (lhs, rhs) { - case (0...1, 0...1): return true - case (2...3, 2...3): return true - case (4...5, 4...5): return true - case (6...7, 6...7): return true - default: return false - } - } - - checkHashable( - values, - equalityOracle: equalityOracle, - allowBrokenTransitivity: true) + checkHashableGroups(groups) } AnyHashableTests.test("AnyHashable(Array)/Hashable") { - let values: [AnyHashable] = [ - [1, 2, 3], - NSArray(array: [1, 2, 3]), - [3, 2, 1], - NSArray(array: [3, 2, 1]), - [[1, 2], [3, 4]], - NSArray(array: [NSArray(array: [1, 2]), NSArray(array: [3, 4])]), - [[3, 4], [1, 2]], - NSArray(array: [NSArray(array: [3, 4]), NSArray(array: [1, 2])]), + let groups: [[AnyHashable]] = [ + [ + [1, 2, 3], + [1, 2, 3] as [Int8], + [1, 2, 3] as [Double], + NSArray(array: [1, 2, 3]), + ], + [ + [3, 2, 1], + [3, 2, 1] as [AnyHashable], + NSArray(array: [3, 2, 1]), + ], + [ + [[1, 2], [3, 4]], + NSArray(array: [NSArray(array: [1, 2]), NSArray(array: [3, 4])]), + ], + [ + [[3, 4], [1, 2]], + NSArray(array: [NSArray(array: [3, 4]), NSArray(array: [1, 2])]), + ] ] - - func equalityOracle(_ lhs: Int, _ rhs: Int) -> Bool { - switch (lhs, rhs) { - case (0...1, 0...1): return true - case (2...3, 2...3): return true - case (4...5, 4...5): return true - case (6...7, 6...7): return true - default: return false - } - } - - checkHashable(values, equalityOracle: equalityOracle, - allowBrokenTransitivity: true) + checkHashableGroups(groups) } AnyHashableTests.test("AnyHashable(Dictionary)/Hashable") { - let values: [AnyHashable] = [ - ["hello": 1, "world": 2], - NSDictionary(dictionary: ["hello": 1, "world": 2]), - ["hello": 2, "world": 1], - NSDictionary(dictionary: ["hello": 2, "world": 1]), - ["hello": ["foo": 1, "bar": 2], - "world": ["foo": 2, "bar": 1]], - NSDictionary(dictionary: [ - "hello": ["foo": 1, "bar": 2], - "world": ["foo": 2, "bar": 1]]), - ["hello": ["foo": 2, "bar": 1], - "world": ["foo": 1, "bar": 2]], - NSDictionary(dictionary: [ - "hello": ["foo": 2, "bar": 1], - "world": ["foo": 1, "bar": 2]]), + let groups: [[AnyHashable]] = [ + [ + ["hello": 1, "world": 2] as [String: Int], + ["hello": 1, "world": 2] as [String: Int16], + ["hello": 1, "world": 2] as [String: Float], + NSDictionary(dictionary: ["hello": 1, "world": 2]), + ], + [ + ["hello": 2, "world": 1], + NSDictionary(dictionary: ["hello": 2, "world": 1]), + ], + [ + ["hello": ["foo": 1, "bar": 2], + "world": ["foo": 2, "bar": 1]], + NSDictionary(dictionary: [ + "hello": ["foo": 1, "bar": 2], + "world": ["foo": 2, "bar": 1]]), + ], + [ + ["hello": ["foo": 2, "bar": 1], + "world": ["foo": 1, "bar": 2]], + NSDictionary(dictionary: [ + "hello": ["foo": 2, "bar": 1], + "world": ["foo": 1, "bar": 2]]), + ], ] - - func equalityOracle(_ lhs: Int, _ rhs: Int) -> Bool { - switch (lhs, rhs) { - case (0...1, 0...1): return true - case (2...3, 2...3): return true - case (4...5, 4...5): return true - case (6...7, 6...7): return true - default: return false - } - } - - checkHashable(values, equalityOracle: equalityOracle, - allowBrokenTransitivity: true) + checkHashableGroups(groups) } AnyHashableTests.test("AnyHashable(_SwiftNativeNSError(MinimalHashablePODSwiftError))/Hashable") { diff --git a/validation-test/stdlib/DictionaryAnyHashableExtensions.swift b/validation-test/stdlib/DictionaryAnyHashableExtensions.swift index 8cfaaa6030df2..9d577945f9eba 100644 --- a/validation-test/stdlib/DictionaryAnyHashableExtensions.swift +++ b/validation-test/stdlib/DictionaryAnyHashableExtensions.swift @@ -6,26 +6,73 @@ import StdlibUnittest var DictionaryTests = TestSuite("Dictionary") DictionaryTests.test("index(forKey:)") { - let d: [AnyHashable : Int] = [ - AnyHashable(10) : 1010, - AnyHashable(20) : 2020, - AnyHashable(30.0) : 3030, + let a = AnyHashable(10 as UInt16) + let b = AnyHashable(20) + let c = AnyHashable(30.0) + let d: [AnyHashable: Int] = [ + a: 1010, + b: 2020, + c: 3030, ] - expectEqual(1010, d[d.index(forKey: 10)!].value) - expectEqual(2020, d[d.index(forKey: 20)!].value) - expectEqual(3030, d[d.index(forKey: 30.0)!].value) - - expectNil(d.index(forKey: 10.0)) - expectNil(d.index(forKey: 20.0)) - expectNil(d.index(forKey: 30)) + for (key, k, value) in [(a, 10, 1010), (b, 20, 2020), (c, 30, 3030)] { + let index = d.index(forKey: key)! + expectEqual(value, d[index].value) + // We must be able to look up the same number in any representation. + expectEqual(index, d.index(forKey: UInt8(k))) + expectEqual(index, d.index(forKey: UInt16(k))) + expectEqual(index, d.index(forKey: UInt32(k))) + expectEqual(index, d.index(forKey: UInt64(k))) + expectEqual(index, d.index(forKey: UInt(k))) + expectEqual(index, d.index(forKey: Int8(k))) + expectEqual(index, d.index(forKey: Int16(k))) + expectEqual(index, d.index(forKey: Int32(k))) + expectEqual(index, d.index(forKey: Int64(k))) + expectEqual(index, d.index(forKey: Int(k))) + expectEqual(index, d.index(forKey: Float(k))) + expectEqual(index, d.index(forKey: Double(k))) + + expectNil(d.index(forKey: String(k))) + } } DictionaryTests.test("subscript(_:)") { - var d: [AnyHashable : Int] = [ - AnyHashable(10) : 1010, - AnyHashable(20) : 2020, - AnyHashable(30.0) : 3030, + let a = AnyHashable(10 as UInt16) + let b = AnyHashable(20) + let c = AnyHashable(30.0) + let d: [AnyHashable: Int] = [ + a: 1010, + b: 2020, + c: 3030, + ] + + for (key, k, value) in [(a, 10, 1010), (b, 20, 2020), (c, 30, 3030)] { + let index = d.index(forKey: key)! + expectEqual(value, d[key]) + // We must be able to look up the same number in any representation. + expectEqual(value, d[UInt8(k)]) + expectEqual(value, d[UInt16(k)]) + expectEqual(value, d[UInt32(k)]) + expectEqual(value, d[UInt64(k)]) + expectEqual(value, d[UInt(k)]) + expectEqual(value, d[Int8(k)]) + expectEqual(value, d[Int16(k)]) + expectEqual(value, d[Int32(k)]) + expectEqual(value, d[Int64(k)]) + expectEqual(value, d[Int(k)]) + expectEqual(value, d[Float(k)]) + expectEqual(value, d[Double(k)]) + + expectNil(d[String(k)]) + } +} + + +DictionaryTests.test("subscript(_:)/2") { + var d: [AnyHashable: Int] = [ + AnyHashable(10): 1010, + AnyHashable(20): 2020, + AnyHashable(30.0): 3030, ] expectEqual(1010, d[10]) @@ -100,57 +147,61 @@ DictionaryTests.test("updateValue(_:forKey:)") { expectEqual(expected, d) } - expectNil(d.updateValue(4040, forKey: 10.0)) + expectEqual(101010, d.updateValue(4040, forKey: 10.0)) do { let expected: [AnyHashable : Int] = [ - AnyHashable(10) : 101010, + AnyHashable(10) : 4040, AnyHashable(20) : 202020, AnyHashable(30.0) : 303030, - AnyHashable(10.0) : 4040, ] expectEqual(expected, d) } - expectNil(d.updateValue(5050, forKey: 20.0)) + expectEqual(202020, d.updateValue(5050, forKey: 20.0)) do { let expected: [AnyHashable : Int] = [ - AnyHashable(10) : 101010, - AnyHashable(20) : 202020, + AnyHashable(10) : 4040, + AnyHashable(20) : 5050, AnyHashable(30.0) : 303030, - AnyHashable(10.0) : 4040, - AnyHashable(20.0) : 5050, ] expectEqual(expected, d) } - expectNil(d.updateValue(6060, forKey: 30)) + expectEqual(303030, d.updateValue(6060, forKey: 30)) do { let expected: [AnyHashable : Int] = [ - AnyHashable(10) : 101010, - AnyHashable(20) : 202020, - AnyHashable(30.0) : 303030, - AnyHashable(10.0) : 4040, - AnyHashable(20.0) : 5050, - AnyHashable(30) : 6060, + AnyHashable(10) : 4040, + AnyHashable(20) : 5050, + AnyHashable(30.0) : 6060, ] expectEqual(expected, d) } } DictionaryTests.test("removeValue(forKey:)") { - var d: [AnyHashable : Int] = [ - AnyHashable(10) : 1010, + let d: [AnyHashable : Int] = [ + AnyHashable(10 as UInt8) : 1010, AnyHashable(20) : 2020, AnyHashable(30.0) : 3030, ] - expectNil(d.removeValue(forKey: 10.0)) - expectNil(d.removeValue(forKey: 20.0)) - expectNil(d.removeValue(forKey: 30)) - - expectEqual(1010, d.removeValue(forKey: 10)!) - expectEqual(2020, d.removeValue(forKey: 20)!) - expectEqual(3030, d.removeValue(forKey: 30.0)!) + for (key, value) in [(10, 1010), (20, 2020), (30, 3030)] { + var dd = d + expectEqual(value, dd.removeValue(forKey: UInt8(key))) + dd = d; expectEqual(value, dd.removeValue(forKey: UInt16(key))) + dd = d; expectEqual(value, dd.removeValue(forKey: UInt32(key))) + dd = d; expectEqual(value, dd.removeValue(forKey: UInt64(key))) + dd = d; expectEqual(value, dd.removeValue(forKey: UInt(key))) + dd = d; expectEqual(value, dd.removeValue(forKey: Int8(key))) + dd = d; expectEqual(value, dd.removeValue(forKey: Int16(key))) + dd = d; expectEqual(value, dd.removeValue(forKey: Int32(key))) + dd = d; expectEqual(value, dd.removeValue(forKey: Int64(key))) + dd = d; expectEqual(value, dd.removeValue(forKey: Int(key))) + dd = d; expectEqual(value, dd.removeValue(forKey: Float(key))) + dd = d; expectEqual(value, dd.removeValue(forKey: Double(key))) + + dd = d; expectNil(dd.removeValue(forKey: String(key))) + } } runAllTests() diff --git a/validation-test/stdlib/SetAnyHashableExtensions.swift b/validation-test/stdlib/SetAnyHashableExtensions.swift index 3ab8cea2682f9..54ef1f72f2d20 100644 --- a/validation-test/stdlib/SetAnyHashableExtensions.swift +++ b/validation-test/stdlib/SetAnyHashableExtensions.swift @@ -28,28 +28,45 @@ var SetTests = TestSuite("Set") SetTests.test("contains(_:)") { let s: Set = [ - AnyHashable(1010), AnyHashable(2020), AnyHashable(3030.0) + AnyHashable(1010 as UInt16), AnyHashable(2020), AnyHashable(3030.0) ] - expectTrue(s.contains(1010)) - expectTrue(s.contains(2020)) - expectTrue(s.contains(3030.0)) - - expectFalse(s.contains(1010.0)) - expectFalse(s.contains(2020.0)) - expectFalse(s.contains(3030)) + for i in [1010, 2020, 3030] { + // We must be able to look up the same number in any representation. + expectTrue(s.contains(UInt16(i))) + expectTrue(s.contains(UInt32(i))) + expectTrue(s.contains(UInt64(i))) + expectTrue(s.contains(UInt(i))) + expectTrue(s.contains(Int16(i))) + expectTrue(s.contains(Int32(i))) + expectTrue(s.contains(Int64(i))) + expectTrue(s.contains(Int(i))) + expectTrue(s.contains(Float(i))) + expectTrue(s.contains(Double(i))) + + expectFalse(s.contains(String(i))) + } } SetTests.test("index(of:)") { - let s: Set = [ - AnyHashable(1010), AnyHashable(2020), AnyHashable(3030.0) - ] - expectEqual(AnyHashable(1010), s[s.firstIndex(of: 1010)!]) - expectEqual(AnyHashable(2020), s[s.firstIndex(of: 2020)!]) - expectEqual(AnyHashable(3030.0), s[s.firstIndex(of: 3030.0)!]) - - expectNil(s.firstIndex(of: 1010.0)) - expectNil(s.firstIndex(of: 2020.0)) - expectNil(s.firstIndex(of: 3030)) + let a = AnyHashable(1010 as UInt16) + let b = AnyHashable(2020) + let c = AnyHashable(3030.0) + let s: Set = [a, b, c] + for (element, i) in [(a, 1010), (b, 2020), (c, 3030)] { + let index = s.firstIndex(of: element)! + + // We must be able to look up the same number in any representation. + expectEqual(index, s.firstIndex(of: UInt16(i))) + expectEqual(index, s.firstIndex(of: UInt32(i))) + expectEqual(index, s.firstIndex(of: UInt64(i))) + expectEqual(index, s.firstIndex(of: UInt(i))) + expectEqual(index, s.firstIndex(of: Int16(i))) + expectEqual(index, s.firstIndex(of: Int32(i))) + expectEqual(index, s.firstIndex(of: Int64(i))) + expectEqual(index, s.firstIndex(of: Int(i))) + expectEqual(index, s.firstIndex(of: Float(i))) + expectEqual(index, s.firstIndex(of: Double(i))) + } } SetTests.test("insert(_:)") { @@ -230,5 +247,28 @@ SetTests.test("remove(_:)/CastTrap") s.remove(TestHashableDerivedB(2020, identity: 2)) } +SetTests.test("Hashable/Conversions") { + let input: [Set] = [ + [10 as UInt8, 20 as UInt8, 30 as UInt8], + [10 as UInt16, 20 as UInt16, 30 as UInt16], + [10 as UInt32, 20 as UInt32, 30 as UInt32], + [10 as UInt64, 20 as UInt64, 30 as UInt64], + [10 as UInt, 20 as UInt, 30 as UInt], + [10 as Int8, 20 as Int8, 30 as Int8], + [10 as Int16, 20 as Int16, 30 as Int16], + [10 as Int32, 20 as Int32, 30 as Int32], + [10 as Int64, 20 as Int64, 30 as Int64], + [10 as Int, 20 as Int, 30 as Int], + [10 as Float, 20 as Float, 30 as Float], + [10 as Double, 20 as Double, 30 as Double], + [[1, 2, 3] as Set, [2, 3, 4] as Set, [3, 4, 5] as Set], + [[1, 2, 3] as Set, [2, 3, 4] as Set, [3, 4, 5] as Set], + [[1, 2, 3] as Set, [2, 3, 4] as Set, [3, 4, 5] as Set], + ] + + checkHashable(input, equalityOracle: { ($0 < 12) == ($1 < 12) }) +} + + runAllTests() From bf872ec1577cc686223eda901bd4ae674c442a86 Mon Sep 17 00:00:00 2001 From: Karoy Lorentey Date: Thu, 3 May 2018 18:23:59 +0100 Subject: [PATCH 2/3] [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. --- stdlib/public/core/Integers.swift.gyb | 33 +++++---------------- validation-test/stdlib/FixedPoint.swift.gyb | 18 ++++++----- 2 files changed, 19 insertions(+), 32 deletions(-) diff --git a/stdlib/public/core/Integers.swift.gyb b/stdlib/public/core/Integers.swift.gyb index 50e728faeb3d3..31de38b033380 100644 --- a/stdlib/public/core/Integers.swift.gyb +++ b/stdlib/public/core/Integers.swift.gyb @@ -3885,37 +3885,20 @@ extension ${Self} : Hashable { /// of this instance. @inlinable public func hash(into hasher: inout Hasher) { - // FIXME(hasher): To correctly bridge `Set`s/`Dictionary`s containing - // `AnyHashable`-boxed integers, all integer values are currently required - // to hash exactly the same way as the corresponding (U)Int64 value. To fix - // this, we should introduce a custom AnyHashable box for integer values - // that sign-extends values to 64 bits. - % if bits <= word_bits: - hasher._combine(_lowWord) - % elif bits == 2 * word_bits: - if let word = ${"" if signed else "U"}Int(exactly: self) { - hasher._combine(word._lowWord) - } else { - hasher._combine(UInt64(_value)) - } - % else: - fatalError("Unsupported integer width") - % end + hasher._combine(${U}${Self}(_value)) } @inlinable public func _rawHashValue(seed: (UInt64, UInt64)) -> Int { - // FIXME(hasher): Note that the AnyHashable concern applies here too, - // because hashValue uses top-level hashing. - % if bits <= word_bits: - return Hasher._hash(seed: seed, _lowWord) - % elif bits == 2 * word_bits: - if let word = ${"" if signed else "U"}Int(exactly: self) { - return Hasher._hash(seed: seed, word._lowWord) - } + % if bits == 64: return Hasher._hash(seed: seed, UInt64(_value)) + % elif bits == word_bits: + return Hasher._hash(seed: seed, UInt(_value)) % else: - fatalError("Unsupported integer width") + return Hasher._hash( + seed: seed, + bytes: UInt64(truncatingIfNeeded: ${U}${Self}(_value)), + count: ${bits / 8}) % end } } diff --git a/validation-test/stdlib/FixedPoint.swift.gyb b/validation-test/stdlib/FixedPoint.swift.gyb index 7ac2f9d2a5ce1..dd371bfe74d04 100644 --- a/validation-test/stdlib/FixedPoint.swift.gyb +++ b/validation-test/stdlib/FixedPoint.swift.gyb @@ -233,22 +233,26 @@ hash_value_test_template = gyb.parse_template("hash_value", % for self_ty in all_integer_types(word_bits): % Self = self_ty.stdlib_name -FixedPoint.test("${Self}.hashValue") { +FixedPoint.test("${Self}.hash(into:)") { % for bit_pattern in test_bit_patterns: do { % input = prepare_bit_pattern(bit_pattern, self_ty.bits, self_ty.is_signed) let input = get${Self}(${input}) - let output = getInt(input.hashValue) + var hasher = Hasher() + input.hash(into: &hasher) + let output = getInt(hasher.finalize()) - var hasher = _SipHash13(_seed: Hasher._seed) -% if prepare_bit_pattern(input, word_bits, self_ty.is_signed) == input: - hasher._combine(UInt(truncatingIfNeeded: ${input} as ${"" if self_ty.is_signed else "U"}Int)) +% reference = prepare_bit_pattern(bit_pattern, self_ty.bits, False) +% if self_ty.bits == 64: + let expected = Hasher._hash(seed: Hasher._seed, ${reference} as UInt64) % else: - hasher._combine(UInt64(truncatingIfNeeded: input)) + let expected = Hasher._hash( + seed: Hasher._seed, + bytes: ${reference}, + count: ${self_ty.bits / 8}) % end - let expected = getInt(Int(truncatingIfNeeded: hasher.finalize())) expectEqual(expected, output, "input: \(input)") } From 2f4ad7982de9f1e5b29f7d8ee275ada74e0b0809 Mon Sep 17 00:00:00 2001 From: Karoy Lorentey Date: Thu, 28 Jun 2018 18:30:54 +0100 Subject: [PATCH 3/3] [stdlib] Eliminate _AnyHashableBox._asSet() & ._asDictionary() _canonicalBox can perform essentially the same job, so there is no reason to have these requirements. --- stdlib/public/core/AnyHashable.swift | 9 --------- stdlib/public/core/Dictionary.swift | 16 ++++++++++------ stdlib/public/core/NewtypeWrapper.swift | 8 -------- stdlib/public/core/Set.swift | 14 ++++++++------ 4 files changed, 18 insertions(+), 29 deletions(-) diff --git a/stdlib/public/core/AnyHashable.swift b/stdlib/public/core/AnyHashable.swift index 436018750301f..23b1341be9a4b 100644 --- a/stdlib/public/core/AnyHashable.swift +++ b/stdlib/public/core/AnyHashable.swift @@ -54,21 +54,12 @@ internal protocol _AnyHashableBox { var _base: Any { get } func _unbox() -> T? func _downCastConditional(into result: UnsafeMutablePointer) -> Bool - - func _asSet() -> Set? - func _asDictionary() -> Dictionary? } extension _AnyHashableBox { var _canonicalBox: _AnyHashableBox { return self } - func _asSet() -> Set? { - return nil - } - func _asDictionary() -> Dictionary? { - return nil - } } @_fixed_layout // FIXME(sil-serialize-all) diff --git a/stdlib/public/core/Dictionary.swift b/stdlib/public/core/Dictionary.swift index 335f97a1dfa6c..cc3657b19de74 100644 --- a/stdlib/public/core/Dictionary.swift +++ b/stdlib/public/core/Dictionary.swift @@ -1541,9 +1541,17 @@ internal struct _DictionaryAnyHashableBox return _value } + internal var _canonicalBox: _AnyHashableBox { + return _DictionaryAnyHashableBox(_canonical) + } + internal func _isEqual(to other: _AnyHashableBox) -> Bool? { - guard let other = other._asDictionary() else { return nil } - return _canonical == other + guard + let other = other as? _DictionaryAnyHashableBox + else { + return nil + } + return _canonical == other._value } internal var _hashValue: Int { @@ -1569,10 +1577,6 @@ internal struct _DictionaryAnyHashableBox result.initialize(to: value) return true } - - internal func _asDictionary() -> Dictionary? { - return _canonical - } } extension Dictionary: CustomStringConvertible, CustomDebugStringConvertible { diff --git a/stdlib/public/core/NewtypeWrapper.swift b/stdlib/public/core/NewtypeWrapper.swift index d8bd13e150de5..c9493a59a48ad 100644 --- a/stdlib/public/core/NewtypeWrapper.swift +++ b/stdlib/public/core/NewtypeWrapper.swift @@ -96,14 +96,6 @@ where Base: _SwiftNewtypeWrapper & Hashable, Base.RawValue: Hashable { } return false } - - func _asSet() -> Set? { - return _canonicalBox._asSet() - } - - func _asDictionary() -> Dictionary? { - return _canonicalBox._asDictionary() - } } #if _runtime(_ObjC) diff --git a/stdlib/public/core/Set.swift b/stdlib/public/core/Set.swift index 83899832bd9a6..0730b3070b7b8 100644 --- a/stdlib/public/core/Set.swift +++ b/stdlib/public/core/Set.swift @@ -522,9 +522,15 @@ internal struct _SetAnyHashableBox: _AnyHashableBox { return _value } + internal var _canonicalBox: _AnyHashableBox { + return _SetAnyHashableBox(_canonical) + } + internal func _isEqual(to other: _AnyHashableBox) -> Bool? { - guard let other = other._asSet() else { return nil } - return _canonical == other + guard let other = other as? _SetAnyHashableBox else { + return nil + } + return _canonical == other._value } internal var _hashValue: Int { @@ -550,10 +556,6 @@ internal struct _SetAnyHashableBox: _AnyHashableBox { result.initialize(to: value) return true } - - internal func _asSet() -> Set? { - return _canonical - } } extension Set: SetAlgebra {