Skip to content

SI-9702 Fix backend crash with classOf[T] annotation argument#5059

Merged
adriaanm merged 1 commit intoscala:2.12.xfrom
lrytz:t9702
Mar 31, 2016
Merged

SI-9702 Fix backend crash with classOf[T] annotation argument#5059
adriaanm merged 1 commit intoscala:2.12.xfrom
lrytz:t9702

Conversation

@lrytz
Copy link
Member

@lrytz lrytz commented Mar 23, 2016

A classOf[List[Int]] annotation argument is translated to a constant
with the AliasArgsTypeRef as constant value.

To obtain the corresponding ASM Type to emit as annotation argument, we
now go directly to the class symbol using typeSymbol, instead of
calling the generic typeToBType method, which expects only
non-parameterized types.

@scala-jenkins scala-jenkins added this to the 2.12.0-M5 milestone Mar 23, 2016
case StringTag =>
assert(const.value != null, const) // TODO this invariant isn't documented in `case class Constant`
av.visit(name, const.stringValue) // `stringValue` special-cases null, but that execution path isn't exercised for a const with StringTag
case ClazzTag => av.visit(name, typeToBType(const.typeValue).toASMType)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need the same treatment in genConstant, which contains the following?

        case ClazzTag   =>
          val toPush: BType = {
            typeToBType(const.typeValue) match {
              case kind: PrimitiveBType => boxedClassOfPrimitive(kind)
              case kind => kind
            }
          }
          mnode.visitLdcInsn(toPush.toASMType)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It turns out that code works fine. The reason is that erasure visits the Literal(Constant(..)) AST and replaces the type by a ClassNoArgsTypeRef to List. In the case of an annotation arg, the Constant(..) is part of a symbol's annotations and not touched by erasure.

@retronym
Copy link
Member

Looks like we've also got a kindred bug in erasure:

scala> class VC(val a: Any) extends AnyVal; type VCAlias = VC
defined class VC
defined type alias VCAlias

scala> classOf[VCAlias]
res1: Class[VCAlias] = class java.lang.Object

scala> classOf[VC]
res2: Class[VC] = class VC

@lrytz
Copy link
Member Author

lrytz commented Mar 23, 2016

the value class type alias issue is easy to fix, will push another commit.

@lrytz
Copy link
Member Author

lrytz commented Mar 23, 2016

review by @retronym. it includes a sketch for having run tests within junit, for now using toolboxes. maybe we should not rely on toolboxes, as java symbols are loaded through java reflection instead of the classfile parser - i fixed one bug in there.

@lrytz
Copy link
Member Author

lrytz commented Mar 23, 2016

it's a bit of a rabbit hole, here's another one that crashes with current 2.12.x (only when final!)

scala> class T { final val k = classOf[List[_]] }
java.lang.AssertionError: assertion failed: type List
    at scala.tools.nsc.backend.jvm.BTypesFromSymbols.nonClassTypeRefToBType$1(BTypesFromSymbols.scala:166)

probably won't finish that stuff today.

@lrytz
Copy link
Member Author

lrytz commented Mar 29, 2016

as so often, once you start looking more details, there's plenty of little surprises :-)

here's what happens for final val k = classOf[List[_]]

pre-req. erasure has two tree transformers: preTransform basically sets all tree's tpe to null (and does a few other adjustments), the second phase runs exitingErasure(newTyper().typed(tree).

  • type-checking a classOf[..]-TypeApply goes to typedClassOf and creates a Literal(Constant(tp)) setType ConstantType(Constant(tp)), where tp is the existential List[_]
  • since it's a final val, the namer assigns the ConstantType to the ValDef's tpt (without widening), and equally to the generated getter
  • for the getter, UnCurry replaces the DefDef's rhs by a Literal (because the return type is a ConstantType)
  • erasure's preTransform has a special case for Literal trees where the constant holds a type (i.e., classOf-literals): the Type in the constant is replaced by its erased version. this applies for both the ValDef's and the getter DefDef's rhs.
    • interesting: treeCopy.Literal(tree, Constant(erased)) will copy the original ConstantType(Constant(tp)) (where tp is still the existential) onto the newly created literal
    • but at the end, preTransform invokes tree.clearType(), so the type is set to null

now we're in the re-typing phase of erasure

  • the Literal is assigned type t1 = ConstantType(Constant(List))
  • typedValDef computes the expected type to type-check the rhs. this expected type is basically typedType(vdef.tpt).
    • the vdef.tpt is a t2 = TypeTree(ConstantType(Constant(List[_]))): the existential is still there. The reason is that the erasure type-maps return ConstantTypes unchanged. this should probably be fixed
    • at the end of type-checking the rhs, the typer invokes adapt
    • because t1 does not conform to t2, erasure's adapt method introduces a cast to t1.
    • the new tree Literal(t2).asInstOf[t2] is type-checked and assigned type t2. at the end the adapt method of the typer sees a tree with ConstantType(Constant(t2)), so adaptConstant replaces the tree by a new Literal(Constant(t2)). So we get the existential back!
  • the same happens for the getter DefDef.

that's how we end up with a non-erased (existential) type in the backend.

if the val is not final, the namer assigns ConstantType(..).widen to the ValDef.tpt, which is a TypeRef for Class[List[_]]. This one is correctly erased by the erasure type map, so the expected when re-typing rhs during erasure is simply Class.

scala> :power

scala> val nonErasedConstantTp = ConstantType(Constant(typeOf[List[_]]))
scala> val erasedConstantTp = ConstantType(Constant(erasure.scalaErasure(typeOf[List[_]])))
scala> val valDefTptForNonFinal = nonErasedConstantTp.widen

scala> exitingErasure { erasedConstantTp <:< nonErasedConstantTp }
res15: Boolean = false // therefore, erasure inserts the cast

scala> exitingErasure { erasedConstantTp <:< erasure.scalaErasure(valDefTptForNonFinal) }
res16: Boolean = true // no cast inserted for non-final val

@lrytz lrytz force-pushed the t9702 branch 4 times, most recently from 66d5eff to e4529ca Compare March 30, 2016 18:43
This commit fixes various issues with classOf literals and Java
annotations.

  - Ensure that a Type within a ConstantType (i.e., a classOf literal)
    is erased, so `classOf[List[Int]]` becomes `classOf[List]`.
  - Ensure that no non-erased types are passed to `typeToBType` in the
    backend. This happens for Java annotations: the annotation type and
    `classOf` annotation arguments are not erased, the annotationInfos
    of a symbol are not touched in the compiler pipeline.
  - If T is an alias to a value class, ensure that `classOf[T]` erases
    to the value class by calling `dealiasWiden` in erasure.
@lrytz
Copy link
Member Author

lrytz commented Mar 30, 2016

I think I went into all the glory details of this topic now, so ping @retronym.

@retronym
Copy link
Member

What a rabbit hole! This looks thoroughly thought through and tested. I'll review this in more detail tomorrow.

@retronym
Copy link
Member

LGTM

@adriaanm Might be a contender for M4

@adriaanm
Copy link
Contributor

Agreed -- merged! Now let's ship this puppy :shipit:

@adriaanm adriaanm merged commit 96f230a into scala:2.12.x Mar 31, 2016
@lrytz lrytz deleted the t9702 branch April 4, 2016 09:42
tp
case ConstantType(ct) =>
if (ct.tag == ClazzTag) ConstantType(Constant(apply(ct.typeValue)))
else tp
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this broke Scala.js. In some cases, a classOf[Unit] becomes classOf[BoxedUnit] after erasure in 2.12.0-M4, but it did not up and until March 31, before this PR was merged (it stayed a classOf[Unit]). The "some" cases include when compiling java.lang.Void and scala.runtime.BoxedUnit in Scala.js; but does not include when compiling the hello world or the test suite. I did not try to minimize this, so far :-s

This causes the following 3 Scala.js tests to fail:

  • org.scalajs.testsuite.javalib.lang.ClassTest
  • org.scalajs.testsuite.scalalib.ArrayBuilderTest
  • org.scalajs.testsuite.scalalib.ClassTagTest

See the full log here: https://scala-ci.typesafe.com/job/scala-2.12.x-integrate-community-build/364/consoleFull (grep for [scala-js] [error] Failed tests:).

At least for ClassTest, this happens because java.lang.Void.TYPE and scala.runtime.BoxedUnit.TYPE return the class scala.runtime.BoxedUnit instead of void. And that, because, when those classes were compiled, classOf[Unit] was transformed into classOf[BoxedUnit] during erasure. The source for these files are at https://github.com/scala-js/scala-js/blob/master/javalanglib/src/main/scala/java/lang/Void.scala and https://github.com/scala-js/scala-js/blob/master/library-aux/src/main/scala/scala/runtime/BoxedUnit.scala

Any idea?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK I can reproduce it. It happens precisely for final vals whose right-hand-side is a classOf[Unit]. Compiling

object Foo {
  final val C = classOf[Unit]
}

gives, after erasure:

  object Foo extends Object {
    def <init>(): helloworld.Foo.type = {
      Foo.super.<init>();
      ()
    };
    final private[this] val C: Class(classOf[scala.runtime.BoxedUnit]) = classOf[scala.runtime.BoxedUnit];
    final <stable> <accessor> def C(): Class(classOf[scala.runtime.BoxedUnit]) = classOf[scala.runtime.BoxedUnit]
  };

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm trying to reproduce:

$ cat Test.scala
object Test {
  final val a              = classOf[Unit]
  final val b: Class[Unit] = classOf[Unit]
  val c                    = classOf[Unit]
  val d      : Class[Unit] = classOf[Unit]

  def main(args: Array[String]): Unit = {
    val bu = classOf[runtime.BoxedUnit]
    println(a)
    println(bu)
    println(a eq b)
    println(a eq c)
    println(a eq d)
    println(a eq bu)
  }
}

$ scala -version
Scala code runner version 2.12.0-M4 -- Copyright 2002-2016, LAMP/EPFL
$ scalac Test.scala
$ scala Test
void
class scala.runtime.BoxedUnit
true
true
true
false

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i do see the difference in the AST, i wonder if that's expected and what happens in the backend so that the two fields hold the same value:

$ cat Test.scala
object Test {
  final val a              = classOf[Unit]
  final val b: Class[Unit] = classOf[Unit]
}
$ scalac Test.scala -Xprint:erasure
[[syntax trees at end of                   erasure]] // Test.scala
package <empty> {
  object Test extends Object {
    def <init>(): Test.type = {
      Test.super.<init>();
      ()
    };
    final private[this] val a: Class(classOf[scala.runtime.BoxedUnit]) = classOf[scala.runtime.BoxedUnit];
    final <stable> <accessor> def a(): Class(classOf[scala.runtime.BoxedUnit]) = classOf[scala.runtime.BoxedUnit];
    final private[this] val b: Class = classOf[scala.Unit];
    final <stable> <accessor> def b(): Class = Test.this.b
  }
}

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it seems to be specific to Unit, doesn't happen for Int.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, in Scala/JVM you can only see it in the AST. Because of constant-folding for final vals coming from Scala-defined classes, the final val is not actually accessed at run-time. You can fully reproduce the bug by implementing an inherited def with the final val:

Welcome to Scala 2.12.0-M4 (Java HotSpot(TM) 64-Bit Server VM, Java 1.8.0_60).
Type in expressions for evaluation. Or try :help.

scala> trait Foo { def C: Class[_] }
defined trait Foo

scala> object Bar extends Foo { final val C = classOf[Unit] }
defined object Bar

scala> Bar.C // constant-folded, works
res0: Class[Unit] = void

scala> val foo: Foo = Bar
foo: Foo = Bar$@16cb0050

scala> foo.C // not constant-folded, does not work
res1: Class[_] = class scala.runtime.BoxedUnit

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants