From 9dd9e96e1d0c0b5bde1a720c6ccefe77f5dc32cc Mon Sep 17 00:00:00 2001 From: Ben Cohen Date: Thu, 11 Dec 2025 14:47:36 -0800 Subject: [PATCH] Allow Hashable: ~Copyable (#85748) Builds on #85746 which covers Equatable. --- lib/SILGen/SILGenExpr.cpp | 18 ++++- stdlib/public/core/Hashable.swift | 8 +- stdlib/public/core/Hasher.swift | 3 +- test/Frontend/dump-parse.swift | 2 +- test/Parse/inverses.swift | 8 +- .../SILGen/synthesized_conformance_enum.swift | 6 +- .../synthesized_conformance_struct.swift | 6 +- test/Sema/moveonly_illegal_types.swift | 6 +- ...tability-stdlib-source-base.swift.expected | 8 +- .../stability-stdlib-abi-without-asserts.test | 16 +++- test/stdlib/NoncopyableHashable.swift | 75 +++++++++++++++++++ 11 files changed, 135 insertions(+), 21 deletions(-) create mode 100644 test/stdlib/NoncopyableHashable.swift diff --git a/lib/SILGen/SILGenExpr.cpp b/lib/SILGen/SILGenExpr.cpp index 01d1c62ca1a..6aad3a9b994 100644 --- a/lib/SILGen/SILGenExpr.cpp +++ b/lib/SILGen/SILGenExpr.cpp @@ -37,6 +37,8 @@ #include "swift/AST/Expr.h" #include "swift/AST/ForeignErrorConvention.h" #include "swift/AST/GenericEnvironment.h" +#include "swift/AST/InFlightSubstitution.h" +#include "swift/AST/KnownProtocols.h" #include "swift/AST/ParameterList.h" #include "swift/AST/ProtocolConformance.h" #include "swift/AST/SubstitutionMap.h" @@ -2430,8 +2432,20 @@ RValue SILGenFunction::emitAnyHashableErasure(SILLocation loc, return emitUndefRValue(loc, getASTContext().getAnyHashableType()); // Construct the substitution for T: Hashable. - auto subMap = SubstitutionMap::getProtocolSubstitutions( - conformance.getProtocol(), type, conformance); + auto subMap = SubstitutionMap::get(convertFn->getGenericSignature(), type, + [&](InFlightSubstitution &ifs, + Type ty, + ProtocolDecl *proto) -> ProtocolConformanceRef { + switch (*proto->getKnownProtocolKind()) { + case KnownProtocolKind::Hashable: + return conformance; + case KnownProtocolKind::Copyable: + case KnownProtocolKind::Escapable: + return lookupConformance(type, proto); + default: + llvm_unreachable("no other conformances should be involved"); + } + }); return emitApplyOfLibraryIntrinsic(loc, convertFn, subMap, value, C); } diff --git a/stdlib/public/core/Hashable.swift b/stdlib/public/core/Hashable.swift index 5dff1f30685..386a20e866f 100644 --- a/stdlib/public/core/Hashable.swift +++ b/stdlib/public/core/Hashable.swift @@ -101,7 +101,7 @@ /// print("New tap detected at (\(nextTap.x), \(nextTap.y)).") /// } /// // Prints "New tap detected at (0, 1).") -public protocol Hashable: Equatable { +public protocol Hashable: Equatable & ~Copyable { /// The hash value. /// /// Hash values are not guaranteed to be equal across different executions of @@ -135,9 +135,10 @@ public protocol Hashable: Equatable { func _rawHashValue(seed: Int) -> Int } -extension Hashable { +extension Hashable where Self: ~Copyable { @inlinable @inline(__always) + @_preInverseGenerics public func _rawHashValue(seed: Int) -> Int { var hasher = Hasher(_seed: seed) hasher.combine(self) @@ -148,7 +149,8 @@ extension Hashable { // Called by synthesized `hashValue` implementations. @inlinable @inline(__always) -public func _hashValue(for value: H) -> Int { +@_preInverseGenerics +public func _hashValue(for value: borrowing H) -> Int { return value._rawHashValue(seed: 0) } diff --git a/stdlib/public/core/Hasher.swift b/stdlib/public/core/Hasher.swift index f7650baf192..5972fd25c1d 100644 --- a/stdlib/public/core/Hasher.swift +++ b/stdlib/public/core/Hasher.swift @@ -350,7 +350,8 @@ public struct Hasher { /// - Parameter value: A value to add to the hasher. @inlinable @inline(__always) - public mutating func combine(_ value: H) { + @_preInverseGenerics + public mutating func combine(_ value: borrowing H) { value.hash(into: &self) } diff --git a/test/Frontend/dump-parse.swift b/test/Frontend/dump-parse.swift index c1abe47da67..9aec3af7e80 100644 --- a/test/Frontend/dump-parse.swift +++ b/test/Frontend/dump-parse.swift @@ -58,7 +58,7 @@ enum TrailingSemi { // CHECK-AST-LABEL: (func_decl{{.*}}"generic(_:)" "" interface_type=" (T) -> ()" access=internal captures=( {{.*}}) func generic(_: T) {} // CHECK-AST: (pattern_binding_decl -// CHECK-AST: (processed_init=declref_expr type="(Int) -> ()" location={{.*}} range={{.*}} decl="main.(file).generic@{{.*}} [with (substitution_map generic_signature= T -> Int)]" function_ref=unapplied)) +// CHECK-AST: (processed_init=declref_expr type="(Int) -> ()" location={{.*}} range={{.*}} decl="main.(file).generic@{{.*}} [with (substitution_map generic_signature= T -> Int)]" function_ref=unapplied)) let _: (Int) -> () = generic // Closures should be marked as escaping or not. diff --git a/test/Parse/inverses.swift b/test/Parse/inverses.swift index 60f78fe88e0..503c4c205bd 100644 --- a/test/Parse/inverses.swift +++ b/test/Parse/inverses.swift @@ -52,7 +52,9 @@ public struct MoveOnlyS1 : ~Copyable { /*deinit {}*/ } public struct MoveOnlyS2 : ~Copyable { /*deinit {}*/ } public struct MoveOnlyS3 : ~Copyable { /*deinit {}*/ } -protocol Rope: Hashable, ~Copyable { // expected-error {{'Self' required to be 'Copyable' but is marked with '~Copyable'}} +protocol CopyHashable { } + +protocol Rope: CopyHashable, ~Copyable { // expected-error {{'Self' required to be 'Copyable' but is marked with '~Copyable'}} associatedtype Element: ~Copyable } @@ -102,8 +104,8 @@ typealias Z4 = ~Rope // expected-error {{type 'Rope' cannot be suppres typealias Z5 = (~Int) -> Void // expected-error {{type 'Int' cannot be suppressed}} typealias Z6 = ~() -> () // expected-error {{single argument function types require parentheses}} // expected-error@-1 {{type '()' cannot be suppressed}} -typealias Z7 = ~(Copyable & Hashable) // expected-error {{type 'Hashable' cannot be suppressed}} -typealias Z8 = ~Copyable & Hashable // expected-error {{composition cannot contain '~Copyable' when another member requires 'Copyable'}} +typealias Z7 = ~(Copyable & CopyHashable) // expected-error {{type 'CopyHashable' cannot be suppressed}} +typealias Z8 = ~Copyable & CopyHashable // expected-error {{composition cannot contain '~Copyable' when another member requires 'Copyable'}} struct NotAProtocol {} diff --git a/test/SILGen/synthesized_conformance_enum.swift b/test/SILGen/synthesized_conformance_enum.swift index 29064c85ceb..ea4ac8c9e0e 100644 --- a/test/SILGen/synthesized_conformance_enum.swift +++ b/test/SILGen/synthesized_conformance_enum.swift @@ -83,9 +83,9 @@ extension NoValues: Codable {} // CHECK-LABEL: sil_witness_table hidden Enum: Hashable module synthesized_conformance_enum { // CHECK-DAG: base_protocol Equatable: Enum: Equatable module synthesized_conformance_enum -// CHECK-DAG: method #Hashable.hashValue!getter: (Self) -> () -> Int : @$s28synthesized_conformance_enum4EnumOyxGSHAASHRzlSH9hashValueSivgTW // protocol witness for Hashable.hashValue.getter in conformance Enum -// CHECK-DAG: method #Hashable.hash: (Self) -> (inout Hasher) -> () : @$s28synthesized_conformance_enum4EnumOyxGSHAASHRzlSH4hash4intoys6HasherVz_tFTW // protocol witness for Hashable.hash(into:) in conformance Enum -// CHECK-DAG: method #Hashable._rawHashValue: (Self) -> (Int) -> Int : @$s28synthesized_conformance_enum4EnumOyxGSHAASHRzlSH13_rawHashValue4seedS2i_tFTW // protocol witness for Hashable._rawHashValue(seed:) in conformance Enum +// CHECK-DAG: method #Hashable.hashValue!getter: (Self) -> () -> Int : @$s28synthesized_conformance_enum4EnumOyxGSHAASHRzlSH9hashValueSivgTW // protocol witness for Hashable.hashValue.getter in conformance Enum +// CHECK-DAG: method #Hashable.hash: (Self) -> (inout Hasher) -> () : @$s28synthesized_conformance_enum4EnumOyxGSHAASHRzlSH4hash4intoys6HasherVz_tFTW // protocol witness for Hashable.hash(into:) in conformance Enum +// CHECK-DAG: method #Hashable._rawHashValue: (Self) -> (Int) -> Int : @$s28synthesized_conformance_enum4EnumOyxGSHAASHRzlSH13_rawHashValue4seedS2i_tFTW // protocol witness for Hashable._rawHashValue(seed:) in conformance Enum // CHECK-DAG: conditional_conformance (T: Hashable): dependent // CHECK: } diff --git a/test/SILGen/synthesized_conformance_struct.swift b/test/SILGen/synthesized_conformance_struct.swift index 08d8bb5bb65..bd29424257c 100644 --- a/test/SILGen/synthesized_conformance_struct.swift +++ b/test/SILGen/synthesized_conformance_struct.swift @@ -69,9 +69,9 @@ extension Struct: Codable where T: Codable {} // CHECK-LABEL: sil_witness_table hidden Struct: Hashable module synthesized_conformance_struct { // CHECK-DAG: base_protocol Equatable: Struct: Equatable module synthesized_conformance_struct -// CHECK-DAG: method #Hashable.hashValue!getter: (Self) -> () -> Int : @$s30synthesized_conformance_struct6StructVyxGSHAASHRzlSH9hashValueSivgTW // protocol witness for Hashable.hashValue.getter in conformance Struct -// CHECK-DAG: method #Hashable.hash: (Self) -> (inout Hasher) -> () : @$s30synthesized_conformance_struct6StructVyxGSHAASHRzlSH4hash4intoys6HasherVz_tFTW // protocol witness for Hashable.hash(into:) in conformance Struct -// CHECK-DAG: method #Hashable._rawHashValue: (Self) -> (Int) -> Int : @$s30synthesized_conformance_struct6StructVyxGSHAASHRzlSH13_rawHashValue4seedS2i_tFTW // protocol witness for Hashable._rawHashValue(seed:) in conformance Struct +// CHECK-DAG: method #Hashable.hashValue!getter: (Self) -> () -> Int : @$s30synthesized_conformance_struct6StructVyxGSHAASHRzlSH9hashValueSivgTW // protocol witness for Hashable.hashValue.getter in conformance Struct +// CHECK-DAG: method #Hashable.hash: (Self) -> (inout Hasher) -> () : @$s30synthesized_conformance_struct6StructVyxGSHAASHRzlSH4hash4intoys6HasherVz_tFTW // protocol witness for Hashable.hash(into:) in conformance Struct +// CHECK-DAG: method #Hashable._rawHashValue: (Self) -> (Int) -> Int : @$s30synthesized_conformance_struct6StructVyxGSHAASHRzlSH13_rawHashValue4seedS2i_tFTW // protocol witness for Hashable._rawHashValue(seed:) in conformance Struct // CHECK-DAG: conditional_conformance (T: Hashable): dependent // CHECK: } diff --git a/test/Sema/moveonly_illegal_types.swift b/test/Sema/moveonly_illegal_types.swift index d911cda072c..c24df16402f 100644 --- a/test/Sema/moveonly_illegal_types.swift +++ b/test/Sema/moveonly_illegal_types.swift @@ -52,11 +52,11 @@ func basic_vararg(_ va: MO...) {} // expected-error {{noncopyable type 'MO' cann func illegalTypes(_ t: T) { let _: Array // expected-error {{type 'MO' does not conform to protocol 'Copyable'}} let _: Maybe // expected-error {{type 'MO' does not conform to protocol 'Copyable'}} - let _: Dictionary // expected-error {{type 'MO' does not conform to protocol 'Hashable'}} + let _: Dictionary // expected-error {{type 'MO' does not conform to protocol 'Copyable'}} let _: [MO] // expected-error {{type 'MO' does not conform to protocol 'Copyable'}} let _: [String : MO] // expected-error {{type 'MO' does not conform to protocol 'Copyable'}} - let _: [MO : MO] // expected-error {{type 'MO' does not conform to protocol 'Hashable'}} - let _: [MO : T] // expected-error {{type 'MO' does not conform to protocol 'Hashable'}} + let _: [MO : MO] // expected-error {{type 'MO' does not conform to protocol 'Copyable'}} + let _: [MO : T] // expected-error {{type 'MO' does not conform to protocol 'Copyable'}} _ = t as! ValBox // expected-error {{type 'MO' does not conform to protocol 'Copyable'}} diff --git a/test/api-digester/Outputs/stability-stdlib-source-base.swift.expected b/test/api-digester/Outputs/stability-stdlib-source-base.swift.expected index a0f972039e5..60bce9a98bf 100644 --- a/test/api-digester/Outputs/stability-stdlib-source-base.swift.expected +++ b/test/api-digester/Outputs/stability-stdlib-source-base.swift.expected @@ -114,7 +114,6 @@ Protocol FixedWidthInteger has added inherited protocol Copyable Protocol FixedWidthInteger has added inherited protocol Escapable Protocol FloatingPoint has added inherited protocol Copyable Protocol FloatingPoint has added inherited protocol Escapable -Protocol Hashable has added inherited protocol Copyable Protocol Hashable has added inherited protocol Escapable Protocol Identifiable has added inherited protocol Copyable Protocol Identifiable has added inherited protocol Escapable @@ -399,3 +398,10 @@ Func Comparable.>(_:_:) has parameter 1 changing from Default to Shared Func Comparable.>=(_:_:) has generic signature change from to Func Comparable.>=(_:_:) has parameter 0 changing from Default to Shared Func Comparable.>=(_:_:) has parameter 1 changing from Default to Shared + +// Hashable: ~Copyable +Protocol Hashable has generic signature change from to +Accessor Hashable.hashValue.Get() has generic signature change from to +Func Hashable.hash(into:) has generic signature change from to +Func Hasher.combine(_:) has generic signature change from to +Func Hasher.combine(_:) has parameter 0 changing from Default to Shared diff --git a/test/api-digester/stability-stdlib-abi-without-asserts.test b/test/api-digester/stability-stdlib-abi-without-asserts.test index e143eb31dfa..52124e21472 100644 --- a/test/api-digester/stability-stdlib-abi-without-asserts.test +++ b/test/api-digester/stability-stdlib-abi-without-asserts.test @@ -232,7 +232,6 @@ Protocol FixedWidthInteger has added inherited protocol Copyable Protocol FixedWidthInteger has added inherited protocol Escapable Protocol FloatingPoint has added inherited protocol Copyable Protocol FloatingPoint has added inherited protocol Escapable -Protocol Hashable has added inherited protocol Copyable Protocol Hashable has added inherited protocol Escapable Protocol Identifiable has added inherited protocol Copyable Protocol Identifiable has added inherited protocol Escapable @@ -911,5 +910,20 @@ Func Comparable.>=(_:_:) has parameter 0 changing from Default to Shared Func Comparable.>=(_:_:) has parameter 1 changing from Default to Shared Func Comparable.>=(_:_:) is now with @_preInverseGenerics +// Hashable: ~Copyable +Protocol Hashable has generic signature change from to +Accessor Hashable.hashValue.Get() has generic signature change from to +Func Hashable._rawHashValue(seed:) has generic signature change from to +Func Hashable._rawHashValue(seed:) has mangled name changing from '(extension in Swift):Swift.Hashable._rawHashValue(seed: Swift.Int) -> Swift.Int' to '(extension in Swift):Swift.Hashable< where A: ~Swift.Copyable>._rawHashValue(seed: Swift.Int) -> Swift.Int' +Func Hashable._rawHashValue(seed:) is now with @_preInverseGenerics +Func Hashable.hash(into:) has generic signature change from to +Func Hasher.combine(_:) has generic signature change from to +Func Hasher.combine(_:) has mangled name changing from 'Swift.Hasher.combine(A) -> ()' to 'Swift.Hasher.combine(A) -> ()' +Func Hasher.combine(_:) has parameter 0 changing from Default to Shared +Func Hasher.combine(_:) is now with @_preInverseGenerics +Func _hashValue(for:) has generic signature change from to +Func _hashValue(for:) has mangled name changing from 'Swift._hashValue(for: A) -> Swift.Int' to 'Swift._hashValue(for: A) -> Swift.Int' +Func _hashValue(for:) has parameter 0 changing from Default to Shared +Func _hashValue(for:) is now with @_preInverseGenerics // *** DO NOT DISABLE OR XFAIL THIS TEST. *** (See comment above.) diff --git a/test/stdlib/NoncopyableHashable.swift b/test/stdlib/NoncopyableHashable.swift new file mode 100644 index 00000000000..295377f4581 --- /dev/null +++ b/test/stdlib/NoncopyableHashable.swift @@ -0,0 +1,75 @@ +//===--- NoncopyableHashable.swift - tests for Hashable: ~Copyable ----------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2014 - 2025 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +// +//===----------------------------------------------------------------------===// +// RUN: %target-run-simple-swift +// REQUIRES: executable_test + +import StdlibUnittest + +let NoncopyableHashableTests = TestSuite("NoncopyableHashable") + +struct Noncopyable: ~Copyable { + var wrapped: Wrapped +} + +extension Noncopyable: Equatable where Wrapped: Equatable & ~Copyable { } + +extension Noncopyable: Hashable where Wrapped: Hashable & ~Copyable { } + + +extension Hashable where Self: ~Copyable { + func sameHash(as other: borrowing Self) -> Bool { + self.hashValue == other.hashValue + } +} + +func differentHash(_ lhs: borrowing T, _ rhs: borrowing T) -> Bool { + lhs.hashValue != rhs.hashValue +} + +@available(SwiftStdlib 6.2, *) +extension InlineArray where Element: Hashable & ~Copyable { + func combinedHashes() -> Int { + var hasher = Hasher() + for i in self.indices { + self[i].hash(into: &hasher) + } + return hasher.finalize() + } +} + +NoncopyableHashableTests.test("hashing noncopyables") { + let a = Noncopyable(wrapped: 1) + let b = Noncopyable(wrapped: 2) + let c = Noncopyable(wrapped: 1) + + expectTrue(a.sameHash(as: a)) + expectFalse(a.sameHash(as: b)) + expectTrue(a.sameHash(as: c)) + + expectTrue(differentHash(a,b)) + expectFalse(differentHash(a,a)) + expectFalse(differentHash(a,c)) + + let nc2 = Noncopyable(wrapped: Noncopyable(wrapped: "1")) + expectTrue(nc2.sameHash(as: nc2)) + expectTrue(differentHash(nc2, .init(wrapped: .init(wrapped: "2")))) + + guard #available(SwiftStdlib 6.2, *) else { return } + + let a1: [_ of _] = [a,b] + let d = Noncopyable(wrapped: 2) + let a2: [_ of _] = [c,d] + expectEqual(a1.combinedHashes(), a2.combinedHashes()) + +} + +runAllTests()