Skip to content
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

🛑[stdlib] Flesh out _Hasher API #15939

Closed
wants to merge 14 commits into from
Closed
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
41 changes: 31 additions & 10 deletions stdlib/private/StdlibUnittest/MinimalTypes.swift.gyb
Original file line number Diff line number Diff line change
Expand Up @@ -110,14 +110,14 @@ public func < (
///
/// This type can be used to check that generic functions don't rely on any
/// other conformances.
public ${decl_keyword} ${Self} : Equatable, Hashable {
public ${decl_keyword} ${Self} : Equatable, Hashable, CustomStringConvertible {
public static var timesEqualEqualWasCalled: Int = 0
public static var timesHashValueWasCalled: Int = 0
public static var timesHashIntoWasCalled: Int = 0

public static var equalImpl =
ResettableValue<(Int, Int) -> Bool>({ $0 == $1 })
public static var hashValueImpl =
ResettableValue<(Int) -> Int>({ $0.hashValue })
public static var hashIntoImpl =
ResettableValue<(Int, inout _Hasher) -> Void>({ $1.combine($0) })

public var value: Int
public var identity: Int
Expand All @@ -133,8 +133,18 @@ public ${decl_keyword} ${Self} : Equatable, Hashable {
}

public var hashValue: Int {
${Self}.timesHashValueWasCalled += 1
return ${Self}.hashValueImpl.value(value)
var hasher = _Hasher()
_hash(into: &hasher)
return hasher.finalize()
}

public func _hash(into hasher: inout _Hasher) {
${Self}.timesHashIntoWasCalled += 1
${Self}.hashIntoImpl.value(value, &hasher)
}

public var description: String {
return "${Self}(value: \(value), identity: \(identity))"
}
}

Expand All @@ -152,18 +162,20 @@ public func == (
% Self = 'GenericMinimalHashable%s' % kind

public var ${Self}_timesEqualEqualWasCalled: Int = 0
public var ${Self}_timesHashIntoWasCalled: Int = 0
public var ${Self}_timesHashValueWasCalled: Int = 0

public var ${Self}_equalImpl = ResettableValue<(Any, Any) -> Bool>(
{ _, _ in fatalError("${Self}_equalImpl is not set yet"); () })
public var ${Self}_hashValueImpl = ResettableValue<(Any) -> Int>(
{ _ in fatalError("${Self}_hashValueImpl is not set yet"); () })
public var ${Self}_hashIntoImpl = ResettableValue<(Any, inout _Hasher) -> Void>(
{ _ in fatalError("${Self}_hashIntoImpl is not set yet"); () })

/// A type that conforms only to `Equatable` and `Hashable`.
///
/// This type can be used to check that generic functions don't rely on any
/// other conformances.
public ${decl_keyword} ${Self}<Wrapped> : Equatable, Hashable {
public ${decl_keyword} ${Self}<Wrapped>
: Equatable, Hashable, CustomStringConvertible {
public var value: Wrapped
public var identity: Int

Expand All @@ -179,7 +191,16 @@ public ${decl_keyword} ${Self}<Wrapped> : Equatable, Hashable {

public var hashValue: Int {
${Self}_timesHashValueWasCalled += 1
return ${Self}_hashValueImpl.value(value)
return _unsafeHashValue()
}

public func _hash(into hasher: inout _Hasher) {
${Self}_timesHashIntoWasCalled += 1
return ${Self}_hashIntoImpl.value(value, &hasher)
}

public var description: String {
return "${Self}(value: \(value), identity: \(identity))"
}
}

Expand Down
80 changes: 75 additions & 5 deletions stdlib/private/StdlibUnittest/StdlibUnittest.swift.gyb
Original file line number Diff line number Diff line change
Expand Up @@ -2095,17 +2095,43 @@ public func checkEquatable<T : Equatable>(
oracle: { expectedEqual || $0 == $1 }, ${trace}, showFrame: false)
}

internal func hash<H: Hashable>(_ value: H, seed: Int? = nil) -> Int {
var hasher = _Hasher()
if let seed = seed {
hasher.combine(seed)
}
hasher.combine(value)
return hasher.finalize()
}

/// 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`.
public func checkHashable<Instances: Collection>(
_ instances: Instances,
equalityOracle: (Instances.Index, Instances.Index) -> Bool,
allowBrokenTransitivity: Bool = false,
${TRACE}
) where Instances.Iterator.Element: Hashable {
checkHashable(
instances,
equalityOracle: equalityOracle,
hashEqualityOracle: equalityOracle,
allowBrokenTransitivity: allowBrokenTransitivity,
${trace})
}

/// Test that the elements of `instances` satisfy the semantic
/// requirements of `Hashable`, using `equalityOracle` to generate
/// equality expectations from pairs of positions in `instances`.
public func checkHashable<Instances : Collection>(
/// equality expectations from pairs of positions in `instances`,
/// and `hashEqualityOracle` to do the same for hashing.
public func checkHashable<Instances: Collection>(
_ instances: Instances,
equalityOracle: (Instances.Index, Instances.Index) -> Bool,
hashEqualityOracle: (Instances.Index, Instances.Index) -> Bool,
allowBrokenTransitivity: Bool = false,
${TRACE}
) where
Instances.Iterator.Element : Hashable {

) where Instances.Iterator.Element: Hashable {
checkEquatable(
instances,
oracle: equalityOracle,
Expand All @@ -2116,11 +2142,55 @@ public func checkHashable<Instances : Collection>(
let x = instances[i]
for j in instances.indices {
let y = instances[j]
let predicted = hashEqualityOracle(i, j)
expectEqual(
predicted, hashEqualityOracle(j, i),
"bad hash oracle: broken symmetry between indices \(i), \(j)",
stackTrace: ${stackTrace})
if x == y {
expectTrue(
predicted,
"""
bad hash oracle: equality must imply hash equality
lhs (at index \(i)): \(x)\nrhs (at index \(j)): \(y)
""",
stackTrace: ${stackTrace})
}
if predicted {
expectEqual(
hash(x), hash(y),
"hash(into:) expected to match, found to differ\n" +
"lhs (at index \(i)): \(x)\nrhs (at index \(j)): \(y)",
stackTrace: ${stackTrace})
expectEqual(
x.hashValue, y.hashValue,
"hashValue expected to match, found to differ\n" +
"lhs (at index \(i)): \(x)\nrhs (at index \(j)): \(y)",
stackTrace: ${stackTrace})
expectEqual(
x._unsafeHashValue(), y._unsafeHashValue(),
"_unsafeHashValue() expected to match, found to differ\n" +
"lhs (at index \(i)): \(x)\nrhs (at index \(j)): \(y)",
stackTrace: ${stackTrace})
} else {
// Try a few different seeds; one of them must discriminate
// between the hashes.
var pass = false
for s in 0..<10 {
if hash(x, seed: s) != hash(y, seed: s) {
// Triggers on first iteration in most cases.
pass = true
break
}
}
if !pass {
expectTrue(
AssertionResult(isPass: pass)
.withDescription(
"_hash(into:) expected to differ, found to match"),
"lhs (at index \(i)): \(x)\nrhs (at index \(j)): \(y)",
stackTrace: ${stackTrace})
}
}
}
}
Expand Down
12 changes: 11 additions & 1 deletion stdlib/public/SDK/ObjectiveC/ObjectiveC.swift
Original file line number Diff line number Diff line change
Expand Up @@ -211,9 +211,19 @@ extension NSObject : Equatable, Hashable {
/// different invocations of the same program. Do not persist the
/// hash value across program runs.
@objc
open var hashValue: Int {
open // FIXME: should be @nonobjc public.
var hashValue: Int {
return hash
}

public final func _hash(into hasher: inout _Hasher) {
// FIXME(hasher): We should call self.hash here; however, that would cause
// compatibility issues with code that mistakenly overrides hashValue rather
// than hash. (Types that do this break Foundation's hashing, but as long as
// they correctly override isEqual, they may still conform to Swift's
// Hashable.)
hasher.combine(self.hashValue)
}
}

extension NSObject : CVarArg {
Expand Down
16 changes: 13 additions & 3 deletions stdlib/public/core/Arrays.swift.gyb
Original file line number Diff line number Diff line change
Expand Up @@ -2265,14 +2265,24 @@ extension ${Self} : Equatable where Element : Equatable {
extension ${Self}: Hashable where Element: Hashable {
@inlinable // FIXME(sil-serialize-all)
public var hashValue: Int {
return _hashValue(for: self)
return _unsafeHashValue()
}

@inlinable // FIXME(sil-serialize-all)
// not @inlinable
public func _hash(into hasher: inout _Hasher) {
hasher.combine(count) // discriminator
for element in self {
hasher.combine(element)
}
}

// not @inlinable
public func _unsafeHashValue(seed: (UInt64, UInt64)) -> Int {
var hasher = _Hasher(_seed: seed)
for element in self {
hasher.append(element)
hasher.combine(element)
}
return hasher.finalize()
}
}

Expand Down
4 changes: 2 additions & 2 deletions stdlib/public/core/Bool.swift
Original file line number Diff line number Diff line change
Expand Up @@ -154,12 +154,12 @@ extension Bool : Equatable, Hashable {
/// program runs.
@inlinable // FIXME(sil-serialize-all)
public var hashValue: Int {
return _hashValue(for: self)
return _unsafeHashValue()
}

@inlinable // FIXME(sil-serialize-all)
public func _hash(into hasher: inout _Hasher) {
hasher.append((self ? 1 : 0) as UInt8)
hasher.combine((self ? 1 : 0) as UInt8)
}

@inlinable // FIXME(sil-serialize-all)
Expand Down
3 changes: 2 additions & 1 deletion stdlib/public/core/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ set(SWIFTLIB_ESSENTIAL
AnyHashable.swift
# END WORKAROUND
HashedCollectionsAnyHashableExtensions.swift
Hasher.swift
Hashing.swift
HeapBuffer.swift
ICU.swift
Expand Down Expand Up @@ -105,7 +106,7 @@ set(SWIFTLIB_ESSENTIAL
Reverse.swift
Runtime.swift.gyb
RuntimeFunctionCounters.swift
SipHash.swift.gyb
SipHash.swift
SentinelCollection.swift
Sequence.swift
SequenceAlgorithms.swift
Expand Down
4 changes: 2 additions & 2 deletions stdlib/public/core/CTypes.swift
Original file line number Diff line number Diff line change
Expand Up @@ -181,12 +181,12 @@ extension OpaquePointer: Hashable {
/// program runs.
@inlinable // FIXME(sil-serialize-all)
public var hashValue: Int {
return _hashValue(for: self)
return _unsafeHashValue()
}

@inlinable // FIXME(sil-serialize-all)
public func _hash(into hasher: inout _Hasher) {
hasher.append(Int(Builtin.ptrtoint_Word(_rawValue)))
hasher.combine(Int(Builtin.ptrtoint_Word(_rawValue)))
}
}

Expand Down
14 changes: 7 additions & 7 deletions stdlib/public/core/ClosedRange.swift
Original file line number Diff line number Diff line change
Expand Up @@ -170,17 +170,17 @@ extension ClosedRange.Index: Hashable
where Bound: Strideable, Bound.Stride: SignedInteger, Bound: Hashable {
@inlinable // FIXME(sil-serialize-all)
public var hashValue: Int {
return _hashValue(for: self)
return _unsafeHashValue()
}

@inlinable // FIXME(sil-serialize-all)
public func _hash(into hasher: inout _Hasher) {
switch self {
case .inRange(let value):
hasher.append(0 as Int8)
hasher.append(value)
hasher.combine(0 as Int8)
hasher.combine(value)
case .pastEnd:
hasher.append(1 as Int8)
hasher.combine(1 as Int8)
}
}
}
Expand Down Expand Up @@ -391,13 +391,13 @@ extension ClosedRange: Equatable {
extension ClosedRange: Hashable where Bound: Hashable {
@inlinable // FIXME(sil-serialize-all)
public var hashValue: Int {
return _hashValue(for: self)
return _unsafeHashValue()
}

@inlinable // FIXME(sil-serialize-all)
public func _hash(into hasher: inout _Hasher) {
hasher.append(lowerBound)
hasher.append(upperBound)
hasher.combine(lowerBound)
hasher.combine(upperBound)
}
}

Expand Down
16 changes: 7 additions & 9 deletions stdlib/public/core/Dictionary.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1445,19 +1445,19 @@ extension Dictionary: Equatable where Value: Equatable {
extension Dictionary: Hashable where Value: Hashable {
@inlinable // FIXME(sil-serialize-all)
public var hashValue: Int {
return _hashValue(for: self)
return _unsafeHashValue()
}

@inlinable // FIXME(sil-serialize-all)
// not @inlinable
public func _hash(into hasher: inout _Hasher) {
var commutativeHash = 0
for (k, v) in self {
var elementHasher = _Hasher()
elementHasher.append(k)
elementHasher.append(v)
var elementHasher = hasher
elementHasher.combine(k)
elementHasher.combine(v)
commutativeHash ^= elementHasher.finalize()
}
hasher.append(commutativeHash)
hasher.combine(commutativeHash)
}
}

Expand Down Expand Up @@ -2436,9 +2436,7 @@ extension _NativeDictionaryBuffer where Key: Hashable
@inlinable // FIXME(sil-serialize-all)
@inline(__always) // For performance reasons.
internal func _bucket(_ k: Key) -> Int {
var hasher = _Hasher(seed: _storage.seed)
hasher.append(k)
return hasher.finalize() & _bucketMask
return k._unsafeHashValue(seed: _storage.seed) & _bucketMask
}

@inlinable // FIXME(sil-serialize-all)
Expand Down
2 changes: 1 addition & 1 deletion stdlib/public/core/DropWhile.swift
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,7 @@ extension LazyDropWhileCollection.Index: Hashable where Base.Index: Hashable {

@inlinable // FIXME(sil-serialize-all)
public func _hash(into hasher: inout _Hasher) {
hasher.append(base)
hasher.combine(base)
}
}

Expand Down
6 changes: 3 additions & 3 deletions stdlib/public/core/Flatten.swift
Original file line number Diff line number Diff line change
Expand Up @@ -231,13 +231,13 @@ extension FlattenCollection.Index : Hashable
where Base.Index : Hashable, Base.Element.Index : Hashable {
@inlinable // FIXME(sil-serialize-all)
public var hashValue: Int {
return _hashValue(for: self)
return _unsafeHashValue()
}

@inlinable // FIXME(sil-serialize-all)
public func _hash(into hasher: inout _Hasher) {
hasher.append(_outer)
hasher.append(_inner)
hasher.combine(_outer)
hasher.combine(_inner)
}
}

Expand Down
Loading