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

Erase Int | Nothing signatures types into Int #14971

Merged

Conversation

nicolasstucki
Copy link
Contributor

@nicolasstucki nicolasstucki commented Apr 19, 2022

Aligns | Nothing with & Any and Nothing | with Any & for value types.

This also fixes summon[ClassTag[Int | Nothing]] as it is now equivalent to summon[ClassTag[Int]].

This applies to all value types.

Fixes #14970
Fixes #14964

⚠️ This is a binary breaking change ⚠️

the Previous version generated the def f: Int and def f: Object variants of the method.
In the new one, we only generate the def f: Int version.
Unfortunately, the previous version called the boxed version of the interface that does not exist anymore.

Is this something that we encounter in practice?

@nicolasstucki nicolasstucki added stat:do not merge needs-minor-release This PR cannot be merged until the next minor release labels Apr 19, 2022
@nicolasstucki
Copy link
Contributor Author

As far as I can see, the Int | Nothing pattern should not appear in any user code. If this is the case then changing it should not impact signatures in real code. It will impact the ClassTag by fixing cases that were buggy.

I would argue that this erasure is a bug and should be fixed. If a library accidentally used this and cares about binary compatibility, they can add the bridge themselves.

@sjrd
Copy link
Member

sjrd commented Apr 20, 2022

they can add the bridge themselves.

I don't think that's possible. How would they do that? It would result in an ambiguous overload, wouldn't it?

@nicolasstucki
Copy link
Contributor Author

nicolasstucki commented Apr 20, 2022

they can add the bridge themselves.

I don't think that's possible. How would they do that? It would result in an ambiguous overload, wouldn't it?

Indeed, it is not possible 😞

@smarter
Copy link
Member

smarter commented Apr 20, 2022

You can avoid ambiguous overload errors by relying on @targetName:

import scala.annotation.targetName
class A:
  def foo: Int = 1
  @targetName("foo") def fooBinCompat: Any = 1

@smarter
Copy link
Member

smarter commented Apr 20, 2022

... but that's not good enough for library authors in general as a workaround to binary compatibility breakages, because their methods might be overrridden in user code as @sjrd pointed out last time I wanted to break binary compatibility 😬

@nicolasstucki
Copy link
Contributor Author

Then the real question is if there are libraries that use these types in their signatures.

