diff --git a/compiler/src/dotty/tools/dotc/core/PhantomErasure.scala b/compiler/src/dotty/tools/dotc/core/PhantomErasure.scala index b75d71cdf65a..fbd10f004c49 100644 --- a/compiler/src/dotty/tools/dotc/core/PhantomErasure.scala +++ b/compiler/src/dotty/tools/dotc/core/PhantomErasure.scala @@ -9,8 +9,8 @@ import dotty.tools.dotc.core.Types.Type * * - Parameters/arguments are erased to BoxedUnit. The next step will remove the parameters * from the method definitions and calls (implemented in branch implement-phantom-types-part-2). - * - Definitions of `def`, `val`, `lazy val` and `var` returning a phantom type to return a BoxedUnit. The next step - * is to erase the fields for phantom types (implemented in branch implement-phantom-types-part-3) + * - Definitions of `def`, `val`, `lazy val` and `var` returning a phantom type to return a BoxedUnit. Where fields + * with BoxedUnit type are not memoized (see transform/Memoize.scala). * - Calls to Phantom.assume become calls to BoxedUnit. Intended to be optimized away by local optimizations. * * BoxedUnit is used as it fits perfectly and homogeneously in all locations where phantoms can be found. diff --git a/compiler/src/dotty/tools/dotc/transform/FirstTransform.scala b/compiler/src/dotty/tools/dotc/transform/FirstTransform.scala index a3cf71ef2203..db91ff26705f 100644 --- a/compiler/src/dotty/tools/dotc/transform/FirstTransform.scala +++ b/compiler/src/dotty/tools/dotc/transform/FirstTransform.scala @@ -161,6 +161,14 @@ class FirstTransform extends MiniPhaseTransform with InfoTransformer with Annota } else ddef } + override def transformValDef(vdef: tpd.ValDef)(implicit ctx: Context, info: TransformerInfo): tpd.Tree = { + if (vdef.tpt.tpe.isPhantom) { + if (vdef.symbol.is(Mutable)) ctx.error("var fields cannot have Phantom types", vdef.pos) + else if (vdef.symbol.hasAnnotation(defn.VolatileAnnot)) ctx.error("Phantom fields cannot be @volatile", vdef.pos) + } + vdef + } + override def transformStats(trees: List[Tree])(implicit ctx: Context, info: TransformerInfo): List[Tree] = ast.Trees.flatten(reorderAndComplete(trees)(ctx.withPhase(thisTransformer.next))) diff --git a/compiler/src/dotty/tools/dotc/transform/Memoize.scala b/compiler/src/dotty/tools/dotc/transform/Memoize.scala index 605f784b602f..293bac40887b 100644 --- a/compiler/src/dotty/tools/dotc/transform/Memoize.scala +++ b/compiler/src/dotty/tools/dotc/transform/Memoize.scala @@ -98,18 +98,40 @@ import Decorators._ val NoFieldNeeded = Lazy | Deferred | JavaDefined | (if (ctx.settings.YnoInline.value) EmptyFlags else Inline) + def isErasableBottomField(cls: Symbol): Boolean = { + // TODO: For Scala.js, return false if this field is in a js.Object unless it was a Phantom before erasure. + // Could time travel to detect phantom types or add an annotation before erasure. + !field.isVolatile && ((cls eq defn.NothingClass) || (cls eq defn.NullClass) || (cls eq defn.BoxedUnitClass)) + } + + def erasedBottomTree(sym: Symbol) = { + if (sym eq defn.NothingClass) Throw(Literal(Constant(null))) + else if (sym eq defn.NullClass) Literal(Constant(null)) + else { + assert(sym eq defn.BoxedUnitClass) + ref(defn.BoxedUnit_UNIT) + } + } + if (sym.is(Accessor, butNot = NoFieldNeeded)) if (sym.isGetter) { var rhs = tree.rhs.changeOwnerAfter(sym, field, thisTransform) if (isWildcardArg(rhs)) rhs = EmptyTree val fieldDef = transformFollowing(ValDef(field, adaptToField(rhs))) - val getterDef = cpy.DefDef(tree)(rhs = transformFollowingDeep(ref(field))(ctx.withOwner(sym), info)) + val rhsClass = tree.tpt.tpe.widenDealias.classSymbol + val getterRhs = + if (isErasableBottomField(rhsClass)) erasedBottomTree(rhsClass) + else transformFollowingDeep(ref(field))(ctx.withOwner(sym), info) + val getterDef = cpy.DefDef(tree)(rhs = getterRhs) Thicket(fieldDef, getterDef) } else if (sym.isSetter) { if (!sym.is(ParamAccessor)) { val Literal(Constant(())) = tree.rhs } // this is intended as an assertion field.setFlag(Mutable) // necessary for vals mixed in from Scala2 traits - val initializer = Assign(ref(field), adaptToField(ref(tree.vparamss.head.head.symbol))) - cpy.DefDef(tree)(rhs = transformFollowingDeep(initializer)(ctx.withOwner(sym), info)) + if (isErasableBottomField(tree.vparamss.head.head.tpt.tpe.classSymbol)) tree + else { + val initializer = Assign(ref(field), adaptToField(ref(tree.vparamss.head.head.symbol))) + cpy.DefDef(tree)(rhs = transformFollowingDeep(initializer)(ctx.withOwner(sym), info)) + } } else tree // curiously, some accessors from Scala2 have ' ' suffixes. They count as // neither getters nor setters diff --git a/tests/neg/phantom-var.scala b/tests/neg/phantom-var.scala new file mode 100644 index 000000000000..5a7e2db3fdb6 --- /dev/null +++ b/tests/neg/phantom-var.scala @@ -0,0 +1,8 @@ + +class Foo { + var foo = Boo.boo // error: var fields cannot have Phantom types +} + +object Boo extends Phantom { + def boo = assume +} diff --git a/tests/neg/phantom-volitile.scala b/tests/neg/phantom-volitile.scala new file mode 100644 index 000000000000..e8874c035503 --- /dev/null +++ b/tests/neg/phantom-volitile.scala @@ -0,0 +1,9 @@ + +class Foo { + @volatile var foo1 = Boo.boo // error: var fields cannot have Phantom types + @volatile val foo2 = Boo.boo // error: Phantom fields cannot be @volatile +} + +object Boo extends Phantom { + def boo = assume +} diff --git a/tests/run/nothing-lazy-val.check b/tests/run/nothing-lazy-val.check new file mode 100644 index 000000000000..d320a534c825 --- /dev/null +++ b/tests/run/nothing-lazy-val.check @@ -0,0 +1,4 @@ +0 +1 +foo +??? diff --git a/tests/run/nothing-lazy-val.scala b/tests/run/nothing-lazy-val.scala new file mode 100644 index 000000000000..3e7aada0da2e --- /dev/null +++ b/tests/run/nothing-lazy-val.scala @@ -0,0 +1,28 @@ + +object Test { + def main(args: Array[String]): Unit = { + try { + println("0") + val f = new Foo + println("1") + println(f.foo) + println("2") + println(f.foo) + } catch { + case e: NotImplementedError => println("???") + } + + // TODO: Erase + // Currently not erasing fields for lazy vals + assert(classOf[Foo].getDeclaredFields.exists(_.getName.startsWith("foo")), "Field foo erased. Optimized accidentally?") + } + + +} + +class Foo { + lazy val foo: Nothing = { + println("foo") + ??? + } +} diff --git a/tests/run/nothing-val.check b/tests/run/nothing-val.check new file mode 100644 index 000000000000..c2245043ce18 --- /dev/null +++ b/tests/run/nothing-val.check @@ -0,0 +1,3 @@ +0 +foo +??? diff --git a/tests/run/nothing-val.scala b/tests/run/nothing-val.scala new file mode 100644 index 000000000000..104be9e435b6 --- /dev/null +++ b/tests/run/nothing-val.scala @@ -0,0 +1,24 @@ + +object Test { + def main(args: Array[String]): Unit = { + try { + println("0") + val f = new Foo + println("1") + println(f.foo) + } catch { + case e: NotImplementedError => println("???") + } + + assert(!classOf[Foo].getDeclaredFields.exists(_.getName.startsWith("foo")), "field foo not erased") + } + + +} + +class Foo { + val foo: Nothing = { + println("foo") + ??? + } +} diff --git a/tests/run/nothing-var.check b/tests/run/nothing-var.check new file mode 100644 index 000000000000..c2245043ce18 --- /dev/null +++ b/tests/run/nothing-var.check @@ -0,0 +1,3 @@ +0 +foo +??? diff --git a/tests/run/nothing-var.scala b/tests/run/nothing-var.scala new file mode 100644 index 000000000000..bb31253439a1 --- /dev/null +++ b/tests/run/nothing-var.scala @@ -0,0 +1,35 @@ + +object Test { + def main(args: Array[String]): Unit = { + try { + println("0") + val f = new Foo + println("1") + f.foo + f.foo + f.foo = { + println("foo3") + ??? + } + println(f.foo) + } catch { + case e: NotImplementedError => println("???") + } + + assert(!classOf[Foo].getDeclaredFields.exists(_.getName.startsWith("foo")), "field foo not erased") + } + + +} + +class Foo { + var foo: Nothing = { + println("foo") + ??? + } + + foo = { + println("foo2") + ??? + } +} diff --git a/tests/run/null-lazy-val.check b/tests/run/null-lazy-val.check new file mode 100644 index 000000000000..5d7a4c14be1f --- /dev/null +++ b/tests/run/null-lazy-val.check @@ -0,0 +1,5 @@ +1 +foo +null +2 +null diff --git a/tests/run/null-lazy-val.scala b/tests/run/null-lazy-val.scala new file mode 100644 index 000000000000..fb7e78f730f0 --- /dev/null +++ b/tests/run/null-lazy-val.scala @@ -0,0 +1,24 @@ + +object Test { + + def main(args: Array[String]): Unit = { + val f = new Foo + println(1) + println(f.foo) + println(2) + println(f.foo) + + // TODO: Erase + // Currently not erasing fields for lazy vals + assert(f.getClass.getDeclaredFields.exists(_.getName.startsWith("foo")), "Field foo erased. Optimized accidentally?") + } + + +} + +class Foo { + lazy val foo: Null = { + println("foo") + null + } +} diff --git a/tests/run/null-val.check b/tests/run/null-val.check new file mode 100644 index 000000000000..cea45764ec0d --- /dev/null +++ b/tests/run/null-val.check @@ -0,0 +1,3 @@ +foo +null +null diff --git a/tests/run/null-val.scala b/tests/run/null-val.scala new file mode 100644 index 000000000000..aa2637ee1a42 --- /dev/null +++ b/tests/run/null-val.scala @@ -0,0 +1,16 @@ + +object Test { + def main(args: Array[String]): Unit = { + val f = new Foo + println(f.foo) + println(f.foo) + assert(!f.getClass.getDeclaredFields.exists(_.getName.startsWith("foo")), "field foo not erased") + } +} + +class Foo { + val foo: Null = { + println("foo") + null + } +} diff --git a/tests/run/null-var.check b/tests/run/null-var.check new file mode 100644 index 000000000000..efb58c1f313c --- /dev/null +++ b/tests/run/null-var.check @@ -0,0 +1,5 @@ +foo +foo2 +null +foo3 +null diff --git a/tests/run/null-var.scala b/tests/run/null-var.scala new file mode 100644 index 000000000000..56bca4b9bcde --- /dev/null +++ b/tests/run/null-var.scala @@ -0,0 +1,26 @@ + +object Test { + def main(args: Array[String]): Unit = { + val f = new Foo + println(f.foo) + f.foo + f.foo = { + println("foo3") + null + } + println(f.foo) + assert(!f.getClass.getDeclaredFields.exists(_.getName.startsWith("foo")), "field foo not erased") + } +} + +class Foo { + var foo: Null = { + println("foo") + null + } + + foo = { + println("foo2") + null + } +} diff --git a/tests/run/phantom-lazy-val-2.check b/tests/run/phantom-lazy-val-2.check new file mode 100644 index 000000000000..069e33cfba04 --- /dev/null +++ b/tests/run/phantom-lazy-val-2.check @@ -0,0 +1,3 @@ +1 +foo +2 diff --git a/tests/run/phantom-lazy-val-2.scala b/tests/run/phantom-lazy-val-2.scala new file mode 100644 index 000000000000..490c3a4113aa --- /dev/null +++ b/tests/run/phantom-lazy-val-2.scala @@ -0,0 +1,31 @@ + +object Test { + + def main(args: Array[String]): Unit = { + val f = new Foo + println(1) + f.foo + println(2) + f.foo + + // TODO: Erase + // Currently not erasing fields for lazy vals + assert(f.getClass.getDeclaredFields.exists(_.getName.startsWith("foo")), "Field foo erased. Optimized accidentally?") + } + + +} + +class Foo { + import Boo._ + + lazy val foo = { + println("foo") + any + } +} + +object Boo extends Phantom { + type BooAny = this.Any + def any: BooAny = assume +} diff --git a/tests/run/phantom-val-2.check b/tests/run/phantom-val-2.check new file mode 100644 index 000000000000..06b2967cef34 --- /dev/null +++ b/tests/run/phantom-val-2.check @@ -0,0 +1,2 @@ +foo +1 diff --git a/tests/run/phantom-val-2.scala b/tests/run/phantom-val-2.scala new file mode 100644 index 000000000000..0fa3bb2ebf42 --- /dev/null +++ b/tests/run/phantom-val-2.scala @@ -0,0 +1,23 @@ + +object Test { + def main(args: Array[String]): Unit = { + val f = new Foo + println(1) + f.foo + f.foo + assert(!f.getClass.getDeclaredFields.exists(_.getName.startsWith("foo")), "field foo not erased") + } +} + +class Foo { + import Boo._ + val foo = { + println("foo") + any + } +} + +object Boo extends Phantom { + type BooAny = this.Any + def any: BooAny = assume +} diff --git a/tests/run/phantom-var.scala b/tests/run/phantom-var.scala deleted file mode 100644 index dda20fe92bfe..000000000000 --- a/tests/run/phantom-var.scala +++ /dev/null @@ -1,30 +0,0 @@ - -/* Run this test with - * `run tests/run/xyz.scala -Xprint-diff-del -Xprint:arrayConstructors,phantomTermErasure,phantomTypeErasure,erasure` - * to see the the diffs after PhantomRefErasure, PhantomDeclErasure and Erasure. - */ - -object Test { - import Boo._ - - def main(args: Array[String]): Unit = { - foo - foo - foo = { - println("foo2") - any - } - foo - - } - - var foo = { - println("foo") - any - } -} - -object Boo extends Phantom { - type BooAny = this.Any - def any: BooAny = assume -} diff --git a/tests/run/unit-lazy-val.check b/tests/run/unit-lazy-val.check new file mode 100644 index 000000000000..069e33cfba04 --- /dev/null +++ b/tests/run/unit-lazy-val.check @@ -0,0 +1,3 @@ +1 +foo +2 diff --git a/tests/run/unit-lazy-val.scala b/tests/run/unit-lazy-val.scala new file mode 100644 index 000000000000..a93d28c81ade --- /dev/null +++ b/tests/run/unit-lazy-val.scala @@ -0,0 +1,23 @@ + +object Test { + + def main(args: Array[String]): Unit = { + val f = new Foo + println(1) + f.foo + println(2) + f.foo + + // TODO: Erase + // Currently not erasing fields for lazy vals + assert(f.getClass.getDeclaredFields.exists(_.getName.startsWith("foo")), "Field foo erased. Optimized accidentally?") + } + + +} + +class Foo { + lazy val foo: Unit = { + println("foo") + } +} diff --git a/tests/run/unit-val.check b/tests/run/unit-val.check new file mode 100644 index 000000000000..06b2967cef34 --- /dev/null +++ b/tests/run/unit-val.check @@ -0,0 +1,2 @@ +foo +1 diff --git a/tests/run/unit-val.scala b/tests/run/unit-val.scala new file mode 100644 index 000000000000..58175d659ff8 --- /dev/null +++ b/tests/run/unit-val.scala @@ -0,0 +1,16 @@ + +object Test { + def main(args: Array[String]): Unit = { + val f = new Foo + println(1) + f.foo + f.foo + assert(!f.getClass.getDeclaredFields.exists(_.getName.startsWith("foo")), "field foo not erased") + } +} + +class Foo { + val foo: Unit = { + println("foo") + } +} diff --git a/tests/run/phantom-var.check b/tests/run/unit-var.check similarity index 64% rename from tests/run/phantom-var.check rename to tests/run/unit-var.check index b210800439ff..c02f34162f78 100644 --- a/tests/run/phantom-var.check +++ b/tests/run/unit-var.check @@ -1,2 +1,3 @@ foo foo2 +foo3 diff --git a/tests/run/unit-var.scala b/tests/run/unit-var.scala new file mode 100644 index 000000000000..c024813a887f --- /dev/null +++ b/tests/run/unit-var.scala @@ -0,0 +1,23 @@ + +object Test { + def main(args: Array[String]): Unit = { + val f = new Foo + f.foo + f.foo + f.foo = { + println("foo3") + } + f.foo + assert(!f.getClass.getDeclaredFields.exists(_.getName.startsWith("foo")), "field foo not erased") + } +} + +class Foo { + var foo: Unit = { + println("foo") + } + + foo = { + println("foo2") + } +} diff --git a/tests/run/unit-volatile-var.scala b/tests/run/unit-volatile-var.scala new file mode 100644 index 000000000000..1c7a77fd6d40 --- /dev/null +++ b/tests/run/unit-volatile-var.scala @@ -0,0 +1,23 @@ + +object Test { + def main(args: Array[String]): Unit = { + val f = new Foo + f.foo + f.foo + f.foo = { + println("foo3") + } + f.foo + assert(f.getClass.getDeclaredFields.exists(_.getName.startsWith("foo")), "@volatile field foo erased") + } +} + +class Foo { + @volatile var foo: Unit = { + println("foo") + } + + foo = { + println("foo2") + } +} diff --git a/tests/run/unit-volitile-var.check b/tests/run/unit-volitile-var.check new file mode 100644 index 000000000000..c02f34162f78 --- /dev/null +++ b/tests/run/unit-volitile-var.check @@ -0,0 +1,3 @@ +foo +foo2 +foo3