Skip to content

Commit e8fb429

Browse files
committed
Erase arguments of phantom type
This commit removes arguments and parameter of phantom type from all applications. Arguments are evaluated before the application in case they have side effects. ``` def foo(i: Int, p: SomePhantom) = ??? foo(42, { println("hello"); getSomePhantom }) ``` becomes ``` def foo(i: Int) = ??? val x$0 = 42 val x$1 = { println("hello"); getSomePhantom } foo(x$0) ``` Note that now `def foo(i: Int)` and def `foo(i: Int, p: SomePhantom)` erase to the same signature. Tests for this commit where added in `Extra tests for Phantoms 1`, they where back ported as the semantics and runtime is not affected.
1 parent 0c11376 commit e8fb429

21 files changed

+490
-10
lines changed

compiler/src/dotty/tools/dotc/Compiler.scala

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,8 @@ class Compiler {
6969
new ShortcutImplicits, // Allow implicit functions without creating closures
7070
new CrossCastAnd, // Normalize selections involving intersection types.
7171
new Splitter), // Expand selections involving union types into conditionals
72-
List(new VCInlineMethods, // Inlines calls to value class methods
72+
List(new PhantomArgumentEval, // Extracts the evaluation of phantom arguments placing them before the call.
73+
new VCInlineMethods, // Inlines calls to value class methods
7374
new IsInstanceOfEvaluator, // Issues warnings when unreachable statements are present in match/if expressions
7475
new SeqLiterals, // Express vararg arguments as arrays
7576
new InterceptedMethods, // Special handling of `==`, `|=`, `getClass` methods

compiler/src/dotty/tools/dotc/core/PhantomErasure.scala

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,13 @@ import dotty.tools.dotc.core.Contexts.Context
55
import dotty.tools.dotc.core.Symbols.defn
66
import dotty.tools.dotc.core.Types.Type
77

8-
/** Phantom erasure erases (minimal erasure):
8+
/** Phantom erasure erases:
99
*
10-
* - Parameters/arguments are erased to ErasedPhantom. The next step will remove the parameters
11-
* from the method definitions and calls (implemented in branch implement-phantom-types-part-2).
10+
* - Parameters/arguments are erased removed from the function definition/call in `PhantomArgumentEval`.
11+
* If the evaluation of the phantom arguments may produce a side effect, these are evaluated and stored in
12+
* local `val`s and then the non phantoms are used in the Apply. Phantom `val`s are then erased to
13+
* `val ev$i: ErasedPhantom = myPhantom` intended to be optimized away by local optimizations. `myPhantom` could be
14+
* a reference to a phantom parameter, a call to Phantom assume or a call to a method that returns a phantom.
1215
* - Definitions of `def`, `val`, `lazy val` and `var` returning a phantom type to return a ErasedPhantom. Where fields
1316
* with ErasedPhantom type are not memoized (see transform/Memoize.scala).
1417
* - Calls to Phantom.assume become calls to ErasedPhantom.UNIT. Intended to be optimized away by local optimizations.
@@ -21,4 +24,7 @@ object PhantomErasure {
2124
/** Returns the default erased tree for a call to Phantom.assume */
2225
def erasedAssume(implicit ctx: Context): Tree = ref(defn.ErasedPhantom_UNIT)
2326

27+
/** Returns the default erased tree for a phantom parameter ref */
28+
def erasedParameterRef(implicit ctx: Context): Tree = ref(defn.ErasedPhantom_UNIT)
29+
2430
}

compiler/src/dotty/tools/dotc/core/Signature.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ case class Signature(paramsSig: List[TypeName], resSig: TypeName) {
8484
* to the parameter part of this signature.
8585
*/
8686
def prepend(params: List[Type], isJava: Boolean)(implicit ctx: Context) =
87-
Signature((params.map(sigName(_, isJava))) ++ paramsSig, resSig)
87+
Signature(params.collect { case p if !p.isPhantom => sigName(p, isJava) } ++ paramsSig, resSig)
8888

8989
/** A signature is under-defined if its paramsSig part contains at least one
9090
* `tpnme.Uninstantiated`. Under-defined signatures arise when taking a signature

compiler/src/dotty/tools/dotc/core/TypeErasure.scala

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -400,12 +400,15 @@ class TypeErasure(isJava: Boolean, semiEraseVCs: Boolean, isConstructor: Boolean
400400
case tp: MethodType =>
401401
def paramErasure(tpToErase: Type) =
402402
erasureFn(tp.isJava, semiEraseVCs, isConstructor, wildcardOK)(tpToErase)
403-
val formals = tp.paramInfos.mapConserve(paramErasure)
403+
val (names, formals0) =
404+
if (tp.paramInfos.forall(!_.isPhantom)) (tp.paramNames, tp.paramInfos)
405+
else tp.paramNames.iterator.zip(tp.paramInfos.iterator).filterNot(_._2.isPhantom).toList.unzip
406+
val formals = formals0.mapConserve(paramErasure)
404407
eraseResult(tp.resultType) match {
405408
case rt: MethodType =>
406-
tp.derivedLambdaType(tp.paramNames ++ rt.paramNames, formals ++ rt.paramInfos, rt.resultType)
409+
tp.derivedLambdaType(names ++ rt.paramNames, formals ++ rt.paramInfos, rt.resultType)
407410
case rt =>
408-
tp.derivedLambdaType(tp.paramNames, formals, rt)
411+
tp.derivedLambdaType(names, formals, rt)
409412
}
410413
case tp: PolyType =>
411414
this(tp.resultType)

compiler/src/dotty/tools/dotc/transform/Erasure.scala

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,8 @@ object Erasure extends TypeTestsCasts{
208208
val tree1 =
209209
if (tree.tpe isRef defn.NullClass)
210210
adaptToType(tree, underlying)
211+
else if (wasPhantom(underlying))
212+
PhantomErasure.erasedParameterRef
211213
else if (!(tree.tpe <:< tycon)) {
212214
assert(!(tree.tpe.typeSymbol.isPrimitiveValueClass))
213215
val nullTree = Literal(Constant(null))
@@ -417,6 +419,7 @@ object Erasure extends TypeTestsCasts{
417419

418420
override def typedIdent(tree: untpd.Ident, pt: Type)(implicit ctx: Context): tpd.Tree =
419421
if (tree.symbol eq defn.Phantom_assume) PhantomErasure.erasedAssume
422+
else if (tree.symbol.is(Flags.Param) && wasPhantom(tree.typeOpt)) PhantomErasure.erasedParameterRef
420423
else super.typedIdent(tree, pt)
421424

422425
override def typedThis(tree: untpd.This)(implicit ctx: Context): Tree =
@@ -474,7 +477,7 @@ object Erasure extends TypeTestsCasts{
474477
.withType(defn.ArrayOf(defn.ObjectType))
475478
args0 = bunchedArgs :: Nil
476479
}
477-
val args1 = args0.zipWithConserve(mt.paramInfos)(typedExpr)
480+
val args1 = args0.filterConserve(arg => !wasPhantom(arg.typeOpt)).zipWithConserve(mt.paramInfos)(typedExpr)
478481
untpd.cpy.Apply(tree)(fun1, args1) withType mt.resultType
479482
case _ =>
480483
throw new MatchError(i"tree $tree has unexpected type of function ${fun1.tpe.widen}, was ${fun.typeOpt.widen}")
@@ -535,6 +538,11 @@ object Erasure extends TypeTestsCasts{
535538
vparamss1 = (tpd.ValDef(bunchedParam) :: Nil) :: Nil
536539
rhs1 = untpd.Block(paramDefs, rhs1)
537540
}
541+
vparamss1 = vparamss1.mapConserve(_.filterConserve(vparam => !wasPhantom(vparam.tpe)))
542+
if (sym.is(Flags.ParamAccessor) && wasPhantom(ddef.tpt.tpe)) {
543+
sym.resetFlag(Flags.ParamAccessor)
544+
rhs1 = PhantomErasure.erasedParameterRef
545+
}
538546
val ddef1 = untpd.cpy.DefDef(ddef)(
539547
tparams = Nil,
540548
vparamss = vparamss1,
@@ -618,4 +626,7 @@ object Erasure extends TypeTestsCasts{
618626

619627
def takesBridges(sym: Symbol)(implicit ctx: Context) =
620628
sym.isClass && !sym.is(Flags.Trait | Flags.Package)
629+
630+
private def wasPhantom(tp: Type)(implicit ctx: Context): Boolean =
631+
tp.widenDealias.classSymbol eq defn.ErasedPhantomClass
621632
}
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
package dotty.tools.dotc.transform
2+
3+
import dotty.tools.dotc.ast.tpd
4+
import dotty.tools.dotc.core.Contexts._
5+
import dotty.tools.dotc.core.Flags._
6+
import dotty.tools.dotc.core.NameKinds._
7+
import dotty.tools.dotc.core.Types._
8+
import dotty.tools.dotc.transform.TreeTransforms.{MiniPhaseTransform, TransformerInfo}
9+
10+
/** This phase extracts the arguments of phantom type before the application to avoid losing any
11+
* effects in the argument tree. This trivializes the removal of parameter in the Erasure phase.
12+
*
13+
* `f(x1,...)(y1,...)...(...)` with at least one phantom argument
14+
*
15+
* -->
16+
*
17+
* `val ev$f = f` // if `f` is some expression that needs evaluation
18+
* `val ev$x1 = x1`
19+
* ...
20+
* `val ev$y1 = y1`
21+
* ...
22+
* `ev$f(ev$x1,...)(ev$y1,...)...(...)`
23+
*
24+
*/
25+
class PhantomArgumentEval extends MiniPhaseTransform {
26+
import tpd._
27+
28+
override def phaseName: String = "phantomArgumentEval"
29+
30+
/** Check what the phase achieves, to be called at any point after it is finished. */
31+
override def checkPostCondition(tree: Tree)(implicit ctx: Context): Unit = tree match {
32+
case tree: Apply =>
33+
tree.args.foreach { arg =>
34+
assert(
35+
!arg.tpe.isPhantom ||
36+
(arg.isInstanceOf[Ident] && !arg.symbol.is(Method) && !arg.symbol.is(Lazy))
37+
)
38+
}
39+
case _ =>
40+
}
41+
42+
/* Tree transform */
43+
44+
override def transformApply(tree: Apply)(implicit ctx: Context, info: TransformerInfo): Tree = tree.tpe match {
45+
case _: MethodType => tree // Do the transformation higher in the tree if needed
46+
case _ =>
47+
def mkNewBlock(newApply: Tree, synthVals: List[ValDef]) = Block(synthVals, newApply)
48+
if (!hasPhantomArgs(tree)) tree
49+
else transformApplication(tree, mkNewBlock)
50+
}
51+
52+
override def transformAssign(tree: Assign)(implicit ctx: Context, info: TransformerInfo): Tree = {
53+
if (!tree.rhs.tpe.isPhantom) super.transformAssign(tree)
54+
else {
55+
// Apply the same transformation to setters before their creation.
56+
val (synthVal, synthValRef) = mkSynthVal(tree.rhs)
57+
Block(List(synthVal), Assign(tree.lhs, synthValRef))
58+
}
59+
}
60+
61+
/* private methods */
62+
63+
/** Returns true if at least on of the arguments is a phantom.
64+
* Inner applies are also checked in case of multiple parameter list.
65+
*/
66+
private def hasPhantomArgs(tree: Apply)(implicit ctx: Context): Boolean = {
67+
tree.args.exists {
68+
case arg: Ident if !arg.symbol.is(Method) && !arg.symbol.is(Lazy) => false
69+
case arg => arg.tpe.isPhantom
70+
} || {
71+
tree.fun match {
72+
case fun: Apply => hasPhantomArgs(fun)
73+
case _ => false
74+
}
75+
}
76+
}
77+
78+
/** Collects all args (and possibly the function) as synthetic vals and replaces them in the tree by the reference to
79+
* the lifted its val.
80+
*/
81+
private def transformApplication(tree: Tree, mkTree: (Tree, List[ValDef]) => Tree)(implicit ctx: Context): Tree = tree match {
82+
case tree: Apply =>
83+
def mkNewApply(newFun: Tree, accSynthVals: List[ValDef]) = {
84+
val (synthVals, synthValRefs) = tree.args.map(mkSynthVal).unzip
85+
mkTree(cpy.Apply(tree)(newFun, synthValRefs), accSynthVals ::: synthVals)
86+
}
87+
transformApplication(tree.fun, mkNewApply)
88+
case tree: TypeApply =>
89+
def mkNewTypeApply(newFun: Tree, accSynthVals: List[ValDef]) =
90+
mkTree(cpy.TypeApply(tree)(newFun, tree.args), accSynthVals)
91+
transformApplication(tree.fun, mkNewTypeApply)
92+
case tree: Select if !tree.qualifier.isInstanceOf[New] =>
93+
val (accSynthVal, synthValRef) = mkSynthVal(tree.qualifier)
94+
mkTree(Select(synthValRef, tree.name), List(accSynthVal))
95+
case _ => mkTree(tree, Nil)
96+
}
97+
98+
/** Makes a synthetic val with the tree and creates a reference to it.
99+
* `tree` --> (`val ev$x = tree`, `ev$x`)
100+
*/
101+
private def mkSynthVal(tree: Tree)(implicit ctx: Context): (ValDef, Tree) = {
102+
val synthVal = SyntheticValDef(TempResultName.fresh(), tree)
103+
(synthVal, ref(synthVal.symbol))
104+
}
105+
}

compiler/src/dotty/tools/dotc/transform/VCInlineMethods.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ class VCInlineMethods extends MiniPhaseTransform with IdentityDenotTransformer {
4444
override def phaseName: String = "vcInlineMethods"
4545

4646
override def runsAfter: Set[Class[_ <: Phase]] =
47-
Set(classOf[ExtensionMethods], classOf[PatternMatcher])
47+
Set(classOf[ExtensionMethods], classOf[PatternMatcher], classOf[PhantomArgumentEval])
4848

4949
/** Replace a value class method call by a call to the corresponding extension method.
5050
*

compiler/test/dotty/tools/dotc/CompilationTests.scala

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,7 @@ class CompilationTests extends ParallelTesting {
154154
compileFile("../tests/neg/customArgs/overloadsOnAbstractTypes.scala", allowDoubleBindings) +
155155
compileFile("../tests/neg/customArgs/xfatalWarnings.scala", defaultOptions.and("-Xfatal-warnings")) +
156156
compileFile("../tests/neg/customArgs/phantom-overload.scala", allowDoubleBindings) +
157+
compileFile("../tests/neg/customArgs/phantom-overload-2.scala", allowDoubleBindings) +
157158
compileFile("../tests/neg/tailcall/t1672b.scala", defaultOptions) +
158159
compileFile("../tests/neg/tailcall/t3275.scala", defaultOptions) +
159160
compileFile("../tests/neg/tailcall/t6574.scala", defaultOptions) +
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
2+
class phantomOverload2 {
3+
import Boo._
4+
5+
def foo1() = ???
6+
def foo1(x: A) = ??? // error
7+
def foo1(x1: B)(x2: N) = ??? // error
8+
9+
def foo2(x1: Int, x2: A) = ???
10+
def foo2(x1: A)(x2: Int) = ??? // error
11+
def foo2(x1: N)(x2: A)(x3: Int) = ??? // error
12+
13+
def foo3(x1: Int, x2: A) = ???
14+
def foo3(x1: Int, x2: A)(x3: A) = ??? // error
15+
}
16+
17+
object Boo extends Phantom {
18+
type A <: this.Any
19+
type B <: this.Any
20+
type N = this.Nothing
21+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
2+
object PhantomInValueClass {
3+
import BooUtil._
4+
new VC("ghi").foo(boo)
5+
}
6+
7+
object BooUtil extends Phantom {
8+
9+
type Boo <: this.Any
10+
def boo: Boo = assume
11+
12+
class VC[T](val x: T) extends AnyVal {
13+
def foo(b: Boo) = println(x)
14+
}
15+
16+
}

tests/run/phantom-erased-methods.check

Whitespace-only changes.
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import dotty.runtime.ErasedPhantom
2+
3+
/* Run this test with
4+
* `run tests/run/xyz.scala -Xprint-diff-del -Xprint:arrayConstructors,phantomTermErasure,phantomTypeErasure,erasure`
5+
* to see the the diffs after PhantomRefErasure, PhantomDeclErasure and Erasure.
6+
*/
7+
8+
object Test {
9+
import Boo._
10+
11+
def main(args: Array[String]): Unit = {
12+
val foo = new Foo
13+
14+
// check that parameters have been removed from the functions
15+
// (would fail with NoSuchMethodException if not erased)
16+
foo.getClass.getDeclaredMethod("fun1")
17+
foo.getClass.getDeclaredMethod("fun2", classOf[String])
18+
19+
assert(foo.getClass.getDeclaredMethod("fun3").getReturnType == classOf[ErasedPhantom])
20+
}
21+
}
22+
23+
class Foo {
24+
import Boo._
25+
def fun1(b: BooAny): Unit = ()
26+
def fun2(b: BooAny, s: String): Unit = ()
27+
def fun3(): BooAny = boo
28+
}
29+
30+
object Boo extends Phantom {
31+
type BooAny = Boo.Any
32+
def boo: BooAny = assume
33+
}

tests/run/phantom-methods-11.check

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
x1
2+
x2
3+
x3
4+
x4
5+
x5
6+
fun
7+
y1
8+
y2
9+
y3
10+
y4
11+
y5
12+
Fun
13+
Fun2
14+
z1
15+
z2
16+
z3
17+
z4
18+
z5
19+
Fun2fun
20+
Fun2
21+
w1
22+
w2
23+
w3
24+
w4
25+
w5
26+
Fun2fun2

0 commit comments

Comments
 (0)