Skip to content

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

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

Conversation

lorentey
Copy link
Member

@lorentey lorentey commented Jun 26, 2018

(Cherry-picked from #17396, removing support for single-shot hashing. See that PR for more details.)

Explanation: AnyHashable violated Equatable/Hashable semantic requirements for numeric values, newtypes with bridged raw values, and bridgeable collections of these, which could result in unexpected behavior and/or traps when items of these types were inserted into heterogeneous sets/dictionaries. Completes the implementation of SE-0206 by implementing type-dependent hashing for standard integer types. (The requirement violations aren't new in 4.2, but completing SE-0206 would've added a bunch more cases, increasing urgency.)
Scope: Affects semantics of Set<AnyHashable>/Dictionary<AnyHashable, Any> when elements are numeric types or newtypes. (Objective-C interfaces using NSSet/NSDictionary without type annotations get imported as these.) Changes integer hashing.
Issue: https://bugs.swift.org/browse/SR-7496, rdar://problem/39648819
Risk: Medium. This has received extensive testing, but AnyHashable is subtle.
Testing: Swift stdlib tests via Swift CI, including new tests constructed specifically for these issues.
Reviewer: @DougGregor

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
(cherry picked from commit ff91f36)

# Conflicts:
#	stdlib/public/core/AnyHashable.swift
#	stdlib/public/core/Arrays.swift.gyb
#	stdlib/public/core/Hasher.swift
#	stdlib/public/core/Integers.swift.gyb
@lorentey
Copy link
Member Author

@swift-ci test

@lorentey
Copy link
Member Author

@swift-ci test source compatibility

@swift-ci
Copy link
Contributor

Build failed
Swift Test Linux Platform
Git Sha - 3802b8c3c49e43f86368caa3fe32d0fa09f995f1

@swift-ci
Copy link
Contributor

Build failed
Swift Test OS X Platform
Git Sha - 3802b8c3c49e43f86368caa3fe32d0fa09f995f1

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.

(cherry picked from commit bf872ec)

# Conflicts:
#	stdlib/public/core/Integers.swift.gyb
@lorentey lorentey force-pushed the anyhashable-is-not-hashable-4.2 branch from 3802b8c to 8bd1930 Compare June 26, 2018 19:14
@lorentey
Copy link
Member Author

@swift-ci test

1 similar comment
@lorentey
Copy link
Member Author

@swift-ci test

_canonicalBox can perform essentially the same job, so there is no reason to have these requirements.

(cherry picked from commit 2f4ad79)
@lorentey
Copy link
Member Author

@swift-ci please test

1 similar comment
@lorentey
Copy link
Member Author

@swift-ci please test

@swift-ci
Copy link
Contributor

Build failed
Swift Test Linux Platform
Git Sha - d193539

@airspeedswift
Copy link
Member

@swift-ci please test linux platform

@swift-ci
Copy link
Contributor

Build failed
Swift Test Linux Platform
Git Sha - d193539

@lorentey
Copy link
Member Author

Hm, the Foundation failure seems unrelated.

@swift-ci clean test linux platform

@lorentey
Copy link
Member Author

@swift-ci please nominate

@airspeedswift airspeedswift merged commit b415588 into swiftlang:swift-4.2-branch Jun 29, 2018
@lorentey lorentey deleted the anyhashable-is-not-hashable-4.2 branch February 13, 2020 19:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants