Skip to content

Fix #6341: Revise purity checking #6377

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

Merged
merged 5 commits into from
Apr 29, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 54 additions & 30 deletions compiler/src/dotty/tools/dotc/ast/TreeInfo.scala
Original file line number Diff line number Diff line change
Expand Up @@ -366,31 +366,27 @@ trait TypedTreeInfo extends TreeInfo[Type] { self: Trees.Instance[Type] =>
// But if we do that the repl/vars test break. Need to figure out why that's the case.
}

/** The purity level of this expression.
* @return SimplyPure if expression has no side effects and cannot contain local definitions
* Pure if expression has no side effects
* Idempotent if running the expression a second time has no side effects
* Impure otherwise
/** The purity level of this expression. See docs for PurityLevel for what that means
*
* Note that purity and idempotency are different. References to modules and lazy
* vals are impure (side-effecting) both because side-effecting code may be executed and because the first reference
* Note that purity and idempotency are treated differently.
* References to modules and lazy vals are impure (side-effecting) both because
* side-effecting code may be executed and because the first reference
* takes a different code path than all to follow; but they are idempotent
* because running the expression a second time gives the cached result.
*/
def exprPurity(tree: Tree)(implicit ctx: Context): PurityLevel = unsplice(tree) match {
case EmptyTree
| This(_)
| Super(_, _)
| Literal(_)
| Closure(_, _, _) =>
SimplyPure
| Literal(_) =>
PurePath
case Ident(_) =>
refPurity(tree)
case Select(qual, _) =>
if (tree.symbol.is(Erased)) Pure
else refPurity(tree).min(exprPurity(qual))
case New(_) =>
SimplyPure
else refPurity(tree) `min` exprPurity(qual)
case New(_) | Closure(_, _, _) =>
Pure
case TypeApply(fn, _) =>
if (fn.symbol.is(Erased)) Pure else exprPurity(fn)
case Apply(fn, args) =>
Expand All @@ -416,37 +412,49 @@ trait TypedTreeInfo extends TreeInfo[Type] { self: Trees.Instance[Type] =>
Impure
}

private def minOf(l0: PurityLevel, ls: List[PurityLevel]) = (l0 /: ls)(_ min _)
private def minOf(l0: PurityLevel, ls: List[PurityLevel]) = (l0 /: ls)(_ `min` _)

def isPurePath(tree: Tree)(implicit ctx: Context): Boolean = tree.tpe match {
case tpe: ConstantType => exprPurity(tree) >= Pure
case _ => exprPurity(tree) == PurePath
}

def isPureExpr(tree: Tree)(implicit ctx: Context): Boolean =
exprPurity(tree) >= Pure

def isIdempotentPath(tree: Tree)(implicit ctx: Context): Boolean = tree.tpe match {
case tpe: ConstantType => exprPurity(tree) >= Idempotent
case _ => exprPurity(tree) >= IdempotentPath
}

def isSimplyPure(tree: Tree)(implicit ctx: Context): Boolean = exprPurity(tree) == SimplyPure
def isPureExpr(tree: Tree)(implicit ctx: Context): Boolean = exprPurity(tree) >= Pure
def isIdempotentExpr(tree: Tree)(implicit ctx: Context): Boolean = exprPurity(tree) >= Idempotent
def isIdempotentExpr(tree: Tree)(implicit ctx: Context): Boolean =
exprPurity(tree) >= Idempotent

def isPureBinding(tree: Tree)(implicit ctx: Context): Boolean = statPurity(tree) >= Pure

/** The purity level of this reference.
* @return
* SimplyPure if reference is (nonlazy and stable) or to a parameterized function
* Idempotent if reference is lazy and stable
* Impure otherwise
* PurePath if reference is (nonlazy and stable) or to a parameterized function
* IdempotentPath if reference is lazy and stable
* Impure otherwise
* @DarkDimius: need to make sure that lazy accessor methods have Lazy and Stable
* flags set.
*/
def refPurity(tree: Tree)(implicit ctx: Context): PurityLevel = {
val sym = tree.symbol
if (!tree.hasType) Impure
else if (!tree.tpe.widen.isParameterless || sym.isEffectivelyErased) SimplyPure
else if (!tree.tpe.widen.isParameterless || sym.isEffectivelyErased) PurePath
else if (!sym.isStableMember) Impure
else if (sym.is(Module))
if (sym.moduleClass.isNoInitsClass) Pure else Idempotent
else if (sym.is(Lazy)) Idempotent
else SimplyPure
if (sym.moduleClass.isNoInitsClass) PurePath else IdempotentPath
else if (sym.is(Lazy)) IdempotentPath
else PurePath
}

def isPureRef(tree: Tree)(implicit ctx: Context): Boolean =
refPurity(tree) == SimplyPure
refPurity(tree) == PurePath
def isIdempotentRef(tree: Tree)(implicit ctx: Context): Boolean =
refPurity(tree) >= Idempotent
refPurity(tree) >= IdempotentPath

/** (1) If `tree` is a constant expression, its value as a Literal,
* or `tree` itself otherwise.
Expand Down Expand Up @@ -840,13 +848,29 @@ trait TypedTreeInfo extends TreeInfo[Type] { self: Trees.Instance[Type] =>
}

object TreeInfo {
/** A purity level is represented as a bitset (expressed as an Int) */
class PurityLevel(val x: Int) extends AnyVal {
def >= (that: PurityLevel): Boolean = x >= that.x
def min(that: PurityLevel): PurityLevel = new PurityLevel(x min that.x)
/** `this` contains the bits of `that` */
def >= (that: PurityLevel): Boolean = (x & that.x) == that.x
Copy link
Contributor

Choose a reason for hiding this comment

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

This change in the semantics of x deserves some documentation.


/** The intersection of the bits of `this` and `that` */
def min(that: PurityLevel): PurityLevel = new PurityLevel(x & that.x)
}

val SimplyPure: PurityLevel = new PurityLevel(3)
val Pure: PurityLevel = new PurityLevel(2)
/** An expression is a stable path. Requires that expression is at least idempotent */
val Path: PurityLevel = new PurityLevel(4)

/** The expression has no side effects */
val Pure: PurityLevel = new PurityLevel(3)

/** Running the expression a second time has no side effects. Implied by `Pure`. */
val Idempotent: PurityLevel = new PurityLevel(1)

val Impure: PurityLevel = new PurityLevel(0)

/** A stable path that is evaluated without side effects */
val PurePath: PurityLevel = new PurityLevel(Pure.x | Path.x)

/** A stable path that is also idempotent */
val IdempotentPath: PurityLevel = new PurityLevel(Idempotent.x | Path.x)
}
10 changes: 4 additions & 6 deletions compiler/src/dotty/tools/dotc/typer/EtaExpansion.scala
Original file line number Diff line number Diff line change
Expand Up @@ -121,12 +121,10 @@ abstract class Lifter {
* val x0 = pre
* x0.f(...)
*
* unless `pre` is a `New` or `pre` is idempotent.
* unless `pre` is idempotent.
*/
def liftPrefix(defs: mutable.ListBuffer[Tree], tree: Tree)(implicit ctx: Context): Tree = tree match {
case New(_) => tree
case _ => if (isIdempotentExpr(tree)) tree else lift(defs, tree)
}
def liftPrefix(defs: mutable.ListBuffer[Tree], tree: Tree)(implicit ctx: Context): Tree =
if (isIdempotentExpr(tree)) tree else lift(defs, tree)
}