@@ -365,10 +365,18 @@ object TypeErasure {
def erasedLub(tp1: Type, tp2: Type)(using Context): Type = {
// After erasure, C | {Null, Nothing} is just C, if C is a reference type.
Copy link
Contributor

@odersky odersky Apr 21, 2022

Choose a reason for hiding this comment

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

I think this logic has gotten a bit messy and can be simplified. Why not:

if tp1.isRef(defn.NothingClass)
    || tp1.isRef(defn.NullClass) && tp2.derivesFrom(defn.ObjectClass))
then return tp2

And the same to return tp1.

Copy link
Contributor

@odersky odersky left a comment

Choose a reason for hiding this comment

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

I would hope that A | Nothing would not normally appear in library code. The current behavior looks wrong since it violates the property that erasure(A | B) = erasure(B) if A <: B.

So I'd tend to go ahead with merging this after the logic is made clearer.

@odersky odersky assigned nicolasstucki and unassigned odersky Apr 21, 2022
@nicolasstucki nicolasstucki force-pushed the fix-nothing-or-value-type-erasure branch from 182847c to 49cef31 Compare April 21, 2022 11:50
@smarter
Copy link
Member

smarter commented Apr 21, 2022

You can avoid ambiguous overload errors by relying on @TargetNAME:

By the way, even if this works around (some) binary-compatibility issues, it can also break tasty-compatibility since we refer to methods by their original names there.

@nicolasstucki
Copy link
Contributor Author

The current behavior looks wrong since it violates the property that erasure(A | B) = erasure(B) if A <: B

I believe it is not violated. With the current behavior, Int | Nothing and Nothing | Int erase to Int.

  • erasure(Int | Nothing) = erasure(Nothing) if Int <: Nothing the guard does not hold
  • erasure(Nothing | Int) = erasure(Int) if Nothing <: Int the guard holds and we have the correct erasure

Similarly for erasure(A | B) = erasure(A) if B <: A.

@nicolasstucki
Copy link
Contributor Author

Apart from signatures and ClassTags is there something else that uses erased lub?

Copy link
Member

@dwijnand dwijnand left a comment

Choose a reason for hiding this comment

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

LGTM

@nicolasstucki
Copy link
Contributor Author

Based on #14998, the only Nothing | T erasedLub is for Nothing | Null which used to erase to Object and still erases to Object.

@smarter
Copy link
Member

smarter commented Apr 21, 2022

Breaking binary compatbility likes this sets a bad precedent. Is the current behavior really fundamentally broken or merely inconvenient?

@nicolasstucki
Copy link
Contributor Author

Breaking binary compatbility likes this sets a bad precedent. Is the current behavior really fundamentally broken or merely inconvenient?

As far as I can see it is only fundamentally broken for the ClassTag use case. It seems that the signature side only causes unnecessary boxing. I will investigate further to see if I can find a situation where the signatures break something.

We could do an alternative fix for #14964 that would make the erased lub behave differently if we are computing the erased type for a ClassTag.

@smarter
Copy link
Member

smarter commented Apr 21, 2022

Actually you don't even need ClassTag to run into issues:

object Test {
  def main(args: Array[String]): Unit = {
    val x: Array[Int | Nothing] = Array()
    val y: Array[Int] = x
  }
}
Exception in thread "main" java.lang.ClassCastException: [Ljava.lang.Object; cannot be cast to [I
        at Test$.main(arri.scala:3)
        at Test.main(arri.scala)

So yeah, I guess the erasure of union types is just fundamentally broken which would justify changing it :/. If we do change it in 3.2, we could warn in 3.1.3 about any code that relies on it to ease migration.

@nicolasstucki nicolasstucki changed the title Aligns | Nothing with & Any and Nothing | with Any & for value types Erase Int | Nothing signatures types into Int Apr 22, 2022
@nicolasstucki nicolasstucki added this to the 3.2.0-RC1 milestone Apr 22, 2022
@nicolasstucki nicolasstucki force-pushed the fix-nothing-or-value-type-erasure branch from 769dc77 to c4c4d00 Compare April 22, 2022 08:37
This make the erasure different for value types as they now correctly
erase to the primitive types instead of `Object`.

Aligns | Nothing with & Any and Nothing | with Any & for value types.

This also fixes `summon[ClassTag[Int | Nothing]]` as it is now equivalent to `summon[ClassTag[Int]]`.

Fixes scala#14970
Fixes scala#14964

> ⚠️ This is a binary breaking change.

> the Previous version generated the `def f: Int` and `def f: Object` variants of the method.
> In the new one, we only generate the `def f: Int` version.
> Unfortunately, the previous version called the boxed version of the interface that does not exist anymore.
>
> Is this something that we encounter in practice?
@nicolasstucki nicolasstucki force-pushed the fix-nothing-or-value-type-erasure branch from c4c4d00 to 9cd8ce8 Compare April 22, 2022 08:38
@nicolasstucki nicolasstucki added release-notes Should be mentioned in the release notes and removed stat:do not merge labels Apr 28, 2022
Copy link
Contributor

@odersky odersky left a comment

Choose a reason for hiding this comment

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

LGTM

@odersky odersky assigned nicolasstucki and unassigned odersky May 2, 2022
@nicolasstucki nicolasstucki merged commit d829f7c into scala:main May 4, 2022
@nicolasstucki nicolasstucki deleted the fix-nothing-or-value-type-erasure branch May 4, 2022 10:11
@bishabosha
Copy link
Member

this will also break backwards tasty compatibility of resolving methods by their signature

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
needs-minor-release This PR cannot be merged until the next minor release release-notes Should be mentioned in the release notes
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Erasure of Int | Nothing Tuple.toList.toArray raise java.lang.ClassCastException
7 participants