Skip to content

[stdlib] Fix AnyHashable's Equatable/Hashable conformance #17396

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 3 commits into from
Jun 29, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
27 changes: 27 additions & 0 deletions stdlib/private/StdlibUnittest/StdlibUnittest.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2410,6 +2410,33 @@ internal func hash<H: Hashable>(_ 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: Collection>(
_ 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`.
Expand Down
97 changes: 37 additions & 60 deletions stdlib/public/core/AnyHashable.swift
Original file line number Diff line number Diff line change
Expand Up @@ -39,20 +39,29 @@ public protocol _HasCustomAnyHashableRepresentation {

@usableFromInline // FIXME(sil-serialize-all)
internal protocol _AnyHashableBox {
func _unbox<T : Hashable>() -> 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: Hashable>() -> T?
func _downCastConditional<T>(into result: UnsafeMutablePointer<T>) -> Bool
}

extension _AnyHashableBox {
var _canonicalBox: _AnyHashableBox {
return self
}
}

@_fixed_layout // FIXME(sil-serialize-all)
@usableFromInline // FIXME(sil-serialize-all)
internal struct _ConcreteHashableBox<Base : Hashable> : _AnyHashableBox {
Expand Down Expand Up @@ -87,6 +96,11 @@ internal struct _ConcreteHashableBox<Base : Hashable> : _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
Expand All @@ -101,19 +115,6 @@ internal struct _ConcreteHashableBox<Base : Hashable> : _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<T>(_ 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
Expand All @@ -137,8 +138,11 @@ internal func _getBridgedCustomAnyHashable<T>(_ 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.
///
Expand All @@ -160,15 +164,13 @@ public struct AnyHashable {
/// - Parameter base: A hashable value to wrap.
@inlinable // FIXME(sil-serialize-all)
public init<H : Hashable>(_ 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)
Expand All @@ -177,7 +179,6 @@ public struct AnyHashable {
@inlinable // FIXME(sil-serialize-all)
internal init<H : Hashable>(_usingDefaultRepresentationOf base: H) {
self._box = _ConcreteHashableBox(base)
self._usedCustomRepresentation = false
}

/// The value wrapped by this instance.
Expand Down Expand Up @@ -206,13 +207,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

Expand Down Expand Up @@ -248,42 +247,15 @@ 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
}
}

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
Expand All @@ -293,7 +265,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)
}
}

Expand Down
73 changes: 73 additions & 0 deletions stdlib/public/core/Array.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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<Element: Hashable>
: _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 : Hashable>() -> T? {
return _value as? T
}

internal func _downCastConditional<T>(
into result: UnsafeMutablePointer<T>
) -> Bool {
guard let value = _value as? T else { return false }
result.initialize(to: value)
return true
}
}
59 changes: 59 additions & 0 deletions stdlib/public/core/Dictionary.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1520,6 +1520,65 @@ extension Dictionary: Hashable where Value: Hashable {
}
}

extension Dictionary: _HasCustomAnyHashableRepresentation
where Value: Hashable {
public func _toCustomAnyHashable() -> AnyHashable? {
return AnyHashable(_box: _DictionaryAnyHashableBox(self))
}
}

internal struct _DictionaryAnyHashableBox<Key: Hashable, Value: Hashable>
: _AnyHashableBox {
internal let _value: Dictionary<Key, Value>
internal let _canonical: Dictionary<AnyHashable, AnyHashable>

internal init(_ value: Dictionary<Key, Value>) {
self._value = value
self._canonical = value as Dictionary<AnyHashable, AnyHashable>
}

internal var _base: Any {
return _value
}

internal var _canonicalBox: _AnyHashableBox {
return _DictionaryAnyHashableBox<AnyHashable, AnyHashable>(_canonical)
}

internal func _isEqual(to other: _AnyHashableBox) -> Bool? {
guard
let other = other as? _DictionaryAnyHashableBox<AnyHashable, AnyHashable>
else {
return nil
}
return _canonical == other._value
}

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: Hashable>() -> T? {
return _value as? T
}

internal func _downCastConditional<T>(
into result: UnsafeMutablePointer<T>
) -> Bool {
guard let value = _value as? T else { return false }
Copy link
Member

Choose a reason for hiding this comment

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

Hmmm. If there were a fast path for type(of: T) == Dictionary<AnyHashable, AnyHashable>.self, would it obviate the need for _asDictionary, because _isEqual would be able to efficiently use _downCastConditional? (Sorry, same question as before).

Copy link
Member Author

@lorentey lorentey Jun 28, 2018

Choose a reason for hiding this comment

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

Not sure I get this, but FWIW, it sure would be helpful if we could analyze the structure of T at runtime and detect that it's a Dictionary, extract its Key & Value types and perform an elementwise downcast/conversion from _value:

if case Dictionary<let K, let V> = T { ... }

There is a similar issue with _SwiftNewtypeWrapper, which uses its rawValue's AnyHashable representation, but can't easily be downcast from it. ("hello" as AnyHashable as? NSAttributedString.Key should be non-nil, but (as far as I know), _ConcreteAnyHashableBox<String>._downCastConditional<T> can't detect that the target type conforms to _SwiftNewtypeWrapper where RawValue == String, so we have to go through Foundation.

result.initialize(to: value)
return true
}
}

extension Dictionary: CustomStringConvertible, CustomDebugStringConvertible {
@inlinable // FIXME(sil-serialize-all)
internal func _makeDescription() -> String {
Expand Down
Loading