Skip to content
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

Implement phantom types part 1 #2136

Merged
merged 32 commits into from
May 8, 2017

Conversation

nicolasstucki
Copy link
Contributor

@nicolasstucki nicolasstucki commented Mar 22, 2017

230 LOC of code without error messages and tests.

In this PR phantom types are defined and implemented. Only a minimal version of phantom term erasure is included.

Phantom types are in latices outside of the normal Any/Nothing type lattice.
A new phantom lattice can be defined by extending an object with scala.Phantom.
This trait defines synthetic members

  • protected final trait Any where this Any does not extends scala.Any
  • protected final abstract class Nothing extends this.Any
  • protected final def assume: this.Nothing

A phantom lattice object can expose any of the phantom members (Any, Nothing, assume)
using an alias but it is not required.

Restriction on lattices:

  • Types from different lattice can not be mixed using & or | types.
  • Type parameters must be bounded by types in a single lattice.

Phantom type erasure (minimalistic):

  • Type references Phantom.Any and Phantom.Nothing are erased to BoxedUnit
  • Phantom.assume is erased to BoxedUnit.UNIT
  • Phantom is removed from the parents of the object inheriting it.

Extra tests for Phantoms 1 and Extra tests for Phantoms 2 are tests intended to check the correct erasure of phantom arguments and fields (not in this PR). These tests also pass on this PR as the semantics and runtime result is equivalent with or without that additional erasure.

@nicolasstucki
Copy link
Contributor Author

This PR is the first part of #1408.

newScopeEntry(newFunctionNTrait(name.asTypeName))
else res
else null
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't that exactly the same logic as before? If yes, I don't see it as an improvement.

}

def isPhantomAnyClass(sym: Symbol)(implicit ctx: Context): Boolean =
sym.exists && (sym.owner eq PhantomClass) && sym.name == tpnme.Any
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you swap the name and owner tests, you can avoid the additional exists test. NoSymbol has a valid name: <none>.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same observation applies to the following methods.

}

private def isPhantomClass(sym: Symbol)(implicit ctx: Context): Boolean =
sym.isClass && (sym.owner eq defn.PhantomClass) && (sym.name == tpnme.Any || sym.name == tpnme.Nothing)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't the last part, (sym.name == tpnme.Any || sym.name == tpnme.Nothing), redundant?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably

if (tp1.symbol eq NothingClass) tp2.isValueTypeOrLambda && !isPhantom(tp2)
else if (tp1.symbol eq NullClass) isNullable(tp2) && !isPhantom(tp2)
else if (defn.isPhantomNothingClass(tp1.symbol)) tp2.isValueTypeOrLambda && (tp1.phantomTopClass == tp2.phantomTopClass)
else false
}
Copy link
Contributor

@odersky odersky Apr 5, 2017

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 return true for

scala.Nothing <: phantom.X

if X is an abstract type in phantom. So I believe you need to follow supertypes to determine isPhantom, 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 the compileStdLib 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.

@@ -199,7 +201,7 @@ trait TypeOps { this: Context => // TODO: Make standalone object.
}

