diff --git a/compiler/src/dotty/tools/dotc/Compiler.scala b/compiler/src/dotty/tools/dotc/Compiler.scala index 381fb3b7869d..84d49502dc54 100644 --- a/compiler/src/dotty/tools/dotc/Compiler.scala +++ b/compiler/src/dotty/tools/dotc/Compiler.scala @@ -69,7 +69,8 @@ class Compiler { new ShortcutImplicits, // Allow implicit functions without creating closures new CrossCastAnd, // Normalize selections involving intersection types. new Splitter), // Expand selections involving union types into conditionals - List(new VCInlineMethods, // Inlines calls to value class methods + List(new PhantomArgLift, // Extracts the evaluation of phantom arguments placing them before the call. + new VCInlineMethods, // Inlines calls to value class methods new SeqLiterals, // Express vararg arguments as arrays new InterceptedMethods, // Special handling of `==`, `|=`, `getClass` methods new Getters, // Replace non-private vals and vars with getter defs (fields are added later) diff --git a/compiler/src/dotty/tools/dotc/core/PhantomErasure.scala b/compiler/src/dotty/tools/dotc/core/PhantomErasure.scala index 9064bc5aaad9..9821c69713c0 100644 --- a/compiler/src/dotty/tools/dotc/core/PhantomErasure.scala +++ b/compiler/src/dotty/tools/dotc/core/PhantomErasure.scala @@ -5,10 +5,13 @@ import dotty.tools.dotc.core.Contexts.Context import dotty.tools.dotc.core.Symbols.defn import dotty.tools.dotc.core.Types.Type -/** Phantom erasure erases (minimal erasure): +/** Phantom erasure erases: * - * - Parameters/arguments are erased to ErasedPhantom. The next step will remove the parameters - * from the method definitions and calls (implemented in branch implement-phantom-types-part-2). + * - Parameters/arguments are removed from the function definition/call in `PhantomArgLift`. + * If the evaluation of the phantom arguments may produce a side effect, these are evaluated and stored in + * local `val`s and then the non phantoms are used in the Apply. Phantom `val`s are then erased to + * `val ev$i: ErasedPhantom = myPhantom` intended to be optimized away by local optimizations. `myPhantom` could be + * a reference to a phantom parameter, a call to Phantom assume or a call to a method that returns a phantom. * - Definitions of `def`, `val`, `lazy val` and `var` returning a phantom type to return a ErasedPhantom. Where fields * with ErasedPhantom type are not memoized (see transform/Memoize.scala). * - Calls to Phantom.assume become calls to ErasedPhantom.UNIT. Intended to be optimized away by local optimizations. @@ -21,4 +24,7 @@ object PhantomErasure { /** Returns the default erased tree for a call to Phantom.assume */ def erasedAssume(implicit ctx: Context): Tree = ref(defn.ErasedPhantom_UNIT) + /** Returns the default erased tree for a phantom parameter ref */ + def erasedParameterRef(implicit ctx: Context): Tree = ref(defn.ErasedPhantom_UNIT) + } diff --git a/compiler/src/dotty/tools/dotc/core/Signature.scala b/compiler/src/dotty/tools/dotc/core/Signature.scala index f310eefeb851..12decabbc5dc 100644 --- a/compiler/src/dotty/tools/dotc/core/Signature.scala +++ b/compiler/src/dotty/tools/dotc/core/Signature.scala @@ -84,7 +84,7 @@ case class Signature(paramsSig: List[TypeName], resSig: TypeName) { * to the parameter part of this signature. */ def prepend(params: List[Type], isJava: Boolean)(implicit ctx: Context) = - Signature((params.map(sigName(_, isJava))) ++ paramsSig, resSig) + Signature(params.collect { case p if !p.isPhantom => sigName(p, isJava) } ++ paramsSig, resSig) /** A signature is under-defined if its paramsSig part contains at least one * `tpnme.Uninstantiated`. Under-defined signatures arise when taking a signature diff --git a/compiler/src/dotty/tools/dotc/core/TypeErasure.scala b/compiler/src/dotty/tools/dotc/core/TypeErasure.scala index c87fb9857e8d..5e67e191e5cf 100644 --- a/compiler/src/dotty/tools/dotc/core/TypeErasure.scala +++ b/compiler/src/dotty/tools/dotc/core/TypeErasure.scala @@ -400,12 +400,15 @@ class TypeErasure(isJava: Boolean, semiEraseVCs: Boolean, isConstructor: Boolean case tp: MethodType => def paramErasure(tpToErase: Type) = erasureFn(tp.isJava, semiEraseVCs, isConstructor, wildcardOK)(tpToErase) - val formals = tp.paramInfos.mapConserve(paramErasure) + val (names, formals0) = + if (tp.paramInfos.exists(_.isPhantom)) tp.paramNames.zip(tp.paramInfos).filterNot(_._2.isPhantom).unzip + else (tp.paramNames, tp.paramInfos) + val formals = formals0.mapConserve(paramErasure) eraseResult(tp.resultType) match { case rt: MethodType => - tp.derivedLambdaType(tp.paramNames ++ rt.paramNames, formals ++ rt.paramInfos, rt.resultType) + tp.derivedLambdaType(names ++ rt.paramNames, formals ++ rt.paramInfos, rt.resultType) case rt => - tp.derivedLambdaType(tp.paramNames, formals, rt) + tp.derivedLambdaType(names, formals, rt) } case tp: PolyType => this(tp.resultType) diff --git a/compiler/src/dotty/tools/dotc/transform/Erasure.scala b/compiler/src/dotty/tools/dotc/transform/Erasure.scala index 0e426f5e0b98..5837b2b6904a 100644 --- a/compiler/src/dotty/tools/dotc/transform/Erasure.scala +++ b/compiler/src/dotty/tools/dotc/transform/Erasure.scala @@ -209,6 +209,8 @@ object Erasure { val tree1 = if (tree.tpe isRef defn.NullClass) adaptToType(tree, underlying) + else if (wasPhantom(underlying)) + PhantomErasure.erasedParameterRef else if (!(tree.tpe <:< tycon)) { assert(!(tree.tpe.typeSymbol.isPrimitiveValueClass)) val nullTree = Literal(Constant(null)) @@ -418,6 +420,7 @@ object Erasure { override def typedIdent(tree: untpd.Ident, pt: Type)(implicit ctx: Context): tpd.Tree = if (tree.symbol eq defn.Phantom_assume) PhantomErasure.erasedAssume + else if (tree.symbol.is(Flags.Param) && wasPhantom(tree.typeOpt)) PhantomErasure.erasedParameterRef else super.typedIdent(tree, pt) override def typedThis(tree: untpd.This)(implicit ctx: Context): Tree = @@ -475,7 +478,8 @@ object Erasure { .withType(defn.ArrayOf(defn.ObjectType)) args0 = bunchedArgs :: Nil } - val args1 = args0.zipWithConserve(mt.paramInfos)(typedExpr) + // Arguments are phantom if an only if the parameters are phantom, guaranteed by the separation of type lattices + val args1 = args0.filterConserve(arg => !wasPhantom(arg.typeOpt)).zipWithConserve(mt.paramInfos)(typedExpr) untpd.cpy.Apply(tree)(fun1, args1) withType mt.resultType case _ => throw new MatchError(i"tree $tree has unexpected type of function ${fun1.tpe.widen}, was ${fun.typeOpt.widen}") @@ -536,6 +540,11 @@ object Erasure { vparamss1 = (tpd.ValDef(bunchedParam) :: Nil) :: Nil rhs1 = untpd.Block(paramDefs, rhs1) } + vparamss1 = vparamss1.mapConserve(_.filterConserve(vparam => !wasPhantom(vparam.tpe))) + if (sym.is(Flags.ParamAccessor) && wasPhantom(ddef.tpt.tpe)) { + sym.resetFlag(Flags.ParamAccessor) + rhs1 = PhantomErasure.erasedParameterRef + } val ddef1 = untpd.cpy.DefDef(ddef)( tparams = Nil, vparamss = vparamss1, @@ -619,4 +628,7 @@ object Erasure { def takesBridges(sym: Symbol)(implicit ctx: Context) = sym.isClass && !sym.is(Flags.Trait | Flags.Package) + + private def wasPhantom(tp: Type)(implicit ctx: Context): Boolean = + tp.widenDealias.classSymbol eq defn.ErasedPhantomClass } diff --git a/compiler/src/dotty/tools/dotc/transform/PhantomArgLift.scala b/compiler/src/dotty/tools/dotc/transform/PhantomArgLift.scala new file mode 100644 index 000000000000..6b823a8ce56f --- /dev/null +++ b/compiler/src/dotty/tools/dotc/transform/PhantomArgLift.scala @@ -0,0 +1,69 @@ +package dotty.tools.dotc.transform + +import dotty.tools.dotc.ast.tpd +import dotty.tools.dotc.core.Contexts._ +import dotty.tools.dotc.core.NameKinds._ +import dotty.tools.dotc.core.Types._ +import dotty.tools.dotc.transform.TreeTransforms.{MiniPhaseTransform, TransformerInfo} +import dotty.tools.dotc.typer.EtaExpansion + +import scala.collection.mutable.ListBuffer + +/** This phase extracts the arguments of phantom type before the application to avoid losing any + * effects in the argument tree. This trivializes the removal of parameter in the Erasure phase. + * + * `f(x1,...)(y1,...)...(...)` with at least one phantom argument + * + * --> + * + * `val ev$f = f` // if `f` is some expression that needs evaluation + * `val ev$x1 = x1` + * ... + * `val ev$y1 = y1` + * ... + * `ev$f(ev$x1,...)(ev$y1,...)...(...)` + * + */ +class PhantomArgLift extends MiniPhaseTransform { + import tpd._ + + override def phaseName: String = "phantomArgLift" + + /** Check what the phase achieves, to be called at any point after it is finished. */ + override def checkPostCondition(tree: Tree)(implicit ctx: Context): Unit = tree match { + case tree: Apply => + tree.args.foreach { arg => + assert(!arg.tpe.isPhantom || isPureExpr(arg)) + } + case _ => + } + + /* Tree transform */ + + override def transformApply(tree: Apply)(implicit ctx: Context, info: TransformerInfo): Tree = tree.tpe.widen match { + case _: MethodType => tree // Do the transformation higher in the tree if needed + case _ => + if (!hasImpurePhantomArgs(tree)) tree + else { + val buffer = ListBuffer.empty[Tree] + val app = EtaExpansion.liftApp(buffer, tree) + if (buffer.isEmpty) app + else Block(buffer.result(), app) + } + } + + /* private methods */ + + /** Returns true if at least on of the arguments is an impure phantom. + * Inner applies are also checked in case of multiple parameter list. + */ + private def hasImpurePhantomArgs(tree: Apply)(implicit ctx: Context): Boolean = { + tree.args.exists(arg => arg.tpe.isPhantom && !isPureExpr(arg)) || { + tree.fun match { + case fun: Apply => hasImpurePhantomArgs(fun) + case _ => false + } + } + } + +} diff --git a/compiler/src/dotty/tools/dotc/transform/VCInlineMethods.scala b/compiler/src/dotty/tools/dotc/transform/VCInlineMethods.scala index ab24e1720646..af950a209f67 100644 --- a/compiler/src/dotty/tools/dotc/transform/VCInlineMethods.scala +++ b/compiler/src/dotty/tools/dotc/transform/VCInlineMethods.scala @@ -44,7 +44,7 @@ class VCInlineMethods extends MiniPhaseTransform with IdentityDenotTransformer { override def phaseName: String = "vcInlineMethods" override def runsAfter: Set[Class[_ <: Phase]] = - Set(classOf[ExtensionMethods], classOf[PatternMatcher]) + Set(classOf[ExtensionMethods], classOf[PatternMatcher], classOf[PhantomArgLift]) /** Replace a value class method call by a call to the corresponding extension method. * diff --git a/compiler/test/dotty/tools/dotc/CompilationTests.scala b/compiler/test/dotty/tools/dotc/CompilationTests.scala index 361089bca820..4ba729582403 100644 --- a/compiler/test/dotty/tools/dotc/CompilationTests.scala +++ b/compiler/test/dotty/tools/dotc/CompilationTests.scala @@ -165,6 +165,7 @@ class CompilationTests extends ParallelTesting { compileFile("../tests/neg/customArgs/xfatalWarnings.scala", defaultOptions.and("-Xfatal-warnings")) + compileFile("../tests/neg/customArgs/pureStatement.scala", defaultOptions.and("-Xfatal-warnings")) + compileFile("../tests/neg/customArgs/phantom-overload.scala", allowDoubleBindings) + + compileFile("../tests/neg/customArgs/phantom-overload-2.scala", allowDoubleBindings) + compileFile("../tests/neg/tailcall/t1672b.scala", defaultOptions) + compileFile("../tests/neg/tailcall/t3275.scala", defaultOptions) + compileFile("../tests/neg/tailcall/t6574.scala", defaultOptions) + diff --git a/tests/neg/customArgs/phantom-overload-2.scala b/tests/neg/customArgs/phantom-overload-2.scala new file mode 100644 index 000000000000..5f7dda4d4fb3 --- /dev/null +++ b/tests/neg/customArgs/phantom-overload-2.scala @@ -0,0 +1,21 @@ + +class phantomOverload2 { + import Boo._ + + def foo1() = ??? + def foo1(x: A) = ??? // error + def foo1(x1: B)(x2: N) = ??? // error + + def foo2(x1: Int, x2: A) = ??? + def foo2(x1: A)(x2: Int) = ??? // error + def foo2(x1: N)(x2: A)(x3: Int) = ??? // error + + def foo3(x1: Int, x2: A) = ??? + def foo3(x1: Int, x2: A)(x3: A) = ??? // error +} + +object Boo extends Phantom { + type A <: this.Any + type B <: this.Any + type N = this.Nothing +} diff --git a/tests/pos/phantom-in-value-class.scala b/tests/pos/phantom-in-value-class.scala new file mode 100644 index 000000000000..8714cc50c37c --- /dev/null +++ b/tests/pos/phantom-in-value-class.scala @@ -0,0 +1,16 @@ + +object PhantomInValueClass { + import BooUtil._ + new VC("ghi").foo(boo) +} + +object BooUtil extends Phantom { + + type Boo <: this.Any + def boo: Boo = assume + + class VC[T](val x: T) extends AnyVal { + def foo(b: Boo) = println(x) + } + +} diff --git a/tests/run/phantom-erased-methods.check b/tests/run/phantom-erased-methods.check new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/tests/run/phantom-erased-methods.scala b/tests/run/phantom-erased-methods.scala new file mode 100644 index 000000000000..23b73ab7f920 --- /dev/null +++ b/tests/run/phantom-erased-methods.scala @@ -0,0 +1,33 @@ +import dotty.runtime.ErasedPhantom + +/* 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 = { + val foo = new Foo + + // check that parameters have been removed from the functions + // (would fail with NoSuchMethodException if not erased) + foo.getClass.getDeclaredMethod("fun1") + foo.getClass.getDeclaredMethod("fun2", classOf[String]) + + assert(foo.getClass.getDeclaredMethod("fun3").getReturnType == classOf[ErasedPhantom]) + } +} + +class Foo { + import Boo._ + def fun1(b: BooAny): Unit = () + def fun2(b: BooAny, s: String): Unit = () + def fun3(): BooAny = boo +} + +object Boo extends Phantom { + type BooAny = Boo.Any + def boo: BooAny = assume +} diff --git a/tests/run/phantom-methods-11.check b/tests/run/phantom-methods-11.check new file mode 100644 index 000000000000..fcaa59464f2f --- /dev/null +++ b/tests/run/phantom-methods-11.check @@ -0,0 +1,26 @@ +x1 +x2 +x3 +x4 +x5 +fun +y1 +y2 +y3 +y4 +y5 +Fun +Fun2 +z1 +z2 +z3 +z4 +z5 +Fun2fun +Fun2 +w1 +w2 +w3 +w4 +w5 +Fun2fun2 diff --git a/tests/run/phantom-methods-11.scala b/tests/run/phantom-methods-11.scala new file mode 100644 index 000000000000..82ace83286f8 --- /dev/null +++ b/tests/run/phantom-methods-11.scala @@ -0,0 +1,74 @@ +/* Run this test with + * `run tests/run/xyz.scala -Xprint-diff-del -Xprint:arrayConstructors,phantomRefErasure,phantomErasure,erasure` + * to see the the diffs after PhantomRefErasure, PhantomDeclErasure and Erasure. + */ + +object Test { + import Boo._ + + def main(args: Array[String]): Unit = { + fun( + { println("x1"); boo }, + { println("x2"); boo } + )( + { println("x3"); boo } + )( + { println("x4"); boo }, + { println("x5"); boo } + ) + + new Fun( + { println("y1"); boo }, + { println("y2"); boo } + )( + { println("y3"); boo } + )( + { println("y4"); boo }, + { println("y5"); boo } + ) + + (new Fun2().fun)( + { println("z1"); boo }, + { println("z2"); boo } + )( + { println("z3"); boo } + )( + { println("z4"); boo }, + { println("z5"); boo } + ) + + (new Fun2().fun2)( + { println("w1"); boo }, + { println("w2"); boo } + )( + { println("w3"); boo } + )( + { println("w4"); boo }, + { println("w5"); boo } + ) + } + + def fun(x1: Inky, x2: Inky)(x3: Inky)(x4: Inky, x5: Inky) = { + println("fun") + } + + class Fun(y1: Inky, y2: Inky)(y3: Inky)(y4: Inky, y5: Inky) { + println("Fun") + } + + class Fun2 { + println("Fun2") + def fun(z1: Inky, z2: Inky)(z3: Inky)(z4: Inky, z5: Inky) = { + println("Fun2fun") + } + + def fun2[T](z1: Inky, z2: Inky)(z3: Inky)(z4: Inky, z5: Inky) = { + println("Fun2fun2") + } + } +} + +object Boo extends Phantom { + type Inky <: this.Any + def boo: Inky = assume +} diff --git a/tests/run/phantom-methods-12.check b/tests/run/phantom-methods-12.check new file mode 100644 index 000000000000..7a69fa94b7ea --- /dev/null +++ b/tests/run/phantom-methods-12.check @@ -0,0 +1,18 @@ +x1 +x2 +x3 +x4 +x5 +fun1 +y1 +y2 +y3 +y4 +y5 +fun2 +z1 +z2 +z3 +z4 +z5 +fun3 diff --git a/tests/run/phantom-methods-12.scala b/tests/run/phantom-methods-12.scala new file mode 100644 index 000000000000..d3daf50b909f --- /dev/null +++ b/tests/run/phantom-methods-12.scala @@ -0,0 +1,58 @@ +/* Run this test with + * `run tests/run/xyz.scala -Xprint-diff-del -Xprint:arrayConstructors,phantomRefErasure,phantomErasure,erasure` + * to see the the diffs after PhantomRefErasure, PhantomDeclErasure and Erasure. + */ + +object Test { + import Boo._ + + def main(args: Array[String]): Unit = { + fun1( + { println("x1"); boo }, + { println("x2"); boo } + )( + { println("x3"); boo } + )( + { println("x4"); boo }, + { println("x5"); boo } + ) + + fun2( + { println("y1"); 1 }, + { println("y2"); 2 } + )( + { println("y3"); boo } + )( + { println("y4"); boo }, + { println("y5"); boo } + ) + + fun3( + { println("z1"); boo }, + { println("z2"); boo } + )( + { println("z3"); 4 } + )( + { println("z4"); 5 }, + { println("z5"); 6 } + ) + } + + def fun1(x1: Inky, x2: Inky)(x3: Inky)(x4: Inky, x5: Inky) = { + println("fun1") + } + + def fun2(x1: Int, x2: Int)(x3: Inky)(x4: Inky, x5: Inky) = { + println("fun2") + } + + def fun3(x1: Inky, x2: Inky)(x3: Int)(x4: Int, x5: Int) = { + println("fun3") + } + +} + +object Boo extends Phantom { + type Inky <: this.Any + def boo: Inky = assume +} diff --git a/tests/run/phantom-methods-13.check b/tests/run/phantom-methods-13.check new file mode 100644 index 000000000000..7a69fa94b7ea --- /dev/null +++ b/tests/run/phantom-methods-13.check @@ -0,0 +1,18 @@ +x1 +x2 +x3 +x4 +x5 +fun1 +y1 +y2 +y3 +y4 +y5 +fun2 +z1 +z2 +z3 +z4 +z5 +fun3 diff --git a/tests/run/phantom-methods-13.scala b/tests/run/phantom-methods-13.scala new file mode 100644 index 000000000000..f9d1bab1f871 --- /dev/null +++ b/tests/run/phantom-methods-13.scala @@ -0,0 +1,58 @@ +/* Run this test with + * `run tests/run/xyz.scala -Xprint-diff-del -Xprint:arrayConstructors,phantomRefErasure,phantomErasure,erasure` + * to see the the diffs after PhantomRefErasure, PhantomDeclErasure and Erasure. + */ + +object Test { + import Boo._ + + def main(args: Array[String]): Unit = { + fun1( + { println("x1"); boo }, + { println("x2"); boo } + )( + { println("x3"); 0 } + )( + { println("x4"); boo }, + { println("x5"); boo } + ) + + fun2( + { println("y1"); 1 }, + { println("y2"); 2 } + )( + { println("y3"); boo } + )( + { println("y4"); boo }, + { println("y5"); boo } + ) + + fun3( + { println("z1"); boo }, + { println("z2"); boo } + )( + { println("z3"); 4 } + )( + { println("z4"); 5 }, + { println("z5"); 6 } + ) + } + + def fun1[T](x1: Inky, x2: Inky)(x3: T)(x4: Inky, x5: Inky) = { + println("fun1") + } + + def fun2[T](x1: T, x2: T)(x3: Inky)(x4: Inky, x5: Inky) = { + println("fun2") + } + + def fun3[T](x1: Inky, x2: Inky)(x3: T)(x4: T, x5: T) = { + println("fun3") + } + +} + +object Boo extends Phantom { + type Inky <: this.Any + def boo: Inky = assume +} diff --git a/tests/run/phantom-methods-14.check b/tests/run/phantom-methods-14.check new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/tests/run/phantom-methods-14.scala b/tests/run/phantom-methods-14.scala new file mode 100644 index 000000000000..d149ddf1efed --- /dev/null +++ b/tests/run/phantom-methods-14.scala @@ -0,0 +1,17 @@ +import scala.reflect.ClassTag + +object Test { + + def main(args: Array[String]): Unit = { + bar1(Foo.a) + bar2(Foo.a)(null) + } + + def bar1(ev: Foo.A) = () + def bar2(ev: Foo.A)(implicit c: ClassTag[Int]) = implicitly[ClassTag[Int]] +} + +object Foo extends Phantom { + type A <: this.Any + def a: A = assume +} diff --git a/tests/run/phantom-param-accessor.scala b/tests/run/phantom-param-accessor.scala new file mode 100644 index 000000000000..eab1bd13d17e --- /dev/null +++ b/tests/run/phantom-param-accessor.scala @@ -0,0 +1,14 @@ +import Boo._ + +object Test { + def main(args: Array[String]): Unit = { + new Foo(a).aVal + } +} + +class Foo(val aVal: A) + +object Boo extends Phantom { + type A = this.Nothing + def a = assume +}