Skip to content

Allowing local variables to be initialized with non-hot values in initialization checker #12867

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 9 commits into from
Jul 30, 2021
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
4 changes: 2 additions & 2 deletions compiler/src/dotty/tools/dotc/Compiler.scala
Original file line number Diff line number Diff line change
Expand Up @@ -64,8 +64,8 @@ class Compiler {
new CheckStatic, // Check restrictions that apply to @static members
new BetaReduce, // Reduce closure applications
new InlineVals, // Check right hand-sides of an `inline val`s
new ExpandSAMs, // Expand single abstract method closures to anonymous classes
new init.Checker) :: // Check initialization of objects
new ExpandSAMs) :: // Expand single abstract method closures to anonymous classes
List(new init.Checker) :: // Check initialization of objects
List(new ElimRepeated, // Rewrite vararg parameters and arguments
new ProtectedAccessors, // Add accessors for protected members
new ExtensionMethods, // Expand methods of value classes with extension methods
Expand Down
2 changes: 1 addition & 1 deletion compiler/src/dotty/tools/dotc/core/Phases.scala
Original file line number Diff line number Diff line change
Expand Up @@ -290,7 +290,7 @@ object Phases {
/** If set, implicit search is enabled */
def allowsImplicitSearch: Boolean = false

/** List of names of phases that should precede this phase */
/** List of names of phases that should precede this phase */
def runsAfter: Set[String] = Set.empty

/** @pre `isRunnable` returns true */
Expand Down
34 changes: 30 additions & 4 deletions compiler/src/dotty/tools/dotc/transform/init/Checker.scala
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,16 @@ import dotty.tools.dotc.core._
import Contexts._
import Types._
import Symbols._
import StdNames._

import dotty.tools.dotc.transform._
import MegaPhase._
import Phases._


import scala.collection.mutable


class Checker extends MiniPhase {
class Checker extends Phase {
import tpd._

val phaseName = "initChecker"
Expand All @@ -30,9 +31,34 @@ class Checker extends MiniPhase {
override def isEnabled(using Context): Boolean =
super.isEnabled && ctx.settings.YcheckInit.value

override def transformTypeDef(tree: TypeDef)(using Context): tpd.Tree = {
if (!tree.isClassDef) return tree
override def runOn(units: List[CompilationUnit])(using Context): List[CompilationUnit] =
units.foreach { unit => traverser.traverse(unit.tpdTree) }
super.runOn(units)

val traverser = new TreeTraverser {
override def traverse(tree: Tree)(using Context): Unit =
traverseChildren(tree)
tree match {
case tdef: MemberDef =>
// self-type annotation ValDef has no symbol
if tdef.name != nme.WILDCARD then
tdef.symbol.defTree = tree
case _ =>
}
}

override def run(using Context): Unit = {
val unit = ctx.compilationUnit
unit.tpdTree.foreachSubTree {
case tdef: TypeDef if tdef.isClassDef =>
transformTypeDef(tdef)

case _ =>
}
}


private def transformTypeDef(tree: TypeDef)(using Context): tpd.Tree = {
val cls = tree.symbol.asClass
val instantiable: Boolean =
cls.is(Flags.Module) ||
Expand Down
2 changes: 1 addition & 1 deletion compiler/src/dotty/tools/dotc/transform/init/Errors.scala
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ object Errors {

/** Promote `this` under initialization to fully-initialized */
case class PromoteError(msg: String, source: Tree, trace: Seq[Tree]) extends Error {
def show(using Context): String = "Promote the value under initialization to fully-initialized. " + msg + "."
def show(using Context): String = "Cannot prove that the value is fully initialized. " + msg + "."
}

case class AccessCold(field: Symbol, source: Tree, trace: Seq[Tree]) extends Error {
Expand Down
138 changes: 99 additions & 39 deletions compiler/src/dotty/tools/dotc/transform/init/Semantic.scala
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import Contexts._
import Symbols._
import Types._
import StdNames._
import NameKinds.OuterSelectName

import ast.tpd._
import util.EqHashMap
Expand All @@ -27,24 +28,20 @@ class Semantic {
* Value = Hot | Cold | Warm | ThisRef | Fun | RefSet
*
* Cold
* ┌──────► ▲ ──┐ ◄────┐
* │ │ │ │
* │ │ │ │
* ThisRef(C) │ │
* │ │ │
* Warm(D) Fun RefSet
* │ ▲ ▲ ▲
* │ │ │ │
* Warm(C) │ │
* ▲ │ │ │
* │ │ │ │
* └─────────┴──────┴───────┘
* ┌──────► ▲ ◄────┐ ◄────┐
* │ │ │ │
* │ │ │ │
* | │ │ │
* | │ │
* ThisRef(C) Warm(D) Fun RefSet
* │ ▲ ▲ ▲
* │ │ │ │
* | │ │
* ▲ │ │ │
* │ │ │ │
* └─────────┴──────┴───────┘
* Hot
*
* The most important ordering is the following:
*
* Hot ⊑ Warm(C) ⊑ ThisRef(C) ⊑ Cold
*
* The diagram above does not reflect relationship between `RefSet`
* and other values. `RefSet` represents a set of values which could
* be `ThisRef`, `Warm` or `Fun`. The following ordering applies for
Expand Down Expand Up @@ -194,6 +191,7 @@ class Semantic {
class PromotionInfo {
var isCurrentObjectPromoted: Boolean = false
val values = mutable.Set.empty[Value]
override def toString(): String = values.toString()
}
/** Values that have been safely promoted */
opaque type Promoted = PromotionInfo
Expand Down Expand Up @@ -300,9 +298,6 @@ class Semantic {
case (Cold, _) => Cold
case (_, Cold) => Cold

case (a: Warm, b: ThisRef) if a.klass == b.klass => b
case (a: ThisRef, b: Warm) if a.klass == b.klass => a

case (a: (Fun | Warm | ThisRef), b: (Fun | Warm | ThisRef)) => RefSet(a :: b :: Nil)

case (a: (Fun | Warm | ThisRef), RefSet(refs)) => RefSet(a :: refs)
Expand Down Expand Up @@ -495,6 +490,46 @@ class Semantic {
Result(value2, errors)
}
}

def accessLocal(tmref: TermRef, klass: ClassSymbol, source: Tree): Contextual[Result] =
val sym = tmref.symbol

def default() = Result(Hot, Nil)

if sym.is(Flags.Param) && sym.owner.isConstructor then
// instances of local classes inside secondary constructors cannot
// reach here, as those values are abstracted by Cold instead of Warm.
// This enables us to simplify the domain without sacrificing
// expressiveness nor soundess, as local classes inside secondary
// constructors are uncommon.
if sym.isContainedIn(klass) then
Result(env.lookup(sym), Nil)
else
// We don't know much about secondary constructor parameters in outer scope.
// It's always safe to approximate them with `Cold`.
Result(Cold, Nil)
else if sym.is(Flags.Param) then
default()
Copy link
Contributor Author

Choose a reason for hiding this comment

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

For parameters for normal method calls, we just return Result(Hot, Nil) since we expect that those arguments have been promoted in Call, right?

Copy link
Contributor

Choose a reason for hiding this comment

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

Yes, that's correct.

else
sym.defTree match {
case vdef: ValDef =>
// resolve this for local variable
val enclosingClass = sym.owner.enclosingClass.asClass
val thisValue2 = resolveThis(enclosingClass, value, klass, source)
thisValue2 match {
case Hot => Result(Hot, Errors.empty)

case Cold => Result(Cold, Nil)

case addr: Addr => eval(vdef.rhs, addr, klass)

case _ =>
report.error("unexpected defTree when accessing local variable, sym = " + sym.show + ", defTree = " + sym.defTree.show, source)
default()
}

case _ => default()
}
end extension

// ----- Promotion ----------------------------------------------------
Expand Down Expand Up @@ -756,7 +791,15 @@ class Semantic {
res.call(id.symbol, args, superType = NoType, source = expr)

case Select(qualifier, name) =>
eval(qualifier, thisV, klass).select(expr.symbol, expr)
val qualRes = eval(qualifier, thisV, klass)

name match
case OuterSelectName(_, hops) =>
val SkolemType(tp) = expr.tpe
val outer = resolveOuterSelect(tp.classSymbol.asClass, qualRes.value, hops, source = expr)
Result(outer, qualRes.errors)
case _ =>
qualRes.select(expr.symbol, expr)

case _: This =>
cases(expr.tpe, thisV, klass, expr)
Expand Down Expand Up @@ -846,7 +889,7 @@ class Semantic {
case vdef : ValDef =>
// local val definition
// TODO: support explicit @cold annotation for local definitions
eval(vdef.rhs, thisV, klass).ensureHot("Local definitions may only hold initialized values", vdef)
eval(vdef.rhs, thisV, klass, cacheResult = true)

case ddef : DefDef =>
// local method
Expand Down Expand Up @@ -874,24 +917,7 @@ class Semantic {
Result(Hot, Errors.empty)

case tmref: TermRef if tmref.prefix == NoPrefix =>
val sym = tmref.symbol

def default() = Result(Hot, Nil)

if sym.is(Flags.Param) && sym.owner.isConstructor then
// instances of local classes inside secondary constructors cannot
// reach here, as those values are abstracted by Cold instead of Warm.
// This enables us to simplify the domain without sacrificing
// expressiveness nor soundess, as local classes inside secondary
// constructors are uncommon.
if sym.isContainedIn(klass) then
Result(env.lookup(sym), Nil)
else
// We don't know much about secondary constructor parameters in outer scope.
// It's always safe to approximate them with `Cold`.
Result(Cold, Nil)
else
default()
thisV.accessLocal(tmref, klass, source)

case tmref: TermRef =>
cases(tmref.prefix, thisV, klass, source).select(tmref.symbol, source)
Expand Down Expand Up @@ -939,6 +965,40 @@ class Semantic {

}

/** Resolve outer select introduced during inlining.
*
* See `tpd.outerSelect` and `ElimOuterSelect`.
*/
def resolveOuterSelect(target: ClassSymbol, thisV: Value, hops: Int, source: Tree): Contextual[Value] = log("resolving outer " + target.show + ", this = " + thisV.show + ", hops = " + hops, printer, res => res.asInstanceOf[Value].show) {
// Is `target` reachable from `cls` with the given `hops`?
def reachable(cls: ClassSymbol, hops: Int): Boolean =
if hops == 0 then cls == target
else reachable(cls.lexicallyEnclosingClass.asClass, hops - 1)

thisV match
case Hot => Hot

case addr: Addr =>
val obj = heap(addr)
val curOpt = obj.klass.baseClasses.find(cls => reachable(cls, hops))
curOpt match
case Some(cur) =>
resolveThis(target, thisV, cur, source)

case None =>
report.warning("unexpected outerSelect, thisV = " + thisV + ", target = " + target.show + ", hops = " + hops, source.srcPos)
Cold

case RefSet(refs) =>
refs.map(ref => resolveOuterSelect(target, ref, hops, source)).join

case fun: Fun =>
report.warning("unexpected thisV = " + thisV + ", target = " + target.show + ", hops = " + hops, source.srcPos)
Cold

case Cold => Cold
}

/** Compute the outer value that correspond to `tref.prefix` */
def outerValue(tref: TypeRef, thisV: Addr, klass: ClassSymbol, source: Tree): Contextual[Result] =
val cls = tref.classSymbol.asClass
Expand Down
28 changes: 28 additions & 0 deletions tests/init/crash/i8892.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
trait Reporter:
def report(m: String): Unit

class Dummy extends Reporter:
def report(m: String) = ()

object ABug {
sealed trait Nat {
transparent inline def ++ : Succ[this.type] = Succ(this)

transparent inline def +(inline that: Nat): Nat =
inline this match {
case Zero => that
case Succ(p) => p + that.++
}
}

case object Zero extends Nat
case class Succ[N <: Nat](p: N) extends Nat

transparent inline def toIntg(inline n: Nat): Int =
inline n match {
case Zero => 0
case Succ(p) => toIntg(p) + 1
}

val j31 = toIntg(Zero.++.++.++ + Zero.++)
}
5 changes: 0 additions & 5 deletions tests/init/neg/Desugar.check

This file was deleted.

5 changes: 0 additions & 5 deletions tests/init/neg/InteractiveDriver.check

This file was deleted.

6 changes: 3 additions & 3 deletions tests/init/neg/closureLeak.check
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
-- Error: tests/init/neg/closureLeak.scala:11:14 -----------------------------------------------------------------------
11 | l.foreach(a => a.addX(this)) // error
| ^^^^^^^^^^^^^^^^^
| Cannot prove that the value is fully-initialized. May only use initialized value as arguments.
| Cannot prove that the value is fully-initialized. May only use initialized value as arguments.
|
| The unsafe promotion may cause the following problem:
| Promote the value under initialization to fully-initialized. May only use initialized value as arguments.
| The unsafe promotion may cause the following problem:
| Cannot prove that the value is fully initialized. May only use initialized value as arguments.
4 changes: 2 additions & 2 deletions tests/init/neg/cycle-structure.check
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
-- Error: tests/init/neg/cycle-structure.scala:3:14 --------------------------------------------------------------------
3 | val x = B(this) // error
| ^^^^
| Promote the value under initialization to fully-initialized. May only use initialized value as arguments.
| Cannot prove that the value is fully initialized. May only use initialized value as arguments.
-- Error: tests/init/neg/cycle-structure.scala:9:14 --------------------------------------------------------------------
9 | val x = A(this) // error
| ^^^^
| Promote the value under initialization to fully-initialized. May only use initialized value as arguments.
| Cannot prove that the value is fully initialized. May only use initialized value as arguments.
4 changes: 2 additions & 2 deletions tests/init/neg/default-this.check
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
-- Error: tests/init/neg/default-this.scala:9:8 ------------------------------------------------------------------------
9 | compare() // error
| ^^^^^^^
|Promote the value under initialization to fully-initialized. May only use initialized value as arguments. Calling trace:
| -> val result = updateThenCompare(5) [ default-this.scala:11 ]
| Cannot prove that the value is fully initialized. May only use initialized value as arguments. Calling trace:
| -> val result = updateThenCompare(5) [ default-this.scala:11 ]
16 changes: 16 additions & 0 deletions tests/init/neg/inherit-non-hot.check
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
-- Error: tests/init/neg/inherit-non-hot.scala:6:34 --------------------------------------------------------------------
6 | if b == null then b = new B(this) // error
| ^^^^^^^^^^^
| Cannot prove that the value is fully-initialized. May only assign fully initialized value.
| Calling trace:
| -> val c = new C [ inherit-non-hot.scala:19 ]
| -> class C extends A { [ inherit-non-hot.scala:15 ]
| -> val bAgain = toB.getBAgain [ inherit-non-hot.scala:16 ]
|
| The unsafe promotion may cause the following problem:
| Call method Foo.B.this.aCopy.toB on a value with an unknown initialization. Calling trace:
| -> val c = new C [ inherit-non-hot.scala:19 ]
| -> class C extends A { [ inherit-non-hot.scala:15 ]
| -> val bAgain = toB.getBAgain [ inherit-non-hot.scala:16 ]
| -> if b == null then b = new B(this) // error [ inherit-non-hot.scala:6 ]
| -> def getBAgain: B = aCopy.toB [ inherit-non-hot.scala:12 ]
21 changes: 21 additions & 0 deletions tests/init/neg/inherit-non-hot.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// This is a minimized test for the warning in Names.scala:174
object Foo {
abstract class A {
var b: B = null
def toB: B =
if b == null then b = new B(this) // error
b
}

class B(a: A) {
var aCopy: A = a
def getBAgain: B = aCopy.toB
}

class C extends A {
val bAgain = toB.getBAgain
}

val c = new C
assert(c.b == c.bAgain)
}
Loading