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

foldRight override on indexedSeqOps is forward incompatible #12137

Closed
martijnhoekstra opened this issue Sep 2, 2020 · 12 comments
Closed

foldRight override on indexedSeqOps is forward incompatible #12137

martijnhoekstra opened this issue Sep 2, 2020 · 12 comments

Comments

@martijnhoekstra
Copy link

reproduction steps

as reported by @diesalbla on gitter: https://gitter.im/scala/scala?at=5f4e88c49566774dfe49860c

using scodec-bits 1.1.19 and scala 2.13.2

import scodec.bits._

scodec.bits.ByteVector(1, 2, 3, 4, 5, 6, 7).toIndexedSeq.foldRight(0)(_ + _)

problem


java.lang.NoSuchMethodError: scala.collection.IndexedSeqOps.foldRight$(Lscala/collection/IndexedSeqOps;Ljava/lang/Object;Lscala/Function2;)Ljava/lang/Object;
	at scodec.bits.ByteVector$$anon$6.foldRight(ByteVector.scala:706)

scodec-bits 1.1.19 is compiled against scala 2.13.3 which introduced an override on IndexedSeqOps for foldRight in scala/scala#8929

toIndexedSeq is defined as

final def toIndexedSeq: IndexedSeq[Byte] =
  new IndexedSeq[Byte] {
    val length = toIntSize(ByteVector.this.size)
    def apply(i: Int) = ByteVector.this.apply(i.toLong)
  }

foldRight apparently links to the overload which doesn't exist on 2.13.2

I don't see any mima warnings on that PR.

@dwijnand dwijnand changed the title foldRight override on indexedSeqOps is binary incompatible foldRight override on indexedSeqOps is forward incompatible Sep 2, 2020
@diesalbla
Copy link

diesalbla commented Sep 2, 2020

I would like to ask Michael Pilquist @mpilquist, as maintaner/developer of scodec and scodec-bits, for attention.

@lrytz
Copy link
Member

lrytz commented Sep 2, 2020

To lay out what's going on: every concrete trait method gets a static method on the side

trait T {
  def f = 1
  // static def f$ = invokespecial f
}

This allows invoking the specific implementation T.f from anywhere (bypassing dynamic dispatch) by calling T.f$. These static accessor methods are used in compiler-generated "mixin forwarders". For every concrete trait method that a class inherits, a mixin forwarder is generated

class C extends T {
  override def f = T.f$
}

This means that adding a new override to a trait is not forwards binary compatible: new subclasses will refer to the static forwarder of the new override.

Even though this situation is the same since 2.12.0, I think we were not aware of how it impacts binary compatibility.

@lrytz lrytz added this to the 2.13.4 milestone Sep 2, 2020
@martijnhoekstra
Copy link
Author

Does overriding the overridden method again sound like a plausible workaround for scodec-bits?

@lrytz
Copy link
Member

lrytz commented Sep 2, 2020

Yes, but a simple override def foldRight = super.foldRight would not help, super calls are implemented using these static accessor methods.

@lrytz
Copy link
Member

lrytz commented Sep 18, 2020

Echoing a comment I made elsewhere: "fixing" the incompatibility by reverting 2.13.4 back to the binary format of 2.13.0 is most likely a worse choice than doing nothing.

A library compiled against 2.13.3 may include a reference to the new static super accessor, and therefore not work with a 2.13.2 standard library. However, if we remove the override in 2.13.4, that library build will also not work on 2.13.4. The latter, a library compiled with 2.13.x used with 2.13.(x+n), is quite certainly much more common.

@smarter
Copy link
Member

smarter commented Sep 18, 2020

This means that adding a new override to a trait is not forwards binary compatible: new subclasses will refer to the static forwarder of the new override.

Dotty used to have a different encoding for traits which didn't require static forwarders and so might not have the same compat issue (not sure, I haven't checked): scala/scala3#5928, but we recently switched to the 2.13 encoding to stay binary-compatible with it.

@lrytz
Copy link
Member

lrytz commented Sep 18, 2020

Yeah, with -Xmixin-force-forwarders:false the problem would almost not exist, there are only some corner cases where the mixin forwarder method generated in subclasses is actually required for correctness. But given the default is :true, that doesn't help us much.

@smarter
Copy link
Member

smarter commented Sep 18, 2020

Could 2.13.4 keep the static method for bc with 2.13.3 but make sure calls to it never get emitted for bc with 2.13.2- ?

@lrytz
Copy link
Member

lrytz commented Sep 21, 2020

