Skip to content

Commit 1d0e940

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 ff88246 commit 1d0e940

22 files changed

+479
-12
lines changed

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

Lines changed: 2 additions & 1 deletion
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

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 BoxedUnit. 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: BoxedUnit = 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 BoxedUnit. The next step
1316
* is to erase the fields for phantom types (implemented in branch implement-phantom-types-part-3)
1417
* - Calls to Phantom.assume become calls to BoxedUnit. Intended to be optimized away by local optimizations.
@@ -25,4 +28,7 @@ object PhantomErasure {
2528
/** Returns the default erased tree for a call to Phantom.assume */
2629
def erasedAssume(implicit ctx: Context): Tree = ref(defn.BoxedUnit_UNIT)
2730

31+
/** Returns the default erased tree for a phantom parameter ref */
32+
def erasedParameterRef(implicit ctx: Context): Tree = ref(defn.BoxedUnit_UNIT)
33+
2834
}

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
@@ -385,12 +385,15 @@ class TypeErasure(isJava: Boolean, semiEraseVCs: Boolean, isConstructor: Boolean
385385
case tp: MethodType =>
386386
def paramErasure(tpToErase: Type) =
387387
erasureFn(tp.isJava, semiEraseVCs, isConstructor, wildcardOK)(tpToErase)
388-
val formals = tp.paramInfos.mapConserve(paramErasure)
388+
val (names, formals0) =
389+
if (tp.paramInfos.forall(!_.isPhantom)) (tp.paramNames, tp.paramInfos)
390+
else tp.paramNames.iterator.zip(tp.paramInfos.iterator).filterNot(_._2.isPhantom).toList.unzip
391+
val formals = formals0.mapConserve(paramErasure)
389392
eraseResult(tp.resultType) match {
390393
case rt: MethodType =>
391-
tp.derivedLambdaType(tp.paramNames ++ rt.paramNames, formals ++ rt.paramInfos, rt.resultType)
394+
tp.derivedLambdaType(names ++ rt.paramNames, formals ++ rt.paramInfos, rt.resultType)
392395
case rt =>
393-
tp.derivedLambdaType(tp.paramNames, formals, rt)
396+
tp.derivedLambdaType(names, formals, rt)
394397
}
395398
case tp: PolyType =>
396399
this(tp.resultType)

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

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -470,7 +470,7 @@ object Erasure extends TypeTestsCasts{
470470
.withType(defn.ArrayOf(defn.ObjectType))
471471
args0 = bunchedArgs :: Nil
472472
}
473-
val args1 = args0.zipWithConserve(mt.paramInfos)(typedExpr)
473+
val args1 = args0.filterConserve(arg => !wasPhantom(arg.typeOpt)).zipWithConserve(mt.paramInfos)(typedExpr)
474474
untpd.cpy.Apply(tree)(fun1, args1) withType mt.resultType
475475
case _ =>
476476
throw new MatchError(i"tree $tree has unexpected type of function ${fun1.tpe.widen}, was ${fun.typeOpt.widen}")
@@ -485,6 +485,10 @@ object Erasure extends TypeTestsCasts{
485485
super.typedSeqLiteral(tree, erasure(tree.typeOpt))
486486
// proto type of typed seq literal is original type;
487487

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

@@ -531,6 +535,7 @@ object Erasure extends TypeTestsCasts{
531535
vparamss1 = (tpd.ValDef(bunchedParam) :: Nil) :: Nil
532536
rhs1 = untpd.Block(paramDefs, rhs1)
533537
}
538+
vparamss1 = vparamss1.mapConserve(_.filterConserve(vparam => !wasPhantom(vparam.tpe)))
534539
val ddef1 = untpd.cpy.DefDef(ddef)(
535540
tparams = Nil,
536541
vparamss = vparamss1,
@@ -614,4 +619,6 @@ object Erasure extends TypeTestsCasts{
614619

615620
def takesBridges(sym: Symbol)(implicit ctx: Context) =
616621
sym.isClass && !sym.is(Flags.Trait | Flags.Package)
622+
623+
private def wasPhantom(tp: Type)(implicit ctx: Context): Boolean = tp.isPhantom(ctx.withPhase(ctx.erasurePhase))
617624
}
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
@@ -153,6 +153,7 @@ class CompilationTests extends ParallelTesting {
153153
compileFile("../tests/neg/customArgs/overloadsOnAbstractTypes.scala", allowDoubleBindings) +
154154
compileFile("../tests/neg/customArgs/xfatalWarnings.scala", defaultOptions.and("-Xfatal-warnings")) +
155155
compileFile("../tests/neg/customArgs/phantom-overload.scala", allowDoubleBindings) +
156+
compileFile("../tests/neg/customArgs/phantom-overload-2.scala", allowDoubleBindings) +
156157
compileFile("../tests/neg/tailcall/t1672b.scala", defaultOptions) +
157158
compileFile("../tests/neg/tailcall/t3275.scala", defaultOptions) +
158159
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 scala.runtime.BoxedUnit
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[BoxedUnit])
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

tests/run/phantom-methods-11.scala

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
/* Run this test with
2+
* `run tests/run/xyz.scala -Xprint-diff-del -Xprint:arrayConstructors,phantomRefErasure,phantomErasure,erasure`
3+
* to see the the diffs after PhantomRefErasure, PhantomDeclErasure and Erasure.
4+
*/
5+
6+
object Test {
7+
import Boo._
8+
9+
def main(args: Array[String]): Unit = {
10+
fun(
11+
{ println("x1"); boo },
12+
{ println("x2"); boo }
13+
)(
14+
{ println("x3"); boo }
15+
)(
16+
{ println("x4"); boo },
17+
{ println("x5"); boo }
18+
)
19+
20+
new Fun(
21+
{ println("y1"); boo },
22+
{ println("y2"); boo }
23+
)(
24+
{ println("y3"); boo }
25+
)(
26+
{ println("y4"); boo },
27+
{ println("y5"); boo }
28+
)
29+
30+
(new Fun2().fun)(
31+
{ println("z1"); boo },
32+
{ println("z2"); boo }
33+
)(
34+
{ println("z3"); boo }
35+
)(
36+
{ println("z4"); boo },
37+
{ println("z5"); boo }
38+
)
39+
40+
(new Fun2().fun2)(
41+
{ println("w1"); boo },
42+
{ println("w2"); boo }
43+
)(
44+
{ println("w3"); boo }
45+
)(
46+
{ println("w4"); boo },
47+
{ println("w5"); boo }
48+
)
49+
}
50+
51+
def fun(x1: Inky, x2: Inky)(x3: Inky)(x4: Inky, x5: Inky) = {
52+
println("fun")
53+
}
54+
55+
class Fun(y1: Inky, y2: Inky)(y3: Inky)(y4: Inky, y5: Inky) {
56+
println("Fun")
57+
}
58+
59+
class Fun2 {
60+
println("Fun2")
61+
def fun(z1: Inky, z2: Inky)(z3: Inky)(z4: Inky, z5: Inky) = {
62+
println("Fun2fun")
63+
}
64+
65+
def fun2[T](z1: Inky, z2: Inky)(z3: Inky)(z4: Inky, z5: Inky) = {
66+
println("Fun2fun2")
67+
}
68+
}
69+
}
70+
71+
object Boo extends Phantom {
72+
type Inky <: this.Any
73+
def boo: Inky = assume
74+
}

0 commit comments

Comments
 (0)