Skip to content

Erase arguments of phantom type #2399

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion compiler/src/dotty/tools/dotc/Compiler.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
12 changes: 9 additions & 3 deletions compiler/src/dotty/tools/dotc/core/PhantomErasure.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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)

}
2 changes: 1 addition & 1 deletion compiler/src/dotty/tools/dotc/core/Signature.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
9 changes: 6 additions & 3 deletions compiler/src/dotty/tools/dotc/core/TypeErasure.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
14 changes: 13 additions & 1 deletion compiler/src/dotty/tools/dotc/transform/Erasure.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down Expand Up @@ -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 =
Expand Down Expand Up @@ -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}")
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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
}
69 changes: 69 additions & 0 deletions compiler/src/dotty/tools/dotc/transform/PhantomArgLift.scala
Original file line number Diff line number Diff line change
@@ -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
}
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand Down
1 change: 1 addition & 0 deletions compiler/test/dotty/tools/dotc/CompilationTests.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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) +
Expand Down
21 changes: 21 additions & 0 deletions tests/neg/customArgs/phantom-overload-2.scala
Original file line number Diff line number Diff line change
@@ -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
}
16 changes: 16 additions & 0 deletions tests/pos/phantom-in-value-class.scala
Original file line number Diff line number Diff line change
@@ -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)
}

}
Empty file.
33 changes: 33 additions & 0 deletions tests/run/phantom-erased-methods.scala
Original file line number Diff line number Diff line change
@@ -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
}
26 changes: 26 additions & 0 deletions tests/run/phantom-methods-11.check
Original file line number Diff line number Diff line change
@@ -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
74 changes: 74 additions & 0 deletions tests/run/phantom-methods-11.scala
Original file line number Diff line number Diff line change
@@ -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
}
18 changes: 18 additions & 0 deletions tests/run/phantom-methods-12.check
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
x1
x2
x3
x4
x5
fun1
y1
y2
y3
y4
y5
fun2
z1
z2
z3
z4
z5
fun3
Loading