-
Notifications
You must be signed in to change notification settings - Fork 10.3k
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
Custom Hashable implementation in protocol extension causes infinite loop when used by RawRepresentable types #59780
Comments
Fascinating! IIUC, the problem appears to be that The compiler does emit a warning that indicates that this happened, but it's a bit subtle:
Cc @hborla & @slavapestov for help -- I can't explain why the compiler prefers to synthesize ( Meanwhile, for protocols that refine extension P {
static func ==(left: Self, right: Self) -> Bool {
left.name == right.name
}
func hash(into hasher: inout Hasher) {
hasher.combine(name)
}
var hashValue: Int {
name.hashValue
}
} Weirdly, doing this seems to resolve the issue, at least superficially. (Which indicates that there is no underlying ambiguity, which makes it even more mysterious to me why the compiler synthesizes Evidently with these changes, the compiler uses For reference, as of Swift 5.5, the stdlib has: @inlinable // trivial-implementation
public func == <T: RawRepresentable>(lhs: T, rhs: T) -> Bool
where T.RawValue: Equatable {
return lhs.rawValue == rhs.rawValue
}
extension RawRepresentable where RawValue: Hashable, Self: Hashable {
@inlinable // trivial
public var hashValue: Int {
// Note: in Swift 5.5 and below, this used to return `rawValue.hashValue`.
// The current definition matches the default `hashValue` implementation,
// so that RawRepresentable types don't need to implement both `hashValue`
// and `hash(into:)` to customize their hashing.
_hashValue(for: self)
}
@inlinable // trivial
public func hash(into hasher: inout Hasher) {
hasher.combine(rawValue)
}
@inlinable // trivial
public func _rawHashValue(seed: Int) -> Int {
// In 5.0, this used to return rawValue._rawHashValue(seed: seed). This was
// slightly faster, but it interfered with conforming types' ability to
// customize their hashing. The current definition is equivalent to the
// default implementation; however, we need to keep the definition to remain
// ABI compatible with code compiled on 5.0.
//
// Note that unless a type provides a custom hash(into:) implementation,
// this new version returns the same values as the original 5.0 definition,
// so code that used to work in 5.0 remains working whether or not the
// original definition was inlined.
//
// See https://bugs.swift.org/browse/SR-10734
var hasher = Hasher(_seed: seed)
self.hash(into: &hasher)
return hasher._finalize()
}
} The inconsistency with At this point, I think I'm going to stop tweaking |
Ah, that isn't right -- the two competing default implementations of It appears that the ambiguity for If we moved compiler synthesis out of the picture, we do get the expected error: protocol H {
func foo() -> String
}
protocol P: H {
var name: String { get }
}
extension P {
func foo() -> String { name }
}
protocol R {
var rawValue: String { get }
}
extension R {
func foo() -> String { rawValue }
}
struct S: P, R {
var rawValue: String
var name: String { rawValue }
}
|
Reduced reproducer for the original issue:
My expectation is that this would fail to compile due to the ambiguous definition for |
Mmm, this is arguably defensible behavior (I will explain below) and I bet there are at least a number of people whose code depends on it. Rather than making it fail to compile, I think a warning with different options to silence (I will explain below) would be a way to square the circle, so to speak: In your reduced example with Compiler synthesis takes place for that concrete implementation when (a) an implementation is required to make code compile and (b) it is not written out explicitly. The scenario above meets these criteria. If we were implementing things from scratch, an argument could be made that the compiler never synthesizes an implementation for the concrete conforming type when there is more than one equally good default implementation. However:
Instead, I wonder if we could consider the following:
|
I concur with Xiaodi in the intermodular case, except I would rather make it an error (same fix-it) if at least one of the ambiguous candidates is declared in the current module. |
This seems a pretty clear cut case of a missing diagnostic to me. Member requirements are only ever intended to be synthesized when there is no implementation visible -- most certainly not when there are multiple competing choices. (Source: I implemented one of the iterations of The right way to fix this issue is the same as with any other bug of this nature: keep the existing behavior but emit a compiler warning in Swift 5.x, then later upgrade the warning to an error in Swift 6 language mode, once we have such a thing. Please let's not overthink this. |
My concern is that the end user writing the concrete type should be able to get a synthesized protocol conformance somehow in Swift 6. I don’t think having two or more viable defaults should mean that this useful functionality should be suppressed with no way to get it back. I understand that shipping such functionality was not intended in the first place, but now that it’s been shipped, those relying on it for their code to compile shouldn’t be punished for doing so where the fault isn’t theirs and where the synthesized code doesn’t cause problems. Sympathetic with keeping the fix as simple as possible, though, for sure. |
Yep, we should have syntax to explicitly request a synthesized implementation for a type member, no matter what default implementations are available. (And/or, to have the same implementation generated as explicit code by a refactoring tool in the development environment.) But the fix for this need not wait until this gets implemented -- it would indeed be nice to have a fix-it to get the current behavior for cases when that is desirable, but it's by no means essential. (Personally, I'd prefer if we just forced people to spell out the conforming implementation rather than relying on heuristics such as the protocol being explicitly included on the type's conformance list. The stakes are pretty high here, and the current behavior is broken and very surprising.) |
For sure, I'm in no way suggesting a need to implement the general case of explicitly requesting a synthesized implementation in order to fix a bug. |
(From https://forums.swift.org/t/possible-bug-in-swift-5-6-stdlib-custom-hashable-implementation-in-protocol-extension-causes-infinite-loop-when-used-by-rawrepresentable-types/58536)
Describe the bug
A custom Hashable implementation in a protocol extension causes an infinite loop when it is used by types that also conform to RawRepresentable.
To Reproduce
Steps to reproduce the behavior:
Expected behavior
I would expect the app to continue running and the console output to be:
Environment (please complete the following information):
Additional context
The text was updated successfully, but these errors were encountered: