bug
bug copied to clipboard
ClassCastException on Value Class when implementing an interface using a Single Abstract Method
Reproduction steps
Scala version: 2.13.14
scalac option -Ydelambdafy:method (which is the default setting, bug doesn't happen with -Ydelambdafy:inline)
case class StringValue(value: String) extends AnyVal
trait Foo[A] {
def singleMethod(arg: A): StringValue
}
val foo1: Foo[Int] = new Foo[Int] {
override def singleMethod(arg: Int): StringValue = StringValue(arg.toString)
}
val foo2: Foo[Int] = (arg: Int) => StringValue(arg.toString)
println(foo1.singleMethod(1)) // prints "StringValue(1)"
println(foo2.singleMethod(1)) // throws ClassCastException
Exception in thread "main" java.lang.ClassCastException: class Main$StringValue cannot be cast to class java.lang.String (Main$StringValue is in unnamed module of loader 'app'; java.lang.String is in module java.base of loader 'bootstrap')
The stack trace for the exception points to the line of foo2's declaration.
Problem
Implementations of foo1 and foo2 should have identical runtime behavior. foo1's is the correct behavior
looking at the disassmbly of this code I see:
public static final java.lang.String $anonfun$foo2$1$adapted(java.lang.Object);
Code:
0: new #14 // class Test$StringValue
3: dup
4: aload_0
5: invokestatic #74 // Method scala/runtime/BoxesRunTime.unboxToInt:(Ljava/lang/Object;)I
8: invokestatic #76 // Method $anonfun$foo2$1:(I)Ljava/lang/String;
11: invokespecial #79 // Method Test$StringValue."<init>":(Ljava/lang/String;)V
14: checkcast #81 // class java/lang/String
17: areturn
LineNumberTable:
line 11: 0
LocalVariableTable:
Start Length Slot Name Signature
0 18 0 arg Ljava/lang/Object;
note that this is creating a new StringValue then immediately casting it to a String.I don't completely understand how that method is converted to a Foo, but I'm pretty sure that is getting used to construct it.
And interestingly there is a also the $anonfun$foo2$1 method, which simply calls toString on the Int argument. Which this method calls.
if you define foo2 as:
val foo2: Foo[Integer] = (arg: Integer) => StringValue(arg.toString)
then it works fine. So I'm guessing the bug has something to do with unboxing the integer.
Although I'm not sure why unboxing arg results in wrapping the return value in a StringValue at runtime, and not respecting the extends AnyVal.
Running the compiler with -Vprint:_ is a good way to try to pinpoint the point in time (which compiler phase) where something first goes wrong.
Does Scala 3 have the bug as well?
I could not reproduce on Scala3
Observations from the output of scalac -Vprint:_:
After the "uncurry" phase, the definition of the (now private) foo2 field is:
private[this] val foo2: Test.Foo[Int] = {
final <artifact> def $anonfun$foo2(arg: Int): Test.StringValue = new Test.StringValue(arg.toString());
((arg: Int) => $anonfun$foo2(arg))
};
I'm not really sure why it factored out the $anonfun$foo2 function. But I'm not terribly familiar with how the compiler works.
Then everything proceeds about how I would expect, until delambdify.
And we have:
final <static> <artifact> def $anonfun$foo2$1(arg: Int): String = java.lang.Integer.toString(arg);
def <init>(): Test.type = {
Test.super.<init>();
Test.this.foo1 = {
new <$anon: Test$Foo>()
};
Test.this.foo2 = {
$anonfun()
};
()
};
final <static> <artifact> def $anonfun$foo2$1$adapted(arg: Object): String = new Test$StringValue(Test.this.$anonfun$foo2$1(unbox(arg))).$asInstanceOf[String]()
I'm not sure what $anonfun() in the definition of Test.this.foo2 is, since it doesn't seem to be defined, but I'm guessing it has something to do with the new $anonfun$foo2$1$adapted method that was added.
And that method has an obvious type error, because it constructs a new Test$StringValue (that had previously been erased) and immediately casts it to a String.
And this definition persists through the jvm phase.
The anon$1 class that is generated for foo1 on the other hand, has no reference to the Test$StringValue as that class has been erased from it.
NOTE: I wrapped the above code in an object named Test.
I started looking at this earlier, but then got distracted by a different CCE with SAM (insert meme about whistling over my shoulder).
I see you figured this out already. Delambdafy is given correct instructions, but I think the call to adapt the result here is wrong:
https://github.com/scala/scala/blame/2.13.x/src/compiler/scala/tools/nsc/transform/Delambdafy.scala#L247
When it creates the bridge def foo$adapted = foo, it uses ErasedValueType(class StringValue, String) instead of its post-erasure.
val erasedFunctionResultType = postErasure.elimErasedValueType(functionResultType)
val bridge = postErasure.newTransformer(unit).transform(DefDef(methSym, List(bridgeParams.map(ValDef(_))),
//adaptToType(forwarderCall.setType(functionResultType), bridgeResultType))).asInstanceOf[DefDef]
adaptToType(forwarderCall.setType(erasedFunctionResultType), bridgeResultType))).asInstanceOf[DefDef]
That works, but my day is done for now.
The linked PR tries to do something subtle; I wasn't able to glance at what would dotty do.