Skip to content

SAM and Subtyping causes AbstractMethodError #10512

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
ikempf opened this issue Sep 16, 2017 · 2 comments
Closed

SAM and Subtyping causes AbstractMethodError #10512

ikempf opened this issue Sep 16, 2017 · 2 comments
Assignees
Labels
Milestone

Comments

@ikempf
Copy link

ikempf commented Sep 16, 2017

The following fails on 2.12.3
This seems to be related to SAM, subtyping, implicit resolution and ad-hoc polymorphism.

trait JsonEncoder[A] {
  def encode(value: A): JsonValue
}

trait JsonObjectEncoder[A] extends JsonEncoder[A] {
  def encode(value: A): JsonObject
}

object JsonEncoderInstances {

  implicit val stringEncoder: JsonEncoder[String] =
    JsonString.apply

  implicit val intEncoder: JsonEncoder[Int] =
    i => JsonNumber(i)

  implicit def listEncoder[A](implicit encoder: JsonEncoder[A]): JsonObjectEncoder[List[A]] =
    l => JsonObject(l.zipWithIndex.map {
      case (a, index) => index.toString -> encoder.encode(a)
    })

}
// Following fails with java.lang.AbstractMethodError: com.ikempf.sandbox.sam_subtyping_bug.JsonEncoderInstances$$$Lambda$142/1556995360.encode(Ljava/lang/Object;)Lcom/ikempf/sandbox/sam_subtyping_bug/JsonValue;
implicitly[JsonEncoder[List[String]]].encode(List("a","b"))

The bug dissapears under following conditions

  • I do not use the SAM syntax for the listEncoder[A] (non parameterized types do not cause seem to cause the exception)
  • I return a JsonEncoder[List[A]] instead of a JsonObjectEncoder[List[A]]
  • I resolve a implicit[JsonObjectEncoder[List[String]]] instead of a implicit[JsonEncoder[List[String]]]
  • I do not use implicit resolution and directly use listEncoder[String].encode(List("a","b"))

This is an edge case that fails at runtime but it seems to me that it should compile and run just fine.
Here is a sample repo that reproduces the bug.

Environnment

Scala: 2.12.3
Java: 1.8.0_102
JVM: HotSpot 25.102-b14

@hrhino
Copy link

hrhino commented Sep 16, 2017

Turns out you don't the implicits at all:

trait JV; trait JO extends JV
trait JE {
  def enc(a: Any): JV
}
trait JOE extends JE {
  def enc(a: Any): JO
}
object Test extends App {
  val one: JOE = (a: Any) => null
  val two: JE = one
  two.enc(12)
}

gives

java.lang.AbstractMethodError: Test$$$Lambda$144/708049632.enc(Ljava/lang/Object;)LJV;
	at Test$.delayedEndpoint$Test$1(exn.scala:16)
	at Test$delayedInit$body.apply(exn.scala:11)
	at scala.Function0.apply$mcV$sp(Function0.scala:34)
	at scala.Function0.apply$mcV$sp$(Function0.scala:34)
	at scala.runtime.AbstractFunction0.apply$mcV$sp(AbstractFunction0.scala:12)
	at scala.App.$anonfun$main$1(App.scala:76)

because we're not giving our lambda the right bridges:

BootstrapMethods:
  0: #88 invokestatic java/lang/invoke/LambdaMetafactory.altMetafactory:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;[Ljava/lang/Object;)Ljava/lang/invoke/CallSite;
    Method arguments:
      #89 (Ljava/lang/Object;)LJO;
      #92 invokestatic Test$.$anonfun$one$1:(Ljava/lang/Object;)LJO;
      #89 (Ljava/lang/Object;)LJO;
      #93 2
      #94 0

I'm sure I've seen this or one of its twins before, though.

@ikempf
Copy link
Author

ikempf commented Sep 17, 2017

