Skip to content

Commit 346b8fa

Browse files
authored
Merge pull request #4710 from dotty-staging/add-class-purity
Some Rudimentary Tracking of Class Purity
2 parents 5dcf4b9 + 958bc48 commit 346b8fa

File tree

9 files changed

+79
-36
lines changed

9 files changed

+79
-36
lines changed

compiler/src/dotty/tools/dotc/ast/TreeInfo.scala

Lines changed: 45 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -240,6 +240,35 @@ trait TreeInfo[T >: Untyped <: Type] { self: Trees.Instance[T] =>
240240
case y => y
241241
}
242242

243+
/** The largest subset of {NoInits, PureInterface} that a
244+
* trait or class enclosing this statement can have as flags.
245+
*/
246+
def defKind(tree: Tree)(implicit ctx: Context): FlagSet = unsplice(tree) match {
247+
case EmptyTree | _: Import => NoInitsInterface
248+
case tree: TypeDef => if (tree.isClassDef) NoInits else NoInitsInterface
249+
case tree: DefDef =>
250+
if (tree.unforcedRhs == EmptyTree &&
251+
tree.vparamss.forall(_.forall(_.rhs.isEmpty))) NoInitsInterface
252+
else NoInits
253+
case tree: ValDef => if (tree.unforcedRhs == EmptyTree) NoInitsInterface else EmptyFlags
254+
case _ => EmptyFlags
255+
}
256+
257+
/** The largest subset of {NoInits, PureInterface} that a
258+
* trait or class with these parents can have as flags.
259+
*/
260+
def parentsKind(parents: List[Tree])(implicit ctx: Context): FlagSet = parents match {
261+
case Nil => NoInitsInterface
262+
case Apply(_, _ :: _) :: _ => EmptyFlags
263+
case _ :: parents1 => parentsKind(parents1)
264+
}
265+
266+
/** The largest subset of {NoInits, PureInterface} that a
267+
* trait or class with this body can have as flags.
268+
*/
269+
def bodyKind(body: List[Tree])(implicit ctx: Context): FlagSet =
270+
(NoInitsInterface /: body)((fs, stat) => fs & defKind(stat))
271+
243272
/** Checks whether predicate `p` is true for all result parts of this expression,
244273
* where we zoom into Ifs, Matches, and Blocks.
245274
*/
@@ -358,6 +387,8 @@ trait TypedTreeInfo extends TreeInfo[Type] { self: Trees.Instance[Type] =>
358387
refPurity(tree)
359388
case Select(qual, _) =>
360389
refPurity(tree).min(exprPurity(qual))
390+
case New(_) =>
391+
SimplyPure
361392
case TypeApply(fn, _) =>
362393
exprPurity(fn)
363394
/*
@@ -369,13 +400,12 @@ trait TypedTreeInfo extends TreeInfo[Type] { self: Trees.Instance[Type] =>
369400
case Apply(fn, args) =>
370401
def isKnownPureOp(sym: Symbol) =
371402
sym.owner.isPrimitiveValueClass || sym.owner == defn.StringClass
372-
// Note: After uncurry, field accesses are represented as Apply(getter, Nil),
373-
// so an Apply can also be pure.
374-
if (args.isEmpty && fn.symbol.is(Stable)) exprPurity(fn)
375-
else if (tree.tpe.isInstanceOf[ConstantType] && isKnownPureOp(tree.symbol))
376-
// A constant expression with pure arguments is pure.
403+
if (tree.tpe.isInstanceOf[ConstantType] && isKnownPureOp(tree.symbol)
404+
// A constant expression with pure arguments is pure.
405+
|| fn.symbol.isStable)
377406
minOf(exprPurity(fn), args.map(exprPurity)) `min` Pure
378-
else Impure
407+
else
408+
Impure
379409
case Typed(expr, _) =>
380410
exprPurity(expr)
381411
case Block(stats, expr) =>
@@ -402,11 +432,16 @@ trait TypedTreeInfo extends TreeInfo[Type] { self: Trees.Instance[Type] =>
402432
* @DarkDimius: need to make sure that lazy accessor methods have Lazy and Stable
403433
* flags set.
404434
*/
405-
def refPurity(tree: Tree)(implicit ctx: Context): PurityLevel =
406-
if (!tree.tpe.widen.isParameterless || tree.symbol.is(Erased)) SimplyPure
407-
else if (!tree.symbol.isStable) Impure
408-
else if (tree.symbol.is(Lazy)) Idempotent // TODO add Module flag, sinxce Module vals or not Lazy from the start.
435+
def refPurity(tree: Tree)(implicit ctx: Context): PurityLevel = {
436+
val sym = tree.symbol
437+
if (!tree.hasType) Impure
438+
else if (!tree.tpe.widen.isParameterless || sym.is(Erased)) SimplyPure
439+
else if (!sym.isStable) Impure
440+
else if (sym.is(Module))
441+
if (sym.moduleClass.isNoInitsClass) Pure else Idempotent
442+
else if (sym.is(Lazy)) Idempotent
409443
else SimplyPure
444+
}
410445

411446
def isPureRef(tree: Tree)(implicit ctx: Context) =
412447
refPurity(tree) == SimplyPure
@@ -623,17 +658,6 @@ trait TypedTreeInfo extends TreeInfo[Type] { self: Trees.Instance[Type] =>
623658
accum(Nil, root)
624659
}
625660

626-
/** The largest subset of {NoInits, PureInterface} that a
627-
* trait enclosing this statement can have as flags.
628-
*/
629-
def defKind(tree: Tree): FlagSet = unsplice(tree) match {
630-
case EmptyTree | _: Import => NoInitsInterface
631-
case tree: TypeDef => if (tree.isClassDef) NoInits else NoInitsInterface
632-
case tree: DefDef => if (tree.unforcedRhs == EmptyTree) NoInitsInterface else NoInits
633-
case tree: ValDef => if (tree.unforcedRhs == EmptyTree) NoInitsInterface else EmptyFlags
634-
case _ => EmptyFlags
635-
}
636-
637661
/** The top level classes in this tree, including only those module classes that
638662
* are not a linked class of some other class in the result.
639663
*/

compiler/src/dotty/tools/dotc/core/Definitions.scala

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,10 +55,10 @@ class Definitions {
5555
ctx.newSymbol(owner, name, flags | Permanent, info)
5656

5757
private def newClassSymbol(owner: Symbol, name: TypeName, flags: FlagSet, infoFn: ClassSymbol => Type) =
58-
ctx.newClassSymbol(owner, name, flags | Permanent, infoFn)
58+
ctx.newClassSymbol(owner, name, flags | Permanent | NoInits, infoFn)
5959

6060
private def enterCompleteClassSymbol(owner: Symbol, name: TypeName, flags: FlagSet, parents: List[TypeRef], decls: Scope = newScope) =
61-
ctx.newCompleteClassSymbol(owner, name, flags | Permanent, parents, decls).entered
61+
ctx.newCompleteClassSymbol(owner, name, flags | Permanent | NoInits, parents, decls).entered
6262

6363
private def enterTypeField(cls: ClassSymbol, name: TypeName, flags: FlagSet, scope: MutableScope) =
6464
scope.enter(newSymbol(cls, name, flags, TypeBounds.empty))
@@ -275,6 +275,7 @@ class Definitions {
275275
val cls = ctx.requiredClass("java.lang.Object")
276276
assert(!cls.isCompleted, "race for completing java.lang.Object")
277277
cls.info = ClassInfo(cls.owner.thisType, cls, AnyClass.typeRef :: Nil, newScope)
278+
cls.setFlag(NoInits)
278279

279280
// The companion object doesn't really exist, `NoType` is the general
280281
// technique to do that. Here we need to set it before completing
@@ -1206,6 +1207,13 @@ class Definitions {
12061207
for (m <- ScalaShadowingPackageClass.info.decls)
12071208
ScalaPackageClass.enter(m)
12081209

1210+
// Temporary measure, as long as we do not read these classes from Tasty.
1211+
// Scala-2 classes don't have NoInits set even if they are pure. We override this
1212+
// for Product and Serializable so that case classes can be pure. A full solution
1213+
// requiers that we read all Scala code from Tasty.
1214+
ProductClass.setFlag(NoInits)
1215+
SerializableClass.setFlag(NoInits)
1216+
12091217
// force initialization of every symbol that is synthesized or hijacked by the compiler
12101218
val forced = syntheticCoreClasses ++ syntheticCoreMethods ++ ScalaValueClasses()
12111219

compiler/src/dotty/tools/dotc/core/SymDenotations.scala

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -167,11 +167,14 @@ object SymDenotations {
167167
/** Unset given flags(s) of this denotation */
168168
final def resetFlag(flags: FlagSet): Unit = { myFlags &~= flags }
169169

170-
/** Set applicable flags from `flags` which is a subset of {NoInits, PureInterface} */
171-
final def setNoInitsFlags(flags: FlagSet): Unit = {
172-
val mask = if (myFlags.is(Trait)) NoInitsInterface else NoInits
173-
setFlag(flags & mask)
174-
}
170+
/** Set applicable flags in {NoInits, PureInterface}
171+
* @param parentFlags The flags that match the class or trait's parents
172+
* @param bodyFlags The flags that match the class or trait's body
173+
*/
174+
final def setNoInitsFlags(parentFlags: FlagSet, bodyFlags: FlagSet): Unit =
175+
setFlag(
176+
if (myFlags.is(Trait)) NoInitsInterface & bodyFlags // no parents are initialized from a trait
177+
else NoInits & bodyFlags & parentFlags)
175178

176179
private def isCurrent(fs: FlagSet) =
177180
fs <= (
@@ -599,6 +602,12 @@ object SymDenotations {
599602
final def isStable(implicit ctx: Context) =
600603
isType || is(Stable) || !(is(UnstableValue) || info.isInstanceOf[ExprType])
601604

605+
/** Is this a denotation of a class that does not have - either direct or inherited -
606+
* initaliazion code?
607+
*/
608+
def isNoInitsClass(implicit ctx: Context) =
609+
isClass && asClass.baseClasses.forall(_.is(NoInits))
610+
602611
/** Is this a "real" method? A real method is a method which is:
603612
* - not an accessor
604613
* - not a label

compiler/src/dotty/tools/dotc/core/tasty/TreeUnpickler.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -871,7 +871,7 @@ class TreeUnpickler(reader: TastyReader,
871871
else EmptyValDef
872872
cls.info = ClassInfo(cls.owner.thisType, cls, parentTypes, cls.unforcedDecls,
873873
if (self.isEmpty) NoType else self.tpt.tpe)
874-
cls.setNoInitsFlags(fork.indexStats(end))
874+
cls.setNoInitsFlags(parentsKind(parents), fork.indexStats(end))
875875
val constr = readIndexedDef().asInstanceOf[DefDef]
876876
val mappedParents = parents.map(_.changeOwner(localDummy, constr.symbol))
877877

compiler/src/dotty/tools/dotc/sbt/ExtractAPI.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -328,7 +328,7 @@ private class ExtractAPICollector(implicit val ctx: Context) extends ThunkHolder
328328
} else if (sym.is(Mutable, butNot = Accessor)) {
329329
api.Var.of(sym.name.toString, apiAccess(sym), apiModifiers(sym),
330330
apiAnnotations(sym).toArray, apiType(sym.info))
331-
} else if (sym.isStable) {
331+
} else if (sym.isStable && !sym.isRealMethod) {
332332
api.Val.of(sym.name.toString, apiAccess(sym), apiModifiers(sym),
333333
apiAnnotations(sym).toArray, apiType(sym.info))
334334
} else {

compiler/src/dotty/tools/dotc/typer/Namer.scala

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -924,6 +924,8 @@ class Namer { typer: Typer =>
924924
if (isDerivedValueClass(cls)) cls.setFlag(Final)
925925
cls.info = avoidPrivateLeaks(cls, cls.pos)
926926
cls.baseClasses.foreach(_.invalidateBaseTypeCache()) // we might have looked before and found nothing
927+
cls.setNoInitsFlags(parentsKind(parents), bodyKind(rest))
928+
if (cls.isNoInitsClass) cls.primaryConstructor.setFlag(Stable)
927929
}
928930
}
929931

compiler/src/dotty/tools/dotc/typer/Typer.scala

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1537,8 +1537,6 @@ class Typer extends Namer
15371537
val dummy = localDummy(cls, impl)
15381538
val body1 = addAccessorDefs(cls,
15391539
typedStats(impl.body, dummy)(ctx.inClassContext(self1.symbol)))
1540-
if (!ctx.isAfterTyper)
1541-
cls.setNoInitsFlags((NoInitsInterface /: body1) ((fs, stat) => fs & defKind(stat)))
15421540

15431541
// Expand comments and type usecases if `-Ycook-comments` is set.
15441542
if (ctx.settings.YcookComments.value) {
@@ -1922,7 +1920,8 @@ class Typer extends Namer
19221920
traverse(stats ++ rest)
19231921
case stat :: rest =>
19241922
val stat1 = typed(stat)(ctx.exprContext(stat, exprOwner))
1925-
if (!ctx.isAfterTyper && isPureExpr(stat1) && !stat1.tpe.isRef(defn.UnitClass))
1923+
if (!ctx.isAfterTyper && isPureExpr(stat1) &&
1924+
!stat1.tpe.isRef(defn.UnitClass) && !isSelfOrSuperConstrCall(stat1))
19261925
ctx.warning(em"a pure expression does nothing in statement position", stat.pos)
19271926
buf += stat1
19281927
traverse(rest)

compiler/test/dotty/tools/dotc/reporting/ErrorMessagesTests.scala

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -163,7 +163,7 @@ class ErrorMessagesTests extends ErrorMessagesTest {
163163
"""
164164
|object Scope {
165165
| abstract class Concept
166-
| new Concept()
166+
| val x = new Concept()
167167
|}
168168
""".stripMargin
169169
}
@@ -181,7 +181,7 @@ class ErrorMessagesTests extends ErrorMessagesTest {
181181
"""
182182
|object Scope {
183183
| trait Concept
184-
| new Concept()
184+
| val x = new Concept()
185185
|}
186186
""".stripMargin
187187
}
@@ -508,7 +508,7 @@ class ErrorMessagesTests extends ErrorMessagesTest {
508508
"""class Base
509509
|class RequiresBase { self: Base => }
510510
|object Scope {
511-
| new RequiresBase
511+
| val x = new RequiresBase
512512
|}
513513
|""".stripMargin
514514
}

tests/neg-custom-args/fatal-warnings/i2333.scala

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
@deprecated("bla", "2.11.0") class Foo {
2+
println("")
23
def this(x: Int) = this()
34
}
45

0 commit comments

Comments
 (0)