Skip to content

Conversation

@lrytz
Copy link
Member

@lrytz lrytz commented Jul 27, 2020

Methods annotated @inheritSignature are emitted private in bytecode, which allows adding binary compatible overrides when the bytecode signature is refined.

class A { def f: AnyRef = "A.f" }
class C extends A { @inheritSignature override def f: String = "C.f" }

The method C.f: String is made private, the bridge remains public (and the bridge flag is removed). Calls to C.f are rewritten to A.f.

Closes scala/bug#11804. Hat-tip to @szeiger for the idea and for doing the initial research.

@scala-jenkins scala-jenkins added this to the 2.13.4 milestone Jul 27, 2020
@lrytz
Copy link
Member Author

lrytz commented Jul 27, 2020

@sjrd my intuition tells me that this might break something Scala.js 😇

@sjrd
Copy link
Member

sjrd commented Jul 27, 2020

Thanks for the ping. In this case I don't suspect that this will cause any issue in Scala.js. We've got the same model as the JVM for binary signatures, overriding rules and all that. :)

@lrytz lrytz force-pushed the inheritSignature branch from 7b195e0 to 64ebccf Compare July 27, 2020 17:08
@lrytz lrytz requested a review from retronym July 27, 2020 19:40
@SethTisue SethTisue added the release-notes worth highlighting in next release notes label Jul 29, 2020
@retronym
Copy link
Member

Things get hairy when involving Java sources.

$ tail sandbox/{C.scala,Test.java}
==> sandbox/C.scala <==
class C[A] {
  def foo(a: A): A = a
}

class Sub extends C[String] {
  @scala.annotation.inheritSignature
  override def foo(a: String): String = a
}