Yes, we can internalize the known binary incompatibilities (https://github.com/scala/scala/pull/9197/files).

We could then also add an annotation like @inheritSignature to allow new overrides in traits. The compiler would avoid generating static accessors, mixin forwarders and/or use invokespecial for the super calls.

@lrytz lrytz added the blocker label Sep 21, 2020
lrytz added a commit to lrytz/scala that referenced this issue Oct 22, 2020
New overrides added to traits are not forwards binary compatible
(scala/bug#12137).

For those overrides that were added since 2.12.0, a new special case
in Mixin avoids generating the mixin forwarder, so compiling with
2.12.13 will emit code that is binary compatible with 2.12.0.

Tested with this code:

```
object A extends scala.math.Ordering.IntOrdering   // `reverse` override was added
object B extends scala.math.Ordering.FloatOrdering // `reverse` override was removed

object Test {
  def main(args: Array[String]): Unit = {
    println(A.reverse)
    println(B.reverse)
  }
}
```

Code compiled with 2.12.13 (qsc/qs) works with all 2.12 libraries
thanks to this fix.

```
qsc Test.scala && sv 2.12.0 Test && sv 2.12.12 Test && qs Test
```

Code compiled with 2.12.0 fails on 2.12.12. The super call in the
generated mixin forwarder `B.reverse` invokes the static method
`FloatOrdering.reverse$` that no longer exists. This will be the same
in 2.12.13.

```
scv 2.12.0 Test.scala && sv 2.12.12 Test
```
lrytz added a commit to lrytz/scala that referenced this issue Oct 22, 2020
New overrides added to traits are not forwards binary compatible
(scala/bug#12137).

For those overrides that were added since 2.12.0, a new special case
in Mixin avoids generating the mixin forwarder, so compiling with
2.12.13 will emit code that is binary compatible with 2.12.0.

Tested with this code:

```
object A extends scala.math.Ordering.IntOrdering   // `reverse` override was added
object B extends scala.math.Ordering.FloatOrdering // `reverse` override was removed

object Test {
  def main(args: Array[String]): Unit = {
    println(A.reverse)
    println(B.reverse)
  }
}
```

Code compiled with 2.12.13 (qsc/qs) works with all 2.12 libraries
thanks to this fix.

```
qsc Test.scala && sv 2.12.0 Test && sv 2.12.12 Test && qs Test
```

Code compiled with 2.12.0 fails on 2.12.12. The super call in the
generated mixin forwarder `B.reverse` invokes the static method
`FloatOrdering.reverse$` that no longer exists. This will be the same
in 2.12.13.

```
scv 2.12.0 Test.scala && sv 2.12.12 Test
java.lang.NoSuchMethodError: scala.math.Ordering$FloatOrdering.reverse$
```
@lrytz
Copy link
Member

lrytz commented Nov 6, 2020

Could 2.13.4 keep the static method for bc with 2.13.3 but make sure calls to it never get emitted for bc with 2.13.2- ?

The downside of this is that it introduces serialization incompatibilities, see scala/scala#9263 (comment).

@dwijnand
Copy link
Member

dwijnand commented Nov 10, 2020

Current:

  • ✅ faster implementation (method override with a faster implementation)
  • ❌ binary incompatible (the mixin forwarder points to a method that doesn't exist in 2.13.2-)
  • ✅ serialisation compatible (there was a mixin forwarder before and after)

Option 1 - Excluding the forwarders:

  • ✅ faster implementation (invokevirtual to the faster impl if available)
  • ✅ binary compatible (not pointing to anything)
  • ⚠️ serialisation incompatible unless you pin the @SerialVersionUID

Option 2 - Including the forwarders but point to the old impl:

  • ❌ slower implementation (as well as confusing: the method override is never invoked)
  • ✅ binary compatible (pointing to the old impl)
  • ✅ serialisation compatible

So Option 1 looks better than Option 2 and the status quo.

lrytz added a commit to lrytz/scala that referenced this issue Nov 11, 2020
New overrides added to traits are not forwards binary compatible
(scala/bug#12137).

For those overrides that were added since 2.12.0, a new special case
in Mixin avoids generating the mixin forwarder, so compiling with
2.12.13 will emit code that is binary compatible with 2.12.0.

Tested with this code:

```
object A extends scala.math.Ordering.IntOrdering   // `reverse` override was added
object B extends scala.math.Ordering.FloatOrdering // `reverse` override was removed

object Test {
  def main(args: Array[String]): Unit = {
    println(A.reverse)
    println(B.reverse)
  }
}
```

Code compiled with 2.12.13 (qsc/qs) works with all 2.12 libraries
thanks to this fix.

```
qsc Test.scala && sv 2.12.0 Test && sv 2.12.12 Test && qs Test
```

Code compiled with 2.12.0 fails on 2.12.12. The super call in the
generated mixin forwarder `B.reverse` invokes the static method
`FloatOrdering.reverse$` that no longer exists. This will be the same
in 2.12.13.

```
scv 2.12.0 Test.scala && sv 2.12.12 Test
java.lang.NoSuchMethodError: scala.math.Ordering$FloatOrdering.reverse$
```
@dwijnand
Copy link
Member

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

5 participants