Skip to content

Commit d121ad5

Browse files
Merge pull request #2399 from dotty-staging/implement-phantom-types-part-2
Erase arguments of phantom type
2 parents b989415 + 975cee5 commit d121ad5

21 files changed

+455
-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 PhantomArgLift, // Extracts the evaluation of phantom arguments placing them before the call.
73+
new VCInlineMethods, // Inlines calls to value class methods
7374
new SeqLiterals, // Express vararg arguments as arrays
7475
new InterceptedMethods, // Special handling of `==`, `|=`, `getClass` methods
7576
new Getters, // Replace non-private vals and vars with getter defs (fields are added later)

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 removed from the function definition/call in `PhantomArgLift`.
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.exists(_.isPhantom)) tp.paramNames.zip(tp.paramInfos).filterNot(_._2.isPhantom).unzip
405+
else (tp.paramNames, tp.paramInfos)
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: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -209,6 +209,8 @@ object Erasure {
209209
val tree1 =
210210
if (tree.tpe isRef defn.NullClass)
211211
adaptToType(tree, underlying)
212+
else if (wasPhantom(underlying))
213+
PhantomErasure.erasedParameterRef
212214
else if (!(tree.tpe <:< tycon)) {
213215
assert(!(tree.tpe.typeSymbol.isPrimitiveValueClass))
214216
val nullTree = Literal(Constant(null))
@@ -418,6 +420,7 @@ object Erasure {
418420

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

423426
override def typedThis(tree: untpd.This)(implicit ctx: Context): Tree =
@@ -475,7 +478,8 @@ object Erasure {
475478
.withType(defn.ArrayOf(defn.ObjectType))
476479
args0 = bunchedArgs :: Nil
477480
}
478-
val args1 = args0.zipWithConserve(mt.paramInfos)(typedExpr)
481+
// Arguments are phantom if an only if the parameters are phantom, guaranteed by the separation of type lattices
482+
val args1 = args0.filterConserve(arg => !wasPhantom(arg.typeOpt)).zipWithConserve(mt.paramInfos)(typedExpr)
479483
untpd.cpy.Apply(tree)(fun1, args1) withType mt.resultType
480484
case _ =>
481485
throw new MatchError(i"tree $tree has unexpected type of function ${fun1.tpe.widen}, was ${fun.typeOpt.widen}")
@@ -536,6 +540,11 @@ object Erasure {
536540
vparamss1 = (tpd.ValDef(bunchedParam) :: Nil) :: Nil
537541
rhs1 = untpd.Block(paramDefs, rhs1)
538542
}
543+
vparamss1 = vparamss1.mapConserve(_.filterConserve(vparam => !wasPhantom(vparam.tpe)))
544+
if (sym.is(Flags.ParamAccessor) && wasPhantom(ddef.tpt.tpe)) {
545+
sym.resetFlag(Flags.ParamAccessor)
546+
rhs1 = PhantomErasure.erasedParameterRef
547+
}
539548
val ddef1 = untpd.cpy.DefDef(ddef)(
540549
tparams = Nil,
541550
vparamss = vparamss1,
@@ -619,4 +628,7 @@ object Erasure {
619628

620629
def takesBridges(sym: Symbol)(implicit ctx: Context) =
621630
sym.isClass && !sym.is(Flags.Trait | Flags.Package)
631+
632+
private def wasPhantom(tp: Type)(implicit ctx: Context): Boolean =
633+
tp.widenDealias.classSymbol eq defn.ErasedPhantomClass
622634
}
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
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.NameKinds._
6+
import dotty.tools.dotc.core.Types._
7+
import dotty.tools.dotc.transform.TreeTransforms.{MiniPhaseTransform, TransformerInfo}
8+
import dotty.tools.dotc.typer.EtaExpansion
9+
10+
import scala.collection.mutable.ListBuffer
11+
12+
/** This phase extracts the arguments of phantom type before the application to avoid losing any
13+
* effects in the argument tree. This trivializes the removal of parameter in the Erasure phase.
14+
*
15+
* `f(x1,...)(y1,...)...(...)` with at least one phantom argument
16+
*
17+
* -->
18+
*
19+
* `val ev$f = f` // if `f` is some expression that needs evaluation
20+
* `val ev$x1 = x1`
21+
* ...
22+
* `val ev$y1 = y1`
23+
* ...
24+
* `ev$f(ev$x1,...)(ev$y1,...)...(...)`
25+
*
26+
*/
27+
class PhantomArgLift extends MiniPhaseTransform {
28+
import tpd._
29+
30+
override def phaseName: String = "phantomArgLift"
31+
32+
/** Check what the phase achieves, to be called at any point after it is finished. */
33+
override def checkPostCondition(tree: Tree)(implicit ctx: Context): Unit = tree match {
34+
case tree: Apply =>
35+
tree.args.foreach { arg =>
36+
assert(!arg.tpe.isPhantom || isPureExpr(arg))
37+
}
38+
case _ =>
39+
}
40+
41+
/* Tree transform */
42+
43+
override def transformApply(tree: Apply)(implicit ctx: Context, info: TransformerInfo): Tree = tree.tpe.widen match {
44+
case _: MethodType => tree // Do the transformation higher in the tree if needed
45+
case _ =>
46+
if (!hasImpurePhantomArgs(tree)) tree
47+
else {
48+
val buffer = ListBuffer.empty[Tree]
49+
val app = EtaExpansion.liftApp(buffer, tree)
50+
if (buffer.isEmpty) app
51+
else Block(buffer.result(), app)
52+
}
53+
}
54+
55+
/* private methods */
56+
57+
/** Returns true if at least on of the arguments is an impure phantom.
58+
* Inner applies are also checked in case of multiple parameter list.
59+
*/
60+
private def hasImpurePhantomArgs(tree: Apply)(implicit ctx: Context): Boolean = {
61+
tree.args.exists(arg => arg.tpe.isPhantom && !isPureExpr(arg)) || {
62+
tree.fun match {
63+
case fun: Apply => hasImpurePhantomArgs(fun)
64+
case _ => false
65+
}
66+
}
67+
}
68+
69+
}

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[PhantomArgLift])
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
@@ -165,6 +165,7 @@ class CompilationTests extends ParallelTesting {
165165
compileFile("../tests/neg/customArgs/xfatalWarnings.scala", defaultOptions.and("-Xfatal-warnings")) +
166166
compileFile("../tests/neg/customArgs/pureStatement.scala", defaultOptions.and("-Xfatal-warnings")) +
167167
compileFile("../tests/neg/customArgs/phantom-overload.scala", allowDoubleBindings) +
168+
compileFile("../tests/neg/customArgs/phantom-overload-2.scala", allowDoubleBindings) +
168169
compileFile("../tests/neg/tailcall/t1672b.scala", defaultOptions) +
169170
compileFile("../tests/neg/tailcall/t3275.scala", defaultOptions) +
170171
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

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

tests/run/phantom-methods-12.check

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
x1
2+
x2
3+
x3
4+
x4
5+
x5
6+
fun1
7+
y1
8+
y2
9+
y3
10+
y4
11+
y5
12+
fun2
13+
z1
14+
z2
15+
z3
16+
z4
17+
z5
18+
fun3

0 commit comments

Comments
 (0)