bug
bug copied to clipboard
ClassCastException when calling a trait method with call-by-name argument if implemented as single abstract method
The following code:
trait Semigroup[F] { self =>
def append(f1: F, f2: => F): F
val z = 10
}
object bug extends App {
case class Box(i: Int)
val boxSemigroup: Semigroup[Box] = (x1, x2) => Box(Math.max(x1.i, x2.i))
println(boxSemigroup.append(Box(1), Box(2)))
}
crashes at runtime with ClassCastException
Exception in thread "main" java.lang.ClassCastException: bug$Box cannot be cast to scala.Function0
at bug$$anonfun$1.append(bug.scala:75)
at bug$$anonfun$1.append(bug.scala:75)
at bug$.delayedEndpoint$bug$1(bug.scala:76)
at bug$delayedInit$body.apply(bug.scala:73)
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$adapted(App.scala:76)
at scala.collection.immutable.List.foreach(List.scala:389)
at scala.App.main(App.scala:76)
at scala.App.main$(App.scala:74)
at bug$.main(bug.scala:73)
at bug.main(bug.scala)
Seems like the bytecode generated contains erroneous call to function representing call-by-name argument (instruction at 2) and then casting of it's result to Function0 (instruction at 7):
public final bug$Box append(bug$Box, scala.Function0<bug$Box>);
Code:
0: aload_1
1: aload_2
2: invokeinterface #35, 1 // InterfaceMethod scala/Function0.apply:()Ljava/lang/Object;
7: checkcast #31 // class scala/Function0
10: invokestatic #38 // Method bug$.bug$$$anonfun$boxSemigroup$1:(Lbug$Box;Lscala/Function0;)Lbug$Box;
13: areturn
Also note that if name of call-by-name argument in trait and SAM implementation are the same as in:
trait Semigroup[F] { self =>
def append(f1: F, x2: => F): F
val z = 10
}
object bug extends App {
case class Box(i: Int)
val boxSemigroup: Semigroup[Box] = (x1, x2) => Box(Math.max(x1.i, x2.i))
println(boxSemigroup.append(Box(1), Box(2)))
}
then it works as expected and compiler generates correct bytecode for this function:
public final bug$Box append(bug$Box, scala.Function0<bug$Box>);
Code:
0: aload_1
1: aload_2
2: invokestatic #32 // Method bug$.bug$$$anonfun$boxSemigroup$1:(Lbug$Box;Lscala/Function0;)Lbug$Box;
5: areturn
Tried with Scala versions 2.12.7 and 2.13.0-M5.
Also seems like this issue is related to https://github.com/scala/bug/issues/10362 since same exception is being thrown at runtime and similar bytecode is being generated in both cases.
Dotty: Exception in thread "main" java.lang.AssertionError: assertion failed: private method $anonfun in rs$line$1 accessed from method append in null #not-fixed-in-dotty
Fixed in 3.4.2, and verified that the function parameter is inferred "by-name". That is what the PR does.
That is, here, B is printed twice. There is precedent for forwarding thunks, and the justification here is that the types of the function literal are inferred. (You can't write (x: => Int) => in source.)
val boxSemigroup: Semigroup[Box] = (x1, x2) => Box(x1.i + x2.i + x2.i)
//val boxSemigroup: Semigroup[Box] = (x1: Box, x2: Box) => Box(Math.max(x1.i, x2.i)) // disallowed, by-name must be inferred
assert(boxSemigroup.append(Box(1), {println("B");Box(2)}) == Box(3))