I think you are reffering to 10477
I first thought it was a duplicate (it may be ?) but in my case the bug seems to only arise with a specific combination of Scala features used.
Also when I do not use implicit resolution the issue does not manifest (I'll add this to my inital post)

@hrhino hrhino self-assigned this Sep 17, 2017
hrhino added a commit to hrhino/scala that referenced this issue Sep 18, 2017
If a SAM trait's abstract method overrides a method in a
supertrait while changing the return type, the generated
invokedynamic instruction needs to pass the types of the
overridden methods to `LambdaMetaFactory` so that bridge
methods can be added to the generated lambda.

Java does this differently: it generates a default method
in the subinterface overriding the superinterface method.
Theoretically we could also generate the default bridges,
but that is a binary-incompatible change, so I think it's
safer to just add the bridges in the invokedynamic.

Honestly I'm surprised that this hasn't come up already.

Fixes scala/bug#10512.
hrhino added a commit to hrhino/scala that referenced this issue Sep 18, 2017
If a SAM trait's abstract method overrides a method in a
supertrait while changing the return type, the generated
invokedynamic instruction needs to pass the types of the
overridden methods to `LambdaMetaFactory` so that bridge
methods can be added to the generated lambda.

Java does this differently: it generates a default method
in the subinterface overriding the superinterface method.
Theoretically we could also generate the default bridges,
but that is a binary-incompatible change, so I think it's
safer to just add the bridges in the invokedynamic.

Honestly I'm surprised that this hasn't come up already.

Fixes scala/bug#10512.
hrhino added a commit to hrhino/scala that referenced this issue Sep 22, 2017
If a SAM trait's abstract method overrides a method in a
supertrait while changing the return type, the generated
invokedynamic instruction needs to pass the types of the
overridden methods to `LambdaMetaFactory` so that bridge
methods can be added to the generated lambda.

SAM `Function` trees now have a synthetic `ClassSymbol`,
into which we enter an overriding concrete method symbol
that represents the runtime LMF-generated implementation
of that function. `erasure` then computes bridge methods
for that class, which are added to the `LMF.metafactory`
call in `jvm`.

Java does this instead by generating a default method in
the subinterface overriding the superinterface's method.
Theoretically, we could generate the bridges in the same
way, but that has the downside that the project with the
interfaces would need recompiled, which might not be the
project that creates the lambdas. Generating bridges the
LMF way means that only the classes affected by this bug
need to be recompiled to get the fix.

Honestly I'm surprised that this hasn't come up already.

Fixes scala/bug#10512.
hrhino added a commit to hrhino/scala that referenced this issue Sep 22, 2017
If a SAM trait's abstract method overrides a method in a
supertrait while changing the return type, the generated
invokedynamic instruction needs to pass the types of the
overridden methods to `LambdaMetaFactory` so that bridge
methods can be added to the generated lambda.

SAM `Function` trees now have a synthetic `ClassSymbol`,
into which we enter an overriding concrete method symbol
that represents the runtime LMF-generated implementation
of that function. `erasure` then computes bridge methods
for that class, which are added to the `LMF.metafactory`
call in `jvm`.

Java does this instead by generating a default method in
the subinterface overriding the superinterface's method.
Theoretically, we could generate the bridges in the same
way, but that has the downside that the project with the
interfaces would need recompiled, which might not be the
project that creates the lambdas. Generating bridges the
LMF way means that only the classes affected by this bug
need to be recompiled to get the fix.

Honestly I'm surprised that this hasn't come up already.

Fixes scala/bug#10512.
hrhino added a commit to hrhino/scala that referenced this issue Nov 29, 2017
If a SAM trait's abstract method overrides a method in a
supertrait while changing the return type, the generated
invokedynamic instruction needs to pass the types of the
overridden methods to `LambdaMetaFactory` so that bridge
methods can be added to the generated lambda.

SAM `Function` trees now have a synthetic `ClassSymbol`,
into which we enter an overriding concrete method symbol
that represents the runtime LMF-generated implementation
of that function. `erasure` then computes bridge methods
for that class, which are added to the `LMF.metafactory`
call in `jvm`.

Java does this instead by generating a default method in
the subinterface overriding the superinterface's method.
Theoretically, we could generate the bridges in the same
way, but that has the downside that the project with the
interfaces would need recompiled, which might not be the
project that creates the lambdas. Generating bridges the
LMF way means that only the classes affected by this bug
need to be recompiled to get the fix.

Honestly I'm surprised that this hasn't come up already.

Fixes scala/bug#10512.
@retronym retronym added this to the 2.12.5 milestone Jan 12, 2018
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

3 participants