Skip to content

Mix in the productPrefix hash statically in case class hashCode #11023

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
Apr 3, 2025

Conversation

lrytz
Copy link
Member

@lrytz lrytz commented Mar 20, 2025

Since 2.13, case class hashCode mixes in the hash code of the productPrefix string. This is inconsistent with the equals method, subclasses of case classes that override productPrefix compare equal but have a different hashCode.

This commit changes hashCode to mix in the productPrefix.hashCode statically instead of invoking productPrefix at runtime.

For case classes without primitive fields, the synthetic hashCode invokes ScalaRunTime._hashCode, which mixes in the result of productPrefix. To fix that, the synthetic hashCode is changed to invoke MurmurHash3.productHash directly and mix in the name to the seed statically. This works out with keeping productHash forwards and backwards compatible.

The MurmurHash3.productHash method is deprecated / renamed to caseClassHash. This method computes the same hash as the synthetic hashCode, except for the corner case where a case class (or a subclass) override the productPrefix. In this case, the case class name needs to be passed manually to caseClassHash.

Fixes scala/bug#13033

@scala-jenkins scala-jenkins added this to the 2.13.17 milestone Mar 20, 2025
@lrytz
Copy link
Member Author

lrytz commented Mar 20, 2025

The attemt to keep c.hashCode and MurmurHash3.productHash(c) in sync breaks because one depends on the compiler verison, the other on the runtime library version.

Not sure if that's a big issue...

@SethTisue SethTisue added the release-notes worth highlighting in next release notes label Mar 20, 2025
@lrytz lrytz changed the title Mix classOf[C].getName instead of productPrefix in case hashCode Mix in the productPrefix hash statically in case class hashCode Mar 21, 2025
@lrytz lrytz force-pushed the t13033b branch 3 times, most recently from 11b1efa to 37a12ba Compare March 21, 2025 13:44
@lrytz lrytz force-pushed the t13033b branch 2 times, most recently from 2ea7949 to dedfce3 Compare March 21, 2025 20:11
Since 2.13, case class `hashCode` mixes in the hash code of the
`productPrefix` string. This is inconsistent with the `equals` method,
subclasses of case classes that override `productPrefix` compare
equal but have a different `hashCode`.

This commit changes `hashCode` to mix in the `productPrefix.hashCode`
statically instead of invoking `productPrefix` at runtime.

For case classes without primitive fields, the synthetic `hashCode`
invokes `ScalaRunTime._hashCode`, which mixes in the result of
`productPrefix`. To fix that, the synthetic hashCode is changed
to invoke `MurmurHash3.productHash` directly and mix in the name
to the seed statically. This works out with keeping `productHash`
forwards and backwards compatible.

The `MurmurHash3.productHash` method is deprecated / renamed to
`caseClassHash`. This method computes the same hash as the synthetic
`hashCode`, except for the corner case where a case class
(or a subclass) override the `productPrefix`. In this case, the
case class name needs to be passed manually to `caseClassHash`.
} else {
if (arr == 0)
if (!ignorePrefix) x.productPrefix.hashCode else seed
else {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

rare case where I would retain extra braces (Scala 2)

@@ -152,10 +152,16 @@ object ScalaRunTime {
// More background at ticket #2318.
def ensureAccessible(m: JMethod): JMethod = scala.reflect.ensureAccessible(m)

// This is called by the synthetic case class `toString` method.
// It originally had a `CaseClass` parameter type which was changed to `Product`.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"originally" here means like first verse of Genesis

lrytz added a commit to scala/scala3 that referenced this pull request Apr 3, 2025
…22865)

Since 2.13, case class `hashCode` mixes in the hash code of the
`productPrefix` string. This is inconsistent with the `equals` method,
subclasses of case classes that override `productPrefix` compare equal
but have a different `hashCode`
(scala/bug#13033).

This commit changes `hashCode` to mix in the `productPrefix.hashCode`
statically instead of invoking `productPrefix` at runtime.

For case classes without primitive fields, the synthetic `hashCode`
invokes `ScalaRunTime._hashCode`, which mixes in the result of
`productPrefix`. To fix that, the synthetic hashCode is changed to
invoke `MurmurHash3.productHash` directly and mix in the name to the
seed statically.

Scala 3 forward port of scala/scala#11023
@lrytz lrytz merged commit 4345ced into scala:2.13.x Apr 3, 2025
3 checks passed
hamzaremmal pushed a commit to hamzaremmal/scala3 that referenced this pull request May 2, 2025
Mix in the `productPrefix` hash statically in case class `hashCode`
hamzaremmal pushed a commit to scala/scala3 that referenced this pull request May 7, 2025
Mix in the `productPrefix` hash statically in case class `hashCode`
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
release-notes worth highlighting in next release notes
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Overriding productPrefix breaks case class hash code
5 participants