/** No lifting at all */
Expand All @@ -142,7 +140,7 @@ object LiftImpure extends LiftImpure

/** Lift all impure or complex arguments */
class LiftComplex extends Lifter {
def noLift(expr: tpd.Tree)(implicit ctx: Context): Boolean = tpd.isSimplyPure(expr)
def noLift(expr: tpd.Tree)(implicit ctx: Context): Boolean = tpd.isPurePath(expr)
override def exprLifter: Lifter = LiftToDefs
}
object LiftComplex extends LiftComplex
Expand Down
2 changes: 1 addition & 1 deletion compiler/src/dotty/tools/dotc/typer/Inliner.scala
Original file line number Diff line number Diff line change
Expand Up @@ -285,7 +285,7 @@ class Inliner(call: tpd.Tree, rhsToInline: tpd.Tree)(implicit ctx: Context) {
(tp.paramNames, tp.paramInfos, argss.head).zipped.foreach { (name, paramtp, arg) =>
paramSpan(name) = arg.span
paramBinding(name) = arg.tpe.dealias match {
case _: SingletonType if isIdempotentExpr(arg) => arg.tpe
case _: SingletonType if isIdempotentPath(arg) => arg.tpe
case _ => paramBindingDef(name, paramtp, arg, bindingsBuf).symbol.termRef
}
}
Expand Down
7 changes: 7 additions & 0 deletions tests/run/i6341.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
object Test extends App {
class Config(val t1: Int)

inline def m(t2:Int) = t2

m(new Config(3).t1)
}