Skip to content

[3.0] [AnyHashable] Handle comparisons/casting for wrappers around bridged types #4953

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 1 commit into from
Sep 23, 2016
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
71 changes: 66 additions & 5 deletions stdlib/public/core/AnyHashable.swift
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,11 @@ internal protocol _AnyHashableBox {
var _typeID: ObjectIdentifier { get }
func _unbox<T : Hashable>() -> T?

func _isEqual(to: _AnyHashableBox) -> Bool
/// Determine whether values in the boxes are equivalent.
///
/// - 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?
var _hashValue: Int { get }

var _base: Any { get }
Expand All @@ -62,11 +66,11 @@ internal struct _ConcreteHashableBox<Base : Hashable> : _AnyHashableBox {
return (self as _AnyHashableBox as? _ConcreteHashableBox<T>)?._baseHashable
}

internal func _isEqual(to rhs: _AnyHashableBox) -> Bool {
internal func _isEqual(to rhs: _AnyHashableBox) -> Bool? {
if let rhs: Base = rhs._unbox() {
return _baseHashable == rhs
}
return false
return nil
}

internal var _hashValue: Int {
Expand All @@ -85,6 +89,18 @@ 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.
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 @@ -106,6 +122,7 @@ internal struct _ConcreteHashableBox<Base : Hashable> : _AnyHashableBox {
/// print(descriptions[AnyHashable(Set(["a", "b"]))]!) // prints "a set of strings"
public struct AnyHashable {
internal var _box: _AnyHashableBox
internal var _usedCustomRepresentation: Bool

/// Creates a type-erased hashable value that wraps the given instance.
///
Expand All @@ -129,17 +146,20 @@ public struct AnyHashable {
if let customRepresentation =
(base as? _HasCustomAnyHashableRepresentation)?._toCustomAnyHashable() {
self = customRepresentation
self._usedCustomRepresentation = true
return
}

self._box = _ConcreteHashableBox(0 as Int)
self._usedCustomRepresentation = false
_stdlib_makeAnyHashableUpcastingToHashableBaseType(
base,
storingResultInto: &self)
}

internal init<H : Hashable>(_usingDefaultRepresentationOf base: H) {
self._box = _ConcreteHashableBox(base)
self._usedCustomRepresentation = false
}

/// The value wrapped by this instance.
Expand All @@ -162,7 +182,21 @@ public struct AnyHashable {
/// a downcast on `base`.
internal
func _downCastConditional<T>(into result: UnsafeMutablePointer<T>) -> Bool {
return _box._downCastConditional(into: result)
// Attempt the downcast.
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
}
}
#endif

return false
}
}

Expand Down Expand Up @@ -193,7 +227,34 @@ extension AnyHashable : Equatable {
/// - lhs: A type-erased hashable value.
/// - rhs: Another type-erased hashable value.
public static func == (lhs: AnyHashable, rhs: AnyHashable) -> Bool {
return lhs._box._isEqual(to: rhs._box)
// 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
}
}

Expand Down
41 changes: 41 additions & 0 deletions test/1_stdlib/AnyHashableCasts.swift.gyb
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@

import StdlibUnittest

#if _runtime(_ObjC)
import Foundation
#endif

var AnyHashableCasts = TestSuite("AnyHashableCasts")

protocol Implemented {}
Expand Down Expand Up @@ -112,4 +116,41 @@ AnyHashableCasts.test("${valueExpr} as ${coercedType} as? ${castType}") {
}
% end

#if _runtime(_ObjC)
// A wrapper type around a String that bridges to NSString.
struct StringWrapper1 : _SwiftNewtypeWrapper, Hashable, _ObjectiveCBridgeable {
let rawValue: String
}

// A wrapper type around a String that bridges to NSString.
struct StringWrapper2 : _SwiftNewtypeWrapper, Hashable, _ObjectiveCBridgeable {
let rawValue: String
}

AnyHashableCasts.test("Wrappers around bridged types") {
let wrapper1Hello: AnyHashable = StringWrapper1(rawValue: "hello")
let stringHello: AnyHashable = "hello" as String
let nsStringHello: AnyHashable = "hello" as NSString

// Casting from Swift wrapper maintains type identity
expectNotEmpty(wrapper1Hello as? StringWrapper1)
expectEmpty(wrapper1Hello as? StringWrapper2)
expectEmpty(wrapper1Hello as? String)
expectNotEmpty(wrapper1Hello as? NSString)

// Casting from String maintains type identity
expectEmpty(stringHello as? StringWrapper1)
expectEmpty(stringHello as? StringWrapper2)
expectNotEmpty(stringHello as? String)
expectNotEmpty(stringHello as? NSString)

// Casting form NSString works with anything.
expectNotEmpty(nsStringHello as? StringWrapper1)
expectNotEmpty(nsStringHello as? StringWrapper2)
expectNotEmpty(nsStringHello as? String)
expectNotEmpty(nsStringHello as? NSString)
}

#endif

runAllTests()
40 changes: 40 additions & 0 deletions validation-test/stdlib/AnyHashable.swift.gyb
Original file line number Diff line number Diff line change
Expand Up @@ -735,6 +735,46 @@ AnyHashableTests.test("AnyHashable(MinimalHashableRCSwiftError).base") {
expectEqual(MinimalHashableRCSwiftError.self, type(of: ah.base))
}

#if _runtime(_ObjC)
// A wrapper type around a String that bridges to NSString.
struct StringWrapper1 : _SwiftNewtypeWrapper, Hashable, _ObjectiveCBridgeable {
let rawValue: String
}

// A wrapper type around a String that bridges to NSString.
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,
]

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
}

checkHashable(values, equalityOracle: equalityOracle,
allowBrokenTransitivity: true)
}
#endif

#if _runtime(_ObjC)
AnyHashableTests.test("AnyHashable(_SwiftNativeNSError(MinimalHashablePODSwiftError))/Hashable") {
let swiftErrors: [MinimalHashablePODSwiftError] = [
Expand Down