/** The minimal set of classes in `cs` which derive all other classes in `cs` */
def dominators(cs: List[ClassSymbol], accu: List[ClassSymbol]): List[ClassSymbol] = (cs: @unchecked) match {
@tailrec def dominators(cs: List[ClassSymbol], accu: List[ClassSymbol]): List[ClassSymbol] = cs match {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we need the @tailrec here. @tailrec is intended to be normative. We should only use it if it would be a problem if the method was not tail recursive. I don't see this being the case here. dominators will not be called on huge lists of classes, and neither is it very performance sensitive.

// todo: retract mode between Type and Pattern?

/** Check that the are not mixed Any/Phantom.Any types in `&`, `|` and type bounds,
* this includes Phantom.Any of different universes.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

typo: the -> there

case TypeBoundsTree(lo, hi) =>
checkedTops2(lo, hi, PhantomCrossedMixedBounds(lo, hi), tree.pos)
case untpd.InfixOp(left, op, right) =>
checkedTops2(left, right, PhantomCrossedMixedBounds(left, right), tree.pos)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wrong error message. (In general I prefer if one does not immediately generate error messages in new code. It's a premature optimization. Much clearer to write the error directly as a string, then one can check whether it makes sense or not).

case _ => Set(defn.topOf(tree.typeOpt))
}
}
checkedTops(tree)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's better to reorganize type checking as follows: In TypeAssigner, insert check code in the assignType methods for AndType, OrType, TypeBounds, If, and Match. Here's an example for OrType:

def assignType(tree: untpd.OrTypeTree, left: Tree, right: Tree)(implicit ctx: Context) = {
  checkSameUniverse(left, right, "be combined in `|`)
  tree.withType(left.tpe | right.tpe)
}

where

checkSameUniverse(tree1: Tree, tree2: Tree, relationship: => String) =
  if (topClass(tree1.tpe) != topClass(tree2.tpe)) 
    ctx.error(ex"${tree1.tpe} and ${tree2.tpe} are in different universes. They cannot $relationship")

If we do this, Typer can be left completely alone.

import dotty.tools.dotc.core.Types._
import dotty.tools.dotc.transform.TreeTransforms.{MiniPhaseTransform, TransformerInfo}

class PhantomTypeErasure extends MiniPhaseTransform with InfoTransformer {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need a doc comment here that describes in detail what the phase does.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Aren't we supposed to also erase values and parameters of phantom types here? Without a leading doc comment, impossible to tell.

}

override def transformTypeApply(tree: TypeApply)(implicit ctx: Context, info: TransformerInfo): Tree =
if (defn.isPhantomAssume(tree.fun.symbol)) Literal(Constant(null)).withType(defn.ErasedPhantomType) else tree
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why null? Looks like a code smell to me.

@nicolasstucki nicolasstucki force-pushed the implement-phantom-types-part-1 branch 6 times, most recently from d4da0b1 to 069bc38 Compare April 11, 2017 19:08
@nicolasstucki nicolasstucki force-pushed the implement-phantom-types-part-1 branch 5 times, most recently from 74a643a to 8b27beb Compare April 12, 2017 12:09
@nicolasstucki
Copy link
Contributor Author

The following table shows the result of a macro benchmark done on my laptop. The time is the time taken to run the standard lib test in the test suite under vulpix. The columns contain the number of runs that finished in the given time.

sec     before phantoms           after phantoms
68           0                         0
69           0                         5
70           4                         6
71           3                         2
72           4                         1
73           3                         0
74           1                         0
75           0                         0
76           1                         1
77           0                         1
78           0                         0
79           0                         0

Surprisingly the execution times are are slightly faster after the phantom types where added. We would need to run this on a stabler machine if we would wish to have more precise numbers. But at least there is no evident time degradation.

@nicolasstucki
Copy link
Contributor Author

@odersky all requested changes where taken care of.

Additionally, the PhantomTypeErasure erasure phase was removed and integrated into Erasure as it should have been from the start.

@odersky
Copy link
Contributor

odersky commented Apr 13, 2017 via email

@nicolasstucki
Copy link
Contributor Author

I ran the benchmarks locally on my machine, not on the CI.

@nicolasstucki nicolasstucki force-pushed the implement-phantom-types-part-1 branch 3 times, most recently from 0555a33 to 5ee0a9a Compare April 20, 2017 10:46
@nicolasstucki nicolasstucki force-pushed the implement-phantom-types-part-1 branch 7 times, most recently from 36eaf5e to 12da43d Compare April 27, 2017 11:47
@odersky odersky force-pushed the implement-phantom-types-part-1 branch from ddbce6d to 40ab53a Compare May 8, 2017 15:12
@odersky
Copy link
Contributor

odersky commented May 8, 2017

@nicolasstucki I did a pass to polish. Can you review my last commit? If you are happy with it, we can get the PR in.

@nicolasstucki
Copy link
Contributor Author

Last commit LGTM :)


val explanation =
hl"""|""".stripMargin
}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All changes in this file should be reverted.

@odersky odersky merged commit a391a58 into scala:master May 8, 2017
@nicolasstucki
Copy link
Contributor Author

@odersky now it is ready to be merged.

@odersky
Copy link
Contributor

odersky commented May 8, 2017

So, let's merge this at last!

@nicolasstucki
Copy link
Contributor Author

Finally :)

@nicolasstucki nicolasstucki mentioned this pull request Jul 20, 2017
@allanrenucci allanrenucci deleted the implement-phantom-types-part-1 branch December 14, 2017 19:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants