Skip to content

Commit 9e8d5c5

Browse files
authored
Merge pull request #12867 from EnzeXing/non-hot-local-val
Allowing local variables to be initialized with non-hot values in initialization checker
2 parents 798faa6 + e901a36 commit 9e8d5c5

25 files changed

+288
-78
lines changed

compiler/src/dotty/tools/dotc/Compiler.scala

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -64,8 +64,8 @@ class Compiler {
6464
new CheckStatic, // Check restrictions that apply to @static members
6565
new BetaReduce, // Reduce closure applications
6666
new InlineVals, // Check right hand-sides of an `inline val`s
67-
new ExpandSAMs, // Expand single abstract method closures to anonymous classes
68-
new init.Checker) :: // Check initialization of objects
67+
new ExpandSAMs) :: // Expand single abstract method closures to anonymous classes
68+
List(new init.Checker) :: // Check initialization of objects
6969
List(new ElimRepeated, // Rewrite vararg parameters and arguments
7070
new ProtectedAccessors, // Add accessors for protected members
7171
new ExtensionMethods, // Expand methods of value classes with extension methods

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -290,7 +290,7 @@ object Phases {
290290
/** If set, implicit search is enabled */
291291
def allowsImplicitSearch: Boolean = false
292292

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

296296
/** @pre `isRunnable` returns true */

compiler/src/dotty/tools/dotc/transform/init/Checker.scala

Lines changed: 30 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,15 +10,16 @@ import dotty.tools.dotc.core._
1010
import Contexts._
1111
import Types._
1212
import Symbols._
13+
import StdNames._
1314

1415
import dotty.tools.dotc.transform._
15-
import MegaPhase._
16+
import Phases._
1617

1718

1819
import scala.collection.mutable
1920

2021

21-
class Checker extends MiniPhase {
22+
class Checker extends Phase {
2223
import tpd._
2324

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

33-
override def transformTypeDef(tree: TypeDef)(using Context): tpd.Tree = {
34-
if (!tree.isClassDef) return tree
34+
override def runOn(units: List[CompilationUnit])(using Context): List[CompilationUnit] =
35+
units.foreach { unit => traverser.traverse(unit.tpdTree) }
36+
super.runOn(units)
37+
38+
val traverser = new TreeTraverser {
39+
override def traverse(tree: Tree)(using Context): Unit =
40+
traverseChildren(tree)
41+
tree match {
42+
case tdef: MemberDef =>
43+
// self-type annotation ValDef has no symbol
44+
if tdef.name != nme.WILDCARD then
45+
tdef.symbol.defTree = tree
46+
case _ =>
47+
}
48+
}
49+
50+
override def run(using Context): Unit = {
51+
val unit = ctx.compilationUnit
52+
unit.tpdTree.foreachSubTree {
53+
case tdef: TypeDef if tdef.isClassDef =>
54+
transformTypeDef(tdef)
55+
56+
case _ =>
57+
}
58+
}
59+
3560

61+
private def transformTypeDef(tree: TypeDef)(using Context): tpd.Tree = {
3662
val cls = tree.symbol.asClass
3763
val instantiable: Boolean =
3864
cls.is(Flags.Module) ||

compiler/src/dotty/tools/dotc/transform/init/Errors.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ object Errors {
6969

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

7575
case class AccessCold(field: Symbol, source: Tree, trace: Seq[Tree]) extends Error {

compiler/src/dotty/tools/dotc/transform/init/Semantic.scala

Lines changed: 99 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import Contexts._
77
import Symbols._
88
import Types._
99
import StdNames._
10+
import NameKinds.OuterSelectName
1011

1112
import ast.tpd._
1213
import util.EqHashMap
@@ -27,24 +28,20 @@ class Semantic {
2728
* Value = Hot | Cold | Warm | ThisRef | Fun | RefSet
2829
*
2930
* Cold
30-
* ┌──────► ▲ ──┐ ◄────┐
31-
* │ │ │ │
32-
* │ │ │ │
33-
* ThisRef(C) │ │
34-
* │ │ │
35-
* Warm(D) Fun RefSet
36-
* │ ▲ ▲ ▲
37-
* │ │ │ │
38-
* Warm(C) │ │
39-
* ▲ │ │ │
40-
* │ │ │ │
41-
* └─────────┴──────┴───────┘
31+
* ┌──────► ▲ ◄────┐ ◄────┐
32+
* │ │ │ │
33+
* │ │ │ │
34+
* | │ │ │
35+
* | │ │
36+
* ThisRef(C) Warm(D) Fun RefSet
37+
* │ ▲ ▲ ▲
38+
* │ │ │ │
39+
* | │ │
40+
* ▲ │ │ │
41+
* │ │ │ │
42+
* └─────────┴──────┴───────┘
4243
* Hot
4344
*
44-
* The most important ordering is the following:
45-
*
46-
* Hot ⊑ Warm(C) ⊑ ThisRef(C) ⊑ Cold
47-
*
4845
* The diagram above does not reflect relationship between `RefSet`
4946
* and other values. `RefSet` represents a set of values which could
5047
* be `ThisRef`, `Warm` or `Fun`. The following ordering applies for
@@ -194,6 +191,7 @@ class Semantic {
194191
class PromotionInfo {
195192
var isCurrentObjectPromoted: Boolean = false
196193
val values = mutable.Set.empty[Value]
194+
override def toString(): String = values.toString()
197195
}
198196
/** Values that have been safely promoted */
199197
opaque type Promoted = PromotionInfo
@@ -300,9 +298,6 @@ class Semantic {
300298
case (Cold, _) => Cold
301299
case (_, Cold) => Cold
302300

303-
case (a: Warm, b: ThisRef) if a.klass == b.klass => b
304-
case (a: ThisRef, b: Warm) if a.klass == b.klass => a
305-
306301
case (a: (Fun | Warm | ThisRef), b: (Fun | Warm | ThisRef)) => RefSet(a :: b :: Nil)
307302

308303
case (a: (Fun | Warm | ThisRef), RefSet(refs)) => RefSet(a :: refs)
@@ -495,6 +490,46 @@ class Semantic {
495490
Result(value2, errors)
496491
}
497492
}
493+
494+
def accessLocal(tmref: TermRef, klass: ClassSymbol, source: Tree): Contextual[Result] =
495+
val sym = tmref.symbol
496+
497+
def default() = Result(Hot, Nil)
498+
499+
if sym.is(Flags.Param) && sym.owner.isConstructor then
500+
// instances of local classes inside secondary constructors cannot
501+
// reach here, as those values are abstracted by Cold instead of Warm.
502+
// This enables us to simplify the domain without sacrificing
503+
// expressiveness nor soundess, as local classes inside secondary
504+
// constructors are uncommon.
505+
if sym.isContainedIn(klass) then
506+
Result(env.lookup(sym), Nil)
507+
else
508+
// We don't know much about secondary constructor parameters in outer scope.
509+
// It's always safe to approximate them with `Cold`.
510+
Result(Cold, Nil)
511+
else if sym.is(Flags.Param) then
512+
default()
513+
else
514+
sym.defTree match {
515+
case vdef: ValDef =>
516+
// resolve this for local variable
517+
val enclosingClass = sym.owner.enclosingClass.asClass
518+
val thisValue2 = resolveThis(enclosingClass, value, klass, source)
519+
thisValue2 match {
520+
case Hot => Result(Hot, Errors.empty)
521+
522+
case Cold => Result(Cold, Nil)
523+
524+
case addr: Addr => eval(vdef.rhs, addr, klass)
525+
526+
case _ =>
527+
report.error("unexpected defTree when accessing local variable, sym = " + sym.show + ", defTree = " + sym.defTree.show, source)
528+
default()
529+
}
530+
531+
case _ => default()
532+
}
498533
end extension
499534

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

758793
case Select(qualifier, name) =>
759-
eval(qualifier, thisV, klass).select(expr.symbol, expr)
794+
val qualRes = eval(qualifier, thisV, klass)
795+
796+
name match
797+
case OuterSelectName(_, hops) =>
798+
val SkolemType(tp) = expr.tpe
799+
val outer = resolveOuterSelect(tp.classSymbol.asClass, qualRes.value, hops, source = expr)
800+
Result(outer, qualRes.errors)
801+
case _ =>
802+
qualRes.select(expr.symbol, expr)
760803

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

851894
case ddef : DefDef =>
852895
// local method
@@ -874,24 +917,7 @@ class Semantic {
874917
Result(Hot, Errors.empty)
875918

876919
case tmref: TermRef if tmref.prefix == NoPrefix =>
877-
val sym = tmref.symbol
878-
879-
def default() = Result(Hot, Nil)
880-
881-
if sym.is(Flags.Param) && sym.owner.isConstructor then
882-
// instances of local classes inside secondary constructors cannot
883-
// reach here, as those values are abstracted by Cold instead of Warm.
884-
// This enables us to simplify the domain without sacrificing
885-
// expressiveness nor soundess, as local classes inside secondary
886-
// constructors are uncommon.
887-
if sym.isContainedIn(klass) then
888-
Result(env.lookup(sym), Nil)
889-
else
890-
// We don't know much about secondary constructor parameters in outer scope.
891-
// It's always safe to approximate them with `Cold`.
892-
Result(Cold, Nil)
893-
else
894-
default()
920+
thisV.accessLocal(tmref, klass, source)
895921

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

940966
}
941967

968+
/** Resolve outer select introduced during inlining.
969+
*
970+
* See `tpd.outerSelect` and `ElimOuterSelect`.
971+
*/
972+
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) {
973+
// Is `target` reachable from `cls` with the given `hops`?
974+
def reachable(cls: ClassSymbol, hops: Int): Boolean =
975+
if hops == 0 then cls == target
976+
else reachable(cls.lexicallyEnclosingClass.asClass, hops - 1)
977+
978+
thisV match
979+
case Hot => Hot
980+
981+
case addr: Addr =>
982+
val obj = heap(addr)
983+
val curOpt = obj.klass.baseClasses.find(cls => reachable(cls, hops))
984+
curOpt match
985+
case Some(cur) =>
986+
resolveThis(target, thisV, cur, source)
987+
988+
case None =>
989+
report.warning("unexpected outerSelect, thisV = " + thisV + ", target = " + target.show + ", hops = " + hops, source.srcPos)
990+
Cold
991+
992+
case RefSet(refs) =>
993+
refs.map(ref => resolveOuterSelect(target, ref, hops, source)).join
994+
995+
case fun: Fun =>
996+
report.warning("unexpected thisV = " + thisV + ", target = " + target.show + ", hops = " + hops, source.srcPos)
997+
Cold
998+
999+
case Cold => Cold
1000+
}
1001+
9421002
/** Compute the outer value that correspond to `tref.prefix` */
9431003
def outerValue(tref: TypeRef, thisV: Addr, klass: ClassSymbol, source: Tree): Contextual[Result] =
9441004
val cls = tref.classSymbol.asClass

tests/init/crash/i8892.scala

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
trait Reporter:
2+
def report(m: String): Unit
3+
4+
class Dummy extends Reporter:
5+
def report(m: String) = ()
6+
7+
object ABug {
8+
sealed trait Nat {
9+
transparent inline def ++ : Succ[this.type] = Succ(this)
10+
11+
transparent inline def +(inline that: Nat): Nat =
12+
inline this match {
13+
case Zero => that
14+
case Succ(p) => p + that.++
15+
}
16+
}
17+
18+
case object Zero extends Nat
19+
case class Succ[N <: Nat](p: N) extends Nat
20+
21+
transparent inline def toIntg(inline n: Nat): Int =
22+
inline n match {
23+
case Zero => 0
24+
case Succ(p) => toIntg(p) + 1
25+
}
26+
27+
val j31 = toIntg(Zero.++.++.++ + Zero.++)
28+
}

tests/init/neg/Desugar.check

Lines changed: 0 additions & 5 deletions
This file was deleted.

tests/init/neg/InteractiveDriver.check

Lines changed: 0 additions & 5 deletions
This file was deleted.

tests/init/neg/closureLeak.check

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
-- Error: tests/init/neg/closureLeak.scala:11:14 -----------------------------------------------------------------------
22
11 | l.foreach(a => a.addX(this)) // error
33
| ^^^^^^^^^^^^^^^^^
4-
| Cannot prove that the value is fully-initialized. May only use initialized value as arguments.
4+
| Cannot prove that the value is fully-initialized. May only use initialized value as arguments.
55
|
6-
| The unsafe promotion may cause the following problem:
7-
| Promote the value under initialization to fully-initialized. May only use initialized value as arguments.
6+
| The unsafe promotion may cause the following problem:
7+
| Cannot prove that the value is fully initialized. May only use initialized value as arguments.

tests/init/neg/cycle-structure.check

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
-- Error: tests/init/neg/cycle-structure.scala:3:14 --------------------------------------------------------------------
22
3 | val x = B(this) // error
33
| ^^^^
4-
| Promote the value under initialization to fully-initialized. May only use initialized value as arguments.
4+
| Cannot prove that the value is fully initialized. May only use initialized value as arguments.
55
-- Error: tests/init/neg/cycle-structure.scala:9:14 --------------------------------------------------------------------
66
9 | val x = A(this) // error
77
| ^^^^
8-
| Promote the value under initialization to fully-initialized. May only use initialized value as arguments.
8+
| Cannot prove that the value is fully initialized. May only use initialized value as arguments.

tests/init/neg/default-this.check

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
-- Error: tests/init/neg/default-this.scala:9:8 ------------------------------------------------------------------------
22
9 | compare() // error
33
| ^^^^^^^
4-
|Promote the value under initialization to fully-initialized. May only use initialized value as arguments. Calling trace:
5-
| -> val result = updateThenCompare(5) [ default-this.scala:11 ]
4+
| Cannot prove that the value is fully initialized. May only use initialized value as arguments. Calling trace:
5+
| -> val result = updateThenCompare(5) [ default-this.scala:11 ]

tests/init/neg/inherit-non-hot.check

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
-- Error: tests/init/neg/inherit-non-hot.scala:6:34 --------------------------------------------------------------------
2+
6 | if b == null then b = new B(this) // error
3+
| ^^^^^^^^^^^
4+
| Cannot prove that the value is fully-initialized. May only assign fully initialized value.
5+
| Calling trace:
6+
| -> val c = new C [ inherit-non-hot.scala:19 ]
7+
| -> class C extends A { [ inherit-non-hot.scala:15 ]
8+
| -> val bAgain = toB.getBAgain [ inherit-non-hot.scala:16 ]
9+
|
10+
| The unsafe promotion may cause the following problem:
11+
| Call method Foo.B.this.aCopy.toB on a value with an unknown initialization. Calling trace:
12+
| -> val c = new C [ inherit-non-hot.scala:19 ]
13+
| -> class C extends A { [ inherit-non-hot.scala:15 ]
14+
| -> val bAgain = toB.getBAgain [ inherit-non-hot.scala:16 ]
15+
| -> if b == null then b = new B(this) // error [ inherit-non-hot.scala:6 ]
16+
| -> def getBAgain: B = aCopy.toB [ inherit-non-hot.scala:12 ]

tests/init/neg/inherit-non-hot.scala

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
// This is a minimized test for the warning in Names.scala:174
2+
object Foo {
3+
abstract class A {
4+
var b: B = null
5+
def toB: B =
6+
if b == null then b = new B(this) // error
7+
b
8+
}
9+
10+
class B(a: A) {
11+
var aCopy: A = a
12+
def getBAgain: B = aCopy.toB
13+
}
14+
15+
class C extends A {
16+
val bAgain = toB.getBAgain
17+
}
18+
19+
val c = new C
20+
assert(c.b == c.bAgain)
21+
}

0 commit comments

Comments
 (0)