Skip to content

AbstractMethodError when inheriting trait from java abstract class and referencing val from the trait #13702

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

Closed
smarter opened this issue Oct 6, 2021 · 9 comments
Labels
compat:java itype:bug Spree Suitable for a future Spree

Comments

@smarter
Copy link
Member

smarter commented Oct 6, 2021

This is the exact same bug as in Scala 2: scala/bug#12456

trait ScalaTrait {
  def foo1: String
  def foo2: String = "default foo value 2"

  val val1: String
  val val2: String = "default val value 2"
}
public abstract class JavaAbstractClass implements ScalaTrait {
    public String javaFoo1() { return foo1(); }
    public String javaFoo2() { return foo2(); }

    public String javaVal1() { return val1(); }
    public String javaVal2() { return val2(); }
}
import scala.util.Try

object ScalaClass extends JavaAbstractClass {
  override def foo1: String = "implemented foo in scala 1"
  override val val1: String = "implemented val in scala 1"

  def main(args: Array[String]): Unit = {
    println(Try(javaFoo1()))
    println(Try(javaFoo2()))

    println(Try(javaVal1()))
    println(Try(javaVal2()))
  }
}

problem

The program compiles fine, but an exception is thrown at runtime:

Success(implemented foo in scala 1)
Success(default foo value 2)
Success(implemented val in scala 1)
Exception in thread "main" java.lang.AbstractMethodError: Method ScalaClass$.val2()Ljava/lang/String; is abstract
	at ScalaClass$.val2(ScalaClass.scala)
	at JavaAbstractClass.javaVal2(JavaAbstractClass.java:6)
	at ScalaClass$.$anonfun$main$4(ScalaClass.scala:12)
	at scala.util.Try$.apply(Try.scala:210)
	at ScalaClass$.main(ScalaClass.scala:12)
	at ScalaClass.main(ScalaClass.scala)
@anatoliykmetyuk anatoliykmetyuk added the Spree Suitable for a future Spree label Oct 15, 2021
@anatoliykmetyuk
Copy link
Contributor

anatoliykmetyuk commented Apr 4, 2022

This issue was picked for the Issue Spree 14 of 12th April which takes place in a week from now. @gagandeepkalra and @ghostbuster91 will be working on it. If you have any insight into the issue or guidance on how to fix it, please leave it here.

@ghostbuster91
Copy link
Contributor

ghostbuster91 commented Apr 12, 2022

Here is what we discovered:

Currently it is impossible to have a regular java class inherit a scala trait that has a val field which has a default value.

trait ScalaTrait {
  val foo: String = "abc"
}
public class JavaClass implements ScalaTrait {
}
object Test  {

  def main(args: Array[String]): Unit = {

    val javaC = new JavaClass
    assert(javaC.foo == "abc" )
  }
}

Above code doesn't compile because:

 error: JavaClass is not abstract and does not override abstract method ScalaTrait$_setter_$foo$eq(String) in ScalaTrait