==> sandbox/Test.java <==
public class Test extends Sub {
  @Override
  public String foo(String a) {
    return a;
  }
  public static void main(String[] args) {
    System.out.println(new Test().foo("hello"));
  }
}
$ scalac-pr 9141 -d /tmp sandbox/C.scala  && javac -cp /tmp -d /tmp sandbox/Test.java
sandbox/Test.java:3: error: name clash: foo(String) in Test overrides a method whose erasure is the same as another method, yet neither overrides the other
  public String foo(String a) {
                ^
  first method:  foo(Object) in Sub
  second method: foo(A) in C
  where A is a type-variable:
    A extends Object declared in class C
1 error

@mkeskells
Copy link
Contributor

@retronym @lrytz Did anything come of the alternate idea that we had last week, of the compiler ignoring some annotated methods (in this case the more specific signature), and therefore binding to the parent method if this annotation is present?
e.g.
@since (version="1.2.3") def foo
would only be considered as a target if the compiler had a --fromVersion= something>=1.2.3

@lrytz
Copy link
Member Author

lrytz commented Sep 9, 2020

Things get hairy when involving Java sources.

The example compiles when I retain the BRIDGE flag on the Scala compiler generated bridge method foo(Object)Object. However, a super.foo(a) call in the Java source then fails to compile with

Test.java:4: error: foo(String) has private access in Sub
    System.out.println(super.foo(a));
                            ^
1 error

On a higher level, the classfiles produced when using @inheritSignature conform to the VM spec, but they have unexpected shapes when used on javac's classpath.

class C {
  // signature (TA;)TA;
  public foo(Ljava/lang/Object;)Ljava/lang/Object;
}

class Sub {
  private foo(Ljava/lang/String;)Ljava/lang/String;

  public foo(Ljava/lang/Object;)Ljava/lang/Object;
}

What flags should Sub.foo have?

  • if we put bridge, it breaks javac's expectations because there's no corresponding public String-String method, the super call fails
  • if we don't put bridge, it breaks javac's expectations because it's not something you could write in source. class A<T> { T foo(T x) } and then class B extends A<String> { Object foo(Object x) } would give "name clash: foo(Object) in B and foo(T) in A have the same erasure, yet neither overrides the other"
  • maybe a signature attribute on the bridge method could refine it to "(String)String", but if that works it's a hack

The VM doesn't look at generic signatures, and not at the bridge or synthetic flags either.

I tested with an intermediate Scala class (that overrides the method without @inheritSignature), but that doesn't help either, javac still complains about the mismatch in the methods it sees in C and Sub.

So the result seems to be that a method annotated @inheritSignature cannot be called in a super call in a Java subclass anymore.

@dwijnand
Copy link
Member

On the upside (perhaps it was obvious already) but failing at javac time is better than failing at jit time...

@smarter
Copy link
Member

smarter commented Oct 6, 2020

if we put bridge, it breaks javac's expectations because there's no corresponding public String-String method, the super call fails

What happens if you use ACC_SYNTHETIC (aka Artifact) but not ACC_BRIDGE ?

@lrytz
Copy link
Member Author

lrytz commented Oct 7, 2020

Marking the overriding method synthetic (with our without bridge) confuses javac even more and you can't even call it anymore from Java

class C[A] {
  def foo(a: A): A = a
}

class Sub extends C[String] {
  @scala.annotation.inheritSignature
  override def foo(a: String): String = a
}
public class Test extends Sub {
  public static void main(String[] args) {
    System.out.println(new Test().foo("hello"));
  }
}

gives

Test.java:8: error: foo(A) in C is defined in an inaccessible class or interface
    System.out.println(new Test().foo("hello"));
                                 ^
  where A is a type-variable:
    A extends Object declared in class C
1 error

So at least for this example, the best tradeoff seems to be to remove synthetic and leave bridge, which allows calling the method and overriding it, but not invoking it in a super call. As a workaround one could write a Scala subclass with a manual super accessor, something like this:

class Buk extends Sub {
  final def superFoo(a: String) = super[Sub].foo(a)
}

@lrytz lrytz force-pushed the inheritSignature branch from 64ebccf to 435dcaa Compare October 7, 2020 09:06
@smarter
Copy link
Member

smarter commented Oct 7, 2020

what if it's public and synthetic?

@lrytz
Copy link
Member Author

lrytz commented Oct 7, 2020

it (the overriding method, the bridge) was always public in my testing. or do you mean making the actual implementation method public? then we have the issue that javac will link against it.

@smarter
Copy link
Member

smarter commented Oct 7, 2020

The actual implementation method, my assumption was that if it's public but synthetic javac won't link against it, but I haven't checked that.

@lrytz
Copy link
Member Author

lrytz commented Oct 7, 2020

Ah, I see. That doesn't work either... I tested javac 8 and 11.

public class Sub extends C<java.lang.String>
...
  public java.lang.String foo(java.lang.String);
    descriptor: (Ljava/lang/String;)Ljava/lang/String;
    flags: (0x1001) ACC_PUBLIC, ACC_SYNTHETIC
...


public class Test extends Sub
...
  public static void main(java.lang.String[]);
...
        12: invokevirtual #10                 // Method foo:(Ljava/lang/String;)Ljava/lang/String;

@smarter
Copy link
Member

smarter commented Oct 7, 2020

Hmm, could you post a full example? Here's what I tried and what I got:

diff --git src/compiler/scala/tools/nsc/transform/PostErasure.scala src/compiler/scala/tools/nsc/transform/PostErasure.scala
index 6a96fa6574..284c8ec108 100644
--- src/compiler/scala/tools/nsc/transform/PostErasure.scala
+++ src/compiler/scala/tools/nsc/transform/PostErasure.scala
@@ -47,7 +47,7 @@ trait PostErasure extends InfoTransform with TypingTransformers with scala.refle
         case ValueClass.BoxAndUnbox(v)             => finish(v)          // (new B(v)).unbox        ==> v
         case ValueClass.BoxAndCompare(v1, op, v2)  => binop(v1, op, v2)  // new B(v1) == new B(v2)  ==> v1 == v2
         case dd: DefDef if dd.symbol.hasAttachment[InheritedSignature] =>
-          dd.symbol.setFlag(Flags.PRIVATE).resetFlag(Flags.OVERRIDE)
+          dd.symbol.setFlag(Flags.PRIVATE).setFlag(Flags.ARTIFACT).resetFlag(Flags.OVERRIDE)
           dd.symbol.getAndRemoveAttachment[InheritedSignature].get.bridge.resetFlag(Flags.ARTIFACT).setFlag(Flags.OVERRIDE)
           tree
         case tree                                  => tree
class C[A] {
  def foo(a: A): A = a
}

class Sub extends C[String] {
  @scala.annotation.inheritSignature
  override def foo(a: String): String = a
  //   private java.lang.String foo(java.lang.String);
 //   descriptor: (Ljava/lang/String;)Ljava/lang/String;
 //   flags: ACC_PRIVATE, ACC_SYNTHETIC
 // public java.lang.Object foo(java.lang.Object);
 //   descriptor: (Ljava/lang/Object;)Ljava/lang/Object;
 //   flags: ACC_PUBLIC, ACC_BRIDGE, ACC_SYNTHETIC
}
public class Test extends Sub {
  void sup() {
    super.foo(""); //  invokespecial #3       // Method Sub.foo:(Ljava/lang/Object;)Ljava/lang/Object;
  }
  void cur() {
    foo(""); // invokevirtual #4                  // Method foo:(Ljava/lang/Object;)Ljava/lang/Object;
  }
}

@lrytz
Copy link
Member Author

lrytz commented Oct 7, 2020

Oh, you were all along suggesting to make the actual implementation synthetic, not the bridge... I'm sorry I misunderstood 🤦‍♂️ Thank you for digging in! I pushed the change.

@smarter
Copy link
Member

smarter commented Oct 7, 2020

By the way, it seems that the resetFlag(Flags.ARTIFACT) isn't actually preventing ACC_SYNTHETIC from being used, probably because when we have the Bridge flag set we always emit ACC_BRIDGE | ACC_SYNTHETIC (which makes sense, javac never sets ACC_BRIDGE without setting ACC_SYNTHETIC).

@lrytz
Copy link
Member Author

lrytz commented Jan 27, 2021

Added some compile-time errors in RefChecks, allows making assumptions later on.

Added a WIP hack for the REPL issue - the problem is that modifying flags (like setting PRIVATE) isn't reverted in adaptToNewRun. @retronym better ideas are welcome, the WIP is mostly for showing what the issue is.

@lrytz
Copy link
Member Author

lrytz commented Jan 27, 2021

this is the first time that a new annotation will be introduced in a minor release

we did it before at least for @nowarn

@lrytz
Copy link
Member Author

lrytz commented Jan 27, 2021

Also, we'll probably make this annotation private[scala] as it's only needed in the standard library. Other libraries are not bound to forwards binary compatibility.

@dwijnand
Copy link
Member

Does this do the right/enough things for value classes? For example can I define override def signum: Int = math.signum(self.toInt) in RichByte in a forwards-compatible change? Aka without:

[error]  * extension method signum$extension(Byte)Int in object scala.runtime.RichByte does not have a correspondent in other version
[error]    filter with: ProblemFilters.exclude[DirectMissingMethodProblem]("scala.runtime.RichByte.signum$extension")
[error]  * extension static method signum$extension(Byte)Int in class scala.runtime.RichByte does not have a correspondent in other version
[error]    filter with: ProblemFilters.exclude[DirectMissingMethodProblem]("scala.runtime.RichByte.signum$extension")

@lrytz
Copy link
Member Author

lrytz commented Jan 28, 2021

We can't make this work for value classes to avoid boxing.

object Test {
  trait A extends Any { def f = 1 }
  implicit class B(private val s: String) extends AnyVal with A
}

A call "".f translates to new B("").f, so it boxes the string. What you're after is adding an override to avoid the boxing. If you do that (without @inheritSignature), "".f will translate to B.f$extension(""), which is not forwards binary compatible. That's the issue: we need to make sure that code compiled against a new @inheritSignature override runs with an old standard library. So there's no way to avoid the boxing, as this is the only way f can be called on the old standard library.

Shifting away from value classes, we can't add new overrides to avoid boxing either:

class A[T] { def a(x: T): Int = 1}
object B extends A[Int] {
  @annotation.inheritSignature override def a(x: Int): Int = 1
}

A call B.a(1) will box the argument, because in an old standard library there is no method in B that takes an Int parameter. The (I)I method in B is made private.

@lrytz lrytz force-pushed the inheritSignature branch 3 times, most recently from a84c1cb to 822294e Compare January 29, 2021 08:50
@SethTisue SethTisue modified the milestones: 2.13.5, 2.13.6 Feb 9, 2021
*
* Invocations of `C.f: String` are rewritten to `A.f`.
*/
final class inheritSignature extends StaticAnnotation
Copy link
Member Author

Choose a reason for hiding this comment

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

scala/bug#12336 might apply here as well, but in this case we do want to pickle the annotation for separate compilation.

There seems to be support in the compiler for missing annotation classes, just using a method that carries an annotation with a missing symbol doesn't trigger an error. Mixin can fail though, as shown in the bug report.

lrytz added 5 commits April 6, 2021 12:15
Methods annotated `@inheritSignature` are emitted `private` in bytecode,
which allows adding binary compatible overrides when the bytecode
signature is refined.

```
class A { def f: AnyRef = "A.f" }
class C extends A { @inheritSignature override def f: String = "C.f" }
```

The method `C.f: String` is made `private`, the bridge remains `public`
(and the `bridge` flag is removed). Calls to `C.f` are rewritten to
`A.f`.
@lrytz lrytz force-pushed the inheritSignature branch from 822294e to 6a87d16 Compare April 6, 2021 10:16
@lrytz
Copy link
Member Author

lrytz commented Apr 6, 2021

So the issue observed with scala/bug#12336 also applies here, and it's not enough to mark the annotation private[scala].

If we use @inheritSignature in the standard library (compiled with 2.13.6, which includes the annotation)

package scala.kuh
trait Ah {
  def x: Object = ""
}
trait A extends Ah {
  @annotation.inheritSignature override def x = ""
}

some other library creates a sub-trait (compiled also with 2.13.6)

package coop
trait B extends scala.kuh.A

using the sub-trait with an earlier compiler/stdlib (2.13.5) then fails:

class C extends coop.B
C.scala:1: error: Symbol 'type scala.annotation.inheritSignature' is missing from the classpath.
This symbol is required by ' <none>'.
Make sure that type inheritSignature is in your classpath and check for conflicting dependencies with `-Ylog-classpath`.
A full rebuild may help if 'A.class' was compiled against an incompatible version of scala.annotation.
class C extends coop.B
                     ^
1 error

In fact, marking trait methods @inheritSignature shows more bugs, for example:

package scala.kuh
trait Ah {
  def x: Object = ""
}
trait A extends Ah {
  @annotation.inheritSignature override def x = ""
}
class C extends A
object Test {
  def main(args: Array[String]): Unit = {
    println((new C).x)
  }
}

gives at runtime

Exception in thread "main" java.lang.IllegalAccessError: tried to access method scala.kuh.A.x()Ljava/lang/String; from class scala.kuh.C

If the object Test is compiled with an older version (2.13.5), we get

Exception in thread "main" java.lang.NoSuchMethodError: scala.kuh.C.x()Ljava/lang/String;

So maybe what we have to enforce is that @inheritSignature is not allowed in trait methods, I'll think and test if that's safe (and still useful).

@dwijnand dwijnand modified the milestones: 2.13.6, 2.13.7 May 10, 2021
@lrytz
Copy link
Member Author

lrytz commented May 19, 2021

This is not going to work if we assume that an existing compiler will ever encouter a newer standard library in which @inheritSignature was used.

Compiling this with support for @inheritSignature

package scala.something

import scala.annotation.inheritSignature

class A[T] {
  def f(x: T): Int = 0
}

class ASub extends A[Int] {
  @inheritSignature override final def f(x: Int): Int = 1
}

and then this with 2.13.6:

package user

object Test {
  def main(args: Array[String]): Unit = {
    // java.lang.IllegalAccessError: class user.Test$ tried to access private method 'int scala.something.ASub.f(int)'
    println((new scala.something.ASub2).f(1))
  }
}

booms. Seems risky, even if standard library and compiler versions are usually linked in Scala 2 world.

Considering Scala 3, updating the standard library to a 2.13.7 that uses @inheritSignature will only generate working bytecode if support for @inheritSignature is added to the Scala 3 compiler. I assume it's easily possible (and intended) to put a new standard library on the classpath of an existing Scala 3 compiler such as 3.0.0 final.

So we cannot ship this PR.

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.

Bridge methods prevent forward-compatible library evolution

9 participants