Skip to content

Commit de3a1c8

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 e639043 commit de3a1c8

21 files changed

+491
-10
lines changed

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

+2-1
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,8 @@ class Compiler {
6666
new ShortcutImplicits, // Allow implicit functions without creating closures
6767
new CrossCastAnd, // Normalize selections involving intersection types.
6868
new Splitter), // Expand selections involving union types into conditionals
69-
List(new VCInlineMethods, // Inlines calls to value class methods
69+
List(new PhantomArgumentEval, // Extracts the evaluation of phantom arguments placing them before the call.
70+
new VCInlineMethods, // Inlines calls to value class methods
7071
new IsInstanceOfEvaluator, // Issues warnings when unreachable statements are present in match/if expressions
7172
new SeqLiterals, // Express vararg arguments as arrays
7273
new InterceptedMethods, // Special handling of `==`, `|=`, `getClass` methods

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

+9-3
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

+1-1
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

+6-3
Original file line numberDiff line numberDiff line change
@@ -386,12 +386,15 @@ class TypeErasure(isJava: Boolean, semiEraseVCs: Boolean, isConstructor: Boolean
386386
case tp: MethodType =>
387387
def paramErasure(tpToErase: Type) =
388388
erasureFn(tp.isJava, semiEraseVCs, isConstructor, wildcardOK)(tpToErase)
389-
val formals = tp.paramInfos.mapConserve(paramErasure)
389+
val (names, formals0) =
390+
if (tp.paramInfos.forall(!_.isPhantom)) (tp.paramNames, tp.paramInfos)
391+
else tp.paramNames.iterator.zip(tp.paramInfos.iterator).filterNot(_._2.isPhantom).toList.unzip
392+
val formals = formals0.mapConserve(paramErasure)
390393
eraseResult(tp.resultType) match {
391394
case rt: MethodType =>
392-
tp.derivedLambdaType(tp.paramNames ++ rt.paramNames, formals ++ rt.paramInfos, rt.resultType)
395+
tp.derivedLambdaType(names ++ rt.paramNames, formals ++ rt.paramInfos, rt.resultType)
393396
case rt =>
394-
tp.derivedLambdaType(tp.paramNames, formals, rt)
397+
tp.derivedLambdaType(names, formals, rt)
395398
}
396399
case tp: PolyType =>
397400
this(tp.resultType)

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

+13-1
Original file line numberDiff line numberDiff line change
@@ -468,7 +468,7 @@ object Erasure extends TypeTestsCasts{
468468
.withType(defn.ArrayOf(defn.ObjectType))
469469
args0 = bunchedArgs :: Nil
470470
}
471-
val args1 = args0.zipWithConserve(mt.paramInfos)(typedExpr)
471+
val args1 = args0.filterConserve(arg => !wasPhantom(arg.typeOpt)).zipWithConserve(mt.paramInfos)(typedExpr)
472472
untpd.cpy.Apply(tree)(fun1, args1) withType mt.resultType
473473
case _ =>
474474
throw new MatchError(i"tree $tree has unexpected type of function ${fun1.tpe.widen}, was ${fun.typeOpt.widen}")
@@ -483,6 +483,10 @@ object Erasure extends TypeTestsCasts{
483483
super.typedSeqLiteral(tree, erasure(tree.typeOpt))
484484
// proto type of typed seq literal is original type;
485485

486+
override def typedIdent(tree: untpd.Ident, pt: Type)(implicit ctx: Context) =
487+
if (tree.symbol.is(Flags.Param) && wasPhantom(tree.typeOpt)) PhantomErasure.erasedParameterRef
488+
else super.typedIdent(tree, pt)
489+
486490
override def typedIf(tree: untpd.If, pt: Type)(implicit ctx: Context) =
487491
super.typedIf(tree, adaptProto(tree, pt))
488492

@@ -529,6 +533,11 @@ object Erasure extends TypeTestsCasts{
529533
vparamss1 = (tpd.ValDef(bunchedParam) :: Nil) :: Nil
530534
rhs1 = untpd.Block(paramDefs, rhs1)
531535
}
536+
vparamss1 = vparamss1.mapConserve(_.filterConserve(vparam => !wasPhantom(vparam.tpe)))
537+
if (sym.is(Flags.ParamAccessor) && wasPhantom(ddef.tpt.tpe)) {
538+
sym.resetFlag(Flags.ParamAccessor)
539+
rhs1 = PhantomErasure.erasedParameterRef
540+
}
532541
val ddef1 = untpd.cpy.DefDef(ddef)(
533542
tparams = Nil,
534543
vparamss = vparamss1,
@@ -612,4 +621,7 @@ object Erasure extends TypeTestsCasts{
612621

613622
def takesBridges(sym: Symbol)(implicit ctx: Context) =
614623
sym.isClass && !sym.is(Flags.Trait | Flags.Package)
624+
625+
private def wasPhantom(tp: Type)(implicit ctx: Context): Boolean =
626+
tp.widenDealias.classSymbol eq defn.ErasedPhantomClass
615627
}
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

+1-1
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

+1
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) +
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+
}
+16
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.
+33
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

+26
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)