public class JavaClass implements ScalaTrait {

That is because of how the vals in traits with default values are encoded.

trait ScalaTrait {
  val  foo :String = "abc"
}

gets transformed into:

package <empty> {
  @SourceFile("tests/run/i13702_2/ScalaTrait.scala") trait ScalaTrait extends
    Object
   {
    def $init(): Unit =
      {
        this.ScalaTrait$_setter_$foo_$eq("abc")
        ()
      }
    def foo(): String
    def ScalaTrait$_setter_$foo_$eq(x$0: String): Unit
  }
}

Java doesn't implement the getter and setter methods.

In case of abstract class the code compiles, because an abstract java class is allowed to have unimplemented members and for the scala compiler it seems that everything is ok.

It crashes in runtime because the getter is still unimplemented.

Should fix the compiler to fail the compilation in such cases?

In case when there is a scala class between the java class and a scala trait with default value (like in #10509), that scala class implements the abstract trait members and is responsible for calling the initialization method $init.

class ScalaClass extends ScalaTrait {
  
}

trait ScalaTrait {
  val  foo :String = "abc"
}

gets transformed into:

package <empty> {
  @SourceFile("tests/run/i13702_2/ScalaTrait.scala") trait ScalaTrait extends
    Object
   {
    def $init(): Unit =
      {
        this.ScalaTrait$_setter_$foo_$eq("abc")
        ()
      }
    def foo(): String
    def ScalaTrait$_setter_$foo_$eq(x$0: String): Unit
  }
  @SourceFile("tests/run/i13702_2/ScalaTrait.scala") class ScalaClass extends
    Object
  , ScalaTrait {
    def <init>(): Unit =
      {
        super()
        super[ScalaTrait].$init()
        scala.runtime.Statics#releaseFence()
        ()
      }
    private var foo: String
    def foo(): String = this.foo
    def ScalaTrait$_setter_$foo_$eq(x$0: String): Unit = this.foo = x$0
  }
}

We don't see any way how that could work for a java class. Even if we changed the encoding and didn't have to implement the abstract method, the $init method wouldn't be called.

We assumme that changing $int method to <init> which gets called by java isn't an option for us because scala traits compile into java interfaces and they cannot have the initialization block(not sure if that is called like that).

@smarter
Copy link
Member Author

smarter commented Apr 12, 2022

Should fix the compiler to fail the compilation in such cases?

That would certainly be better than crashing at runtime, so if you think that's easier to implement, go for it.

@gagandeepkalra
Copy link
Contributor

gagandeepkalra commented Apr 13, 2022

We can probably reduce scope, considering we have 2 scenarios var/val (with default values)

For the val case, we can treat it exactly like a def with a default value, and not create a setter method at all?

  @SourceFile("tests/run/i8599/ScalaDefs_1.scala") trait A() extends Object {
    def a(): Int = 1
    def A$_setter_$a_$eq(x$0: Int): Unit
  }

and keep the trait initialization as is for var case.

@smarter
Copy link
Member Author

smarter commented Apr 17, 2022

and not create a setter method at all?

This doesn't sound like a binary compatible change, so not something we can do in scala 3.

@anatoliykmetyuk
Copy link
Contributor

This issue was picked for the Issue Spree 15 of May 3rd which takes place a week from now. @gagandeepkalra and @ghostbuster91 will be working on it. If you have any insight into the issue or guidance on how to fix it, please leave it here.

@mbovel
Copy link
Member

mbovel commented Sep 23, 2024

Reproduced today:

  ~/scala-snippets-6 cat 13702/ScalaTrait.scala 
trait ScalaTrait {
  def foo1: String
  def foo2: String = "default foo value 2"

  val val1: String
  val val2: String = "default val value 2"
}
  ~/scala-snippets-6 cat 13702/JavaAbstractClass.java 
public abstract class JavaAbstractClass implements ScalaTrait {
    public String javaFoo1() { return foo1(); }
    public String javaFoo2() { return foo2(); }

    public String javaVal1() { return val1(); }
    public String javaVal2() { return val2(); }
}
  ~/scala-snippets-6 cat 13702/ScalaTrait.scala 
trait ScalaTrait {
  def foo1: String
  def foo2: String = "default foo value 2"

  val val1: String
  val val2: String = "default val value 2"
}
  ~/scala-snippets-6 scala 13702                     
Success(implemented foo in scala 1)
Success(default foo value 2)
Success(implemented val in scala 1)
Exception in thread "main" java.lang.AbstractMethodError: Method ScalaClass$.val2()Ljava/lang/String; is abstract
        at ScalaClass$.val2(ScalaClass.scala)
        at JavaAbstractClass.javaVal2(JavaAbstractClass.java:6)
        at ScalaClass$.main$$anonfun$4(ScalaClass.scala:12)
        at scala.util.Try$.apply(Try.scala:217)
        at ScalaClass$.main(ScalaClass.scala:12)
        at ScalaClass.main(ScalaClass.scala)
  ~/scala-snippets-6 scala --version
Scala code runner version: 1.4.0
Scala version (default): 3.5.0
  ~/scala-snippets-6 javap 13702/.scala-build/13702_b85f7ed1e7-6012b1875a/classes/main/ScalaTrait.class
Compiled from "ScalaTrait.scala"
public interface ScalaTrait {
  public static void $init$(ScalaTrait);
  public abstract java.lang.String foo1();
  public static java.lang.String foo2$(ScalaTrait);
  public default java.lang.String foo2();
  public abstract java.lang.String val1();
  public abstract java.lang.String val2();
  public abstract void ScalaTrait$_setter_$val2_$eq(java.lang.String);
}

@mbovel
Copy link
Member

mbovel commented Sep 23, 2024

It's not clear to me what are the next steps to fix this issue.

If I understand correctly, with the current scheme, a class implementing a trait with a val is responsible to call the trait's init function and to implement the field's getter.

How can that ever work when implementing a Scala trait from Java, as we can't influence the compilation of the Java code?

If that doesn't work, why should we fix the special case where the Java class is again subclassed in Scala?

And if we fix this specific scenario, how should we do it? Should the compiler somehow figure out that val2 has a default value? I guess it can't find it from the Java ByteCodec only? Could it do it from the information in the corresponding tasty file? But would a tasty file always be available?

@mbovel
Copy link
Member

mbovel commented Sep 27, 2024

We discussed this today during the compiler meeting and headed towards "won't fix" for this issue.

The rational is the following:

  • We already have a problem at the Java level: if JavaAbstractClass is not abstract and one tries to call .val2() from Java, this will already fail. With the current encoding (which we cannot change for backward compatibility reasons), Scala traits with fields with default values just will not be usable from Java.
  • Specially handling the case where the Java class is again extended from Scala would be a lot of work for a very specific case and arguably very minor improvement.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
compat:java itype:bug Spree Suitable for a future Spree
Projects
None yet
Development

No branches or pull requests

5 participants