bug icon indicating copy to clipboard operation
bug copied to clipboard

ClassCastException when calling a trait method with call-by-name argument if implemented as single abstract method

Open ruslan2009 opened this issue 7 years ago • 2 comments

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.

ruslan2009 avatar Nov 01 '18 13:11 ruslan2009

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

SethTisue avatar Feb 05 '19 21:02 SethTisue

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))

som-snytt avatar Aug 10 '24 01:08 som-snytt