-
Notifications
You must be signed in to change notification settings - Fork 1.1k
Implement phantom types part 1 #2136
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
Changes from 31 commits
f067b8e
ac57124
84931e3
a23ef92
7ccdbf3
01063d3
5fb733c
e8590f6
621a67b
ee44731
0f4f863
efae3a3
510c0cb
f782288
341cdf1
a50328f
9502cb2
6f03972
621610f
ef62884
49e88fe
e062df3
3ff903e
f0383fe
91558c3
ec13270
62b7732
4202b98
c6fe778
07c02c8
40ab53a
a21404a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
package dotty.tools.dotc.core | ||
|
||
import dotty.tools.dotc.ast.tpd._ | ||
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): | ||
* | ||
* - Parameters/arguments are erased to BoxedUnit. The next step will remove the parameters | ||
* from the method definitions and calls (implemented in branch implement-phantom-types-part-2). | ||
* - Definitions of `def`, `val`, `lazy val` and `var` returning a phantom type to return a BoxedUnit. The next step | ||
* is to erase the fields for phantom types (implemented in branch implement-phantom-types-part-3) | ||
* - Calls to Phantom.assume become calls to BoxedUnit. Intended to be optimized away by local optimizations. | ||
* | ||
* BoxedUnit is used as it fits perfectly and homogeneously in all locations where phantoms can be found. | ||
* Additionally some of the optimizations that can be performed on phantom types can also be directly implemented | ||
* on all boxed units. | ||
*/ | ||
object PhantomErasure { | ||
|
||
/** Returns the default erased type of a phantom type */ | ||
def erasedPhantomType(implicit ctx: Context): Type = defn.BoxedUnitType | ||
|
||
/** Returns the default erased tree for a call to Phantom.assume */ | ||
def erasedAssume(implicit ctx: Context): Tree = ref(defn.BoxedUnit_UNIT) | ||
|
||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -364,6 +364,7 @@ class TypeErasure(isJava: Boolean, semiEraseVCs: Boolean, isConstructor: Boolean | |
else if (semiEraseVCs && isDerivedValueClass(sym)) eraseDerivedValueClassRef(tp) | ||
else if (sym == defn.ArrayClass) apply(tp.appliedTo(TypeBounds.empty)) // i966 shows that we can hit a raw Array type. | ||
else if (defn.isSyntheticFunctionClass(sym)) defn.erasedFunctionType(sym) | ||
else if (defn.isPhantomTerminalClass(tp.symbol)) PhantomErasure.erasedPhantomType | ||
else eraseNormalClassRef(tp) | ||
case tp: RefinedType => | ||
val parent = tp.parent | ||
|
@@ -403,7 +404,8 @@ class TypeErasure(isJava: Boolean, semiEraseVCs: Boolean, isConstructor: Boolean | |
case tr :: trs1 => | ||
assert(!tr.classSymbol.is(Trait), cls) | ||
val tr1 = if (cls is Trait) defn.ObjectType else tr | ||
tr1 :: trs1.filterNot(_ isRef defn.ObjectClass) | ||
// We remove the Phantom trait to erase the definitions of Phantom.{assume, Any, Nothing} | ||
tr1 :: trs1.filterNot(x => x.isRef(defn.ObjectClass) || x.isRef(defn.PhantomClass)) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If phantomclasses are erased, why do we need this? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This clearly needs a comment. It is erasing the phantom lattice (i.e. removing the |
||
case nil => nil | ||
} | ||
val erasedDecls = decls.filteredScope(sym => !sym.isType || sym.isClass) | ||
|
@@ -506,6 +508,8 @@ class TypeErasure(isJava: Boolean, semiEraseVCs: Boolean, isConstructor: Boolean | |
} | ||
if (defn.isSyntheticFunctionClass(sym)) | ||
sigName(defn.erasedFunctionType(sym)) | ||
else if (defn.isPhantomTerminalClass(tp.symbol)) | ||
sigName(PhantomErasure.erasedPhantomType) | ||
else | ||
normalizeClass(sym.asClass).fullName.asTypeName | ||
case defn.ArrayOf(elem) => | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -172,7 +172,44 @@ object Types { | |
case _ => | ||
false | ||
} | ||
cls == defn.AnyClass || loop(this) | ||
loop(this) | ||
} | ||
|
||
/** Returns true if the type is a phantom type | ||
* - true if XYZ extends scala.Phantom and this type is upper bounded XYZ.Any | ||
* - false otherwise | ||
*/ | ||
final def isPhantom(implicit ctx: Context): Boolean = phantomLatticeType.exists | ||
|
||
/** Returns the top type of the lattice | ||
* - XYX.Any if XYZ extends scala.Phantom and this type is upper bounded XYZ.Any | ||
* - scala.Any otherwise | ||
*/ | ||
final def topType(implicit ctx: Context): Type = { | ||
val lattice = phantomLatticeType | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think this is too roundabout. You compute There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In fact |
||
if (lattice.exists) lattice.select(tpnme.Any) | ||
else defn.AnyType | ||
} | ||
|
||
/** Returns the bottom type of the lattice | ||
* - XYZ.Nothing if XYZ extends scala.Phantom and this type is upper bounded XYZ.Any | ||
* - scala.Nothing otherwise | ||
*/ | ||
final def bottomType(implicit ctx: Context): Type = { | ||
val lattice = phantomLatticeType | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Could be: There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It could. The advantage of this version is that computing a bottom type that would result in |
||
if (lattice.exists) lattice.select(tpnme.Nothing) | ||
else defn.NothingType | ||
} | ||
|
||
/** Returns the type of the phantom lattice (i.e. the prefix of the phantom type) | ||
* - XYZ if XYZ extends scala.Phantom and this type is upper bounded XYZ.Any | ||
* - NoType otherwise | ||
*/ | ||
private final def phantomLatticeType(implicit ctx: Context): Type = widen match { | ||
case tp: ClassInfo if defn.isPhantomTerminalClass(tp.classSymbol) => tp.prefix | ||
case tp: TypeProxy if tp.superType ne this => tp.underlying.phantomLatticeType | ||
case tp: AndOrType => tp.tp1.phantomLatticeType | ||
case _ => NoType | ||
} | ||
|
||
/** Is this type guaranteed not to have `null` as a value? | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -135,7 +135,7 @@ object messages { | |
} | ||
|
||
case class EmptyCatchBlock(tryBody: untpd.Tree)(implicit ctx: Context) | ||
extends EmptyCatchOrFinallyBlock(tryBody, EmptyCatchBlockID) { | ||
extends EmptyCatchOrFinallyBlock(tryBody, EmptyCatchBlockID) { | ||
val kind = "Syntax" | ||
val msg = | ||
hl"""|The ${"catch"} block does not contain a valid expression, try | ||
|
@@ -1252,5 +1252,85 @@ object messages { | |
| ${"import"} scala.{ $name => ${name.show + "Tick"} } | ||
|""" | ||
} | ||
|
||
case class ErasedPhantomsSignatureCollision(decls: Iterable[Symbol], erased: (Name, Type))(implicit ctx: Context) | ||
extends Message(ErasedPhantomsSignatureCollisionID) { | ||
val kind = "Phantom restriction" | ||
val msg = em"After phantom erasure methods $methodsString will have the same signature: ${erased._1}${erased._2}" | ||
|
||
private def methodsString = decls.map(decl => em"${decl.name}${decl.info}").mkString(", ") | ||
|
||
val explanation = | ||
hl"""|Phantom erasure removes all phantom parameters/arguments from methods and functions. | ||
|""".stripMargin | ||
} | ||
|
||
case class PhantomInheritance(cdef: tpd.TypeDef)(implicit ctx: Context) | ||
extends Message(PhantomInheritanceID) { | ||
val kind = "Phantom restriction" | ||
val msg = perfix + " cannot extend both Any and PhantomAny." | ||
|
||
def perfix = | ||
if (cdef.symbol.flags.is(Flags.Trait)) "A trait" | ||
else if (cdef.symbol.flags.is(Flags.Abstract)) "An abstract class" | ||
else "A class" | ||
|
||
val explanation = | ||
hl"""|""".stripMargin | ||
} | ||
|
||
case class PhantomMixedBounds(op: untpd.Ident)(implicit ctx: Context) | ||
extends Message(PhantomMixedBoundsID) { | ||
val kind = "Phantom restriction" | ||
val msg = hl"Can not mix types of ${"Any"} and ${"Phantom.Any"} with ${op.show}." | ||
|
||
val explanation = | ||
hl"""|""".stripMargin | ||
} | ||
|
||
case class PhantomCrossedMixedBounds(lo: untpd.Tree, hi: untpd.Tree)(implicit ctx: Context) | ||
extends Message(PhantomCrossedMixedBoundsID) { | ||
val kind = "Phantom restriction" | ||
val msg = hl"Type can not be bounded at the same time by types in the ${"Any"} and ${"Phantom.Any"} latices." | ||
|
||
val explanation = | ||
hl"""|""".stripMargin | ||
} | ||
|
||
case class MatchPhantom(tpdCase: tpd.CaseDef, expected: Type)(implicit ctx: Context) extends Message(MatchPhantomID) { | ||
val kind = "Phantom restriction" | ||
val msg = s"Pattern expected case to return a ${expected.show} but was ${tpdCase.tpe.show}" | ||
|
||
val explanation = | ||
hl"""|""".stripMargin | ||
} | ||
|
||
|
||
case class MatchOnPhantom()(implicit ctx: Context) extends Message(MatchOnPhantomID) { | ||
val kind = "Phantom restriction" | ||
val msg = "Cannot pattern match on phantoms" | ||
|
||
val explanation = | ||
hl"""|""".stripMargin | ||
} | ||
|
||
case class IfElsePhantom(thenp: tpd.Tree, elsep: tpd.Tree)(implicit ctx: Context) extends Message(IfElsePhantomID) { | ||
val kind = "Phantom restriction" | ||
val msg = | ||
s"""if/else cannot have branches with types in different lattices: | ||
| ${thenp.tpe.show} of lattice ${thenp.tpe.topType.show} | ||
| ${elsep.tpe.show} of lattice ${elsep.tpe.topType.show} | ||
""".stripMargin | ||
|
||
val explanation = | ||
hl"""|""".stripMargin | ||
} | ||
|
||
case class PhantomIsInObject()(implicit ctx: Context) extends Message(PhantomIsInObjectID) { | ||
val kind = "Phantom restriction" | ||
val msg = s"Only ${"object"} can extend ${"scala.Phantom"}" | ||
|
||
val explanation = | ||
hl"""|""".stripMargin | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. All changes in this file should be reverted. |
||
} |
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@i think
isPhantom
is not strong enough. Concretely, the test would returntrue
forif
X
is an abstract type inphantom
. So I believe you need to follow supertypes to determineisPhantom
, e.g.isPhantomClass(tp2.phantomTopClass)
.However, we should carefully benchmark the runtime cost of this. Type comparison takes the lion's share of typechecking time. We should get some data, like: how much time takes the
dotty
test or thecompileStdLib
test, or the whole junit test suite? If we see a slowdown, we can try to mitigate by either finding a faster way to test or caching the result of the test in a type.