From 857cc74e9b86069c953313a25d16027ddc3e98ea Mon Sep 17 00:00:00 2001 From: EnzeXing Date: Tue, 24 Aug 2021 22:38:01 -0400 Subject: [PATCH 01/52] Update tests --- tests/init/neg/enum-desugared.check | 4 ++-- tests/init/neg/inner-loop.scala | 2 +- tests/init/neg/local-warm4.check | 5 ++--- tests/init/neg/unsound1.check | 4 ++++ tests/init/neg/unsound1.scala | 11 +++++++++++ tests/init/neg/unsound2.check | 6 ++++++ tests/init/neg/unsound2.scala | 10 ++++++++++ tests/init/neg/unsound3.check | 5 +++++ tests/init/neg/unsound3.scala | 13 +++++++++++++ tests/init/neg/unsound4.check | 6 ++++++ tests/init/neg/unsound4.scala | 4 ++++ 11 files changed, 64 insertions(+), 6 deletions(-) create mode 100644 tests/init/neg/unsound1.check create mode 100644 tests/init/neg/unsound1.scala create mode 100644 tests/init/neg/unsound2.check create mode 100644 tests/init/neg/unsound2.scala create mode 100644 tests/init/neg/unsound3.check create mode 100644 tests/init/neg/unsound3.scala create mode 100644 tests/init/neg/unsound4.check create mode 100644 tests/init/neg/unsound4.scala diff --git a/tests/init/neg/enum-desugared.check b/tests/init/neg/enum-desugared.check index 567b8104c154..2b090378e877 100644 --- a/tests/init/neg/enum-desugared.check +++ b/tests/init/neg/enum-desugared.check @@ -13,6 +13,6 @@ | Cannot prove that the value is fully-initialized. May only use initialized value as method arguments. | | The unsafe promotion may cause the following problem: - | Calling the external method method ordinal may cause initialization errors. Calling trace: + | Calling the external method method name may cause initialization errors. Calling trace: | -> Array(this.LazyErrorId, this.NoExplanationID) // error // error [ enum-desugared.scala:17 ] - | -> def errorNumber: Int = this.ordinal() - 2 [ enum-desugared.scala:8 ] + | -> override def productPrefix: String = this.name() [ enum-desugared.scala:29 ] diff --git a/tests/init/neg/inner-loop.scala b/tests/init/neg/inner-loop.scala index d2b6a1fae8e0..c6d5c615580c 100644 --- a/tests/init/neg/inner-loop.scala +++ b/tests/init/neg/inner-loop.scala @@ -1,6 +1,6 @@ class Outer { outer => class Inner extends Outer { - val x = 5 + outer.n // error + val x = 5 + outer.n } val inner = new Inner val n = 6 // error diff --git a/tests/init/neg/local-warm4.check b/tests/init/neg/local-warm4.check index fda1ee1b928c..664900b3cffa 100644 --- a/tests/init/neg/local-warm4.check +++ b/tests/init/neg/local-warm4.check @@ -6,6 +6,5 @@ | -> class A(x: Int) extends Foo(x) { [ local-warm4.scala:6 ] | -> val b = new B(y) [ local-warm4.scala:10 ] | -> class B(x: Int) extends A(x) { [ local-warm4.scala:13 ] - | -> class A(x: Int) extends Foo(x) { [ local-warm4.scala:6 ] - | -> increment() [ local-warm4.scala:9 ] - | -> updateA() [ local-warm4.scala:21 ] + | -> if y < 10 then increment() [ local-warm4.scala:23 ] + | -> updateA() [ local-warm4.scala:21 ] diff --git a/tests/init/neg/unsound1.check b/tests/init/neg/unsound1.check new file mode 100644 index 000000000000..54e24546845c --- /dev/null +++ b/tests/init/neg/unsound1.check @@ -0,0 +1,4 @@ +-- Error: tests/init/neg/unsound1.scala:2:35 --------------------------------------------------------------------------- +2 | if (m > 0) println(foo(m - 1).a2.n) // error + | ^^^^^^^^^^^^^^^ + | Access field A.this.foo(A.this.m.-(1)).a2.n on a value with an unknown initialization status. diff --git a/tests/init/neg/unsound1.scala b/tests/init/neg/unsound1.scala new file mode 100644 index 000000000000..3854504c8478 --- /dev/null +++ b/tests/init/neg/unsound1.scala @@ -0,0 +1,11 @@ +class A(m: Int) { + if (m > 0) println(foo(m - 1).a2.n) // error + def foo(n: Int): B = + if (n % 2 == 0) + new B(new A(n - 1), foo(n - 1).a1) + else + new B(this, new A(n - 1)) + var n: Int = 10 +} + +class B(val a1: A, val a2: A) \ No newline at end of file diff --git a/tests/init/neg/unsound2.check b/tests/init/neg/unsound2.check new file mode 100644 index 000000000000..346caec4cb1b --- /dev/null +++ b/tests/init/neg/unsound2.check @@ -0,0 +1,6 @@ +-- Error: tests/init/neg/unsound2.scala:5:26 --------------------------------------------------------------------------- +5 | def getN: Int = a.n // error + | ^^^ + | Access field B.this.a.n on a value with an unknown initialization status. Calling trace: + | -> println(foo(x).getB) [ unsound2.scala:8 ] + | -> def foo(y: Int): B = if (y > 10) then B(bar(y - 1), foo(y - 1).getN) else B(bar(y), 10) [ unsound2.scala:2 ] diff --git a/tests/init/neg/unsound2.scala b/tests/init/neg/unsound2.scala new file mode 100644 index 000000000000..5ae0c624c32e --- /dev/null +++ b/tests/init/neg/unsound2.scala @@ -0,0 +1,10 @@ +case class A(x: Int) { + def foo(y: Int): B = if (y > 10) then B(bar(y - 1), foo(y - 1).getN) else B(bar(y), 10) + def bar(y: Int): A = if (y > 10) then A(y - 1) else this + class B(a: A, b: Int) { + def getN: Int = a.n // error + def getB: Int = b + } + println(foo(x).getB) + val n: Int = 10 +} \ No newline at end of file diff --git a/tests/init/neg/unsound3.check b/tests/init/neg/unsound3.check new file mode 100644 index 000000000000..71766cf2d10b --- /dev/null +++ b/tests/init/neg/unsound3.check @@ -0,0 +1,5 @@ +-- Error: tests/init/neg/unsound3.scala:10:38 -------------------------------------------------------------------------- +10 | if (x < 12) then foo().getC().b else newB // error + | ^^^^^^^^^^^^^^ + | Access field C.this.foo().getC().b on a value with an unknown initialization status. Calling trace: + | -> val b = foo() [ unsound3.scala:12 ] diff --git a/tests/init/neg/unsound3.scala b/tests/init/neg/unsound3.scala new file mode 100644 index 000000000000..9ede5c7f97d0 --- /dev/null +++ b/tests/init/neg/unsound3.scala @@ -0,0 +1,13 @@ +class B(c: C) { + def getC() = c +} + +class C { + var x = 10 + def foo(): B = { + x += 1 + val newB = new B(this) + if (x < 12) then foo().getC().b else newB // error + } + val b = foo() +} \ No newline at end of file diff --git a/tests/init/neg/unsound4.check b/tests/init/neg/unsound4.check new file mode 100644 index 000000000000..4ed254444928 --- /dev/null +++ b/tests/init/neg/unsound4.check @@ -0,0 +1,6 @@ +-- Error: tests/init/neg/unsound4.scala:3:8 ---------------------------------------------------------------------------- +3 | val aAgain = foo(5) // error + | ^ + | Access non-initialized value aAgain. Calling trace: + | -> val aAgain = foo(5) // error [ unsound4.scala:3 ] + | -> def foo(x: Int): A = if (x < 5) then this else foo(x - 1).aAgain [ unsound4.scala:2 ] diff --git a/tests/init/neg/unsound4.scala b/tests/init/neg/unsound4.scala new file mode 100644 index 000000000000..8a6e26fe8a6b --- /dev/null +++ b/tests/init/neg/unsound4.scala @@ -0,0 +1,4 @@ +class A { + def foo(x: Int): A = if (x < 5) then this else foo(x - 1).aAgain + val aAgain = foo(5) // error +} \ No newline at end of file From ef43eb510f1b06adbbedf9aff15b1cecb97ed8c4 Mon Sep 17 00:00:00 2001 From: Fengyun Liu Date: Sat, 4 Sep 2021 07:26:22 +0200 Subject: [PATCH 02/52] Add worklist and refactor definition for ThisRef This makes it closer to the Scala Symposium 21 paper. --- .../tools/dotc/transform/init/Checker.scala | 54 ++--- .../tools/dotc/transform/init/Semantic.scala | 207 ++++++++++++------ 2 files changed, 157 insertions(+), 104 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/transform/init/Checker.scala b/compiler/src/dotty/tools/dotc/transform/init/Checker.scala index d13c4df04547..9f521f4b5f0c 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Checker.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Checker.scala @@ -33,33 +33,38 @@ class Checker extends Phase { override def runOn(units: List[CompilationUnit])(using Context): List[CompilationUnit] = units.foreach { unit => traverser.traverse(unit.tpdTree) } + semantic.check() super.runOn(units) + def run(using Context): Unit = { + // ignore, we already called `semantic.check()` in `runOn` + } + val traverser = new TreeTraverser { override def traverse(tree: Tree)(using Context): Unit = traverseChildren(tree) tree match { - case tdef: MemberDef => + case mdef: MemberDef => // self-type annotation ValDef has no symbol - if tdef.name != nme.WILDCARD then - tdef.symbol.defTree = tree + if mdef.name != nme.WILDCARD then + mdef.symbol.defTree = tree + + mdef match + case tdef: TypeDef if tdef.isClassDef => + import semantic._ + val cls = tdef.symbol.asClass + val ctor = cls.primaryConstructor + val args = ctor.defTree.asInstanceOf[DefDef].termParamss.flatten.map(_ => Hot) + val outer = Hot + val thisRef = ThisRef(cls, outer, ctor, args) + if shouldCheckClass(cls) then semantic.addTask(thisRef)(using Trace.empty) + case _ => + 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 + private def shouldCheckClass(cls: ClassSymbol)(using Context) = { val instantiable: Boolean = cls.is(Flags.Module) || !cls.isOneOf(Flags.AbstractOrTrait) && { @@ -71,21 +76,6 @@ class Checker extends Phase { } // A concrete class may not be instantiated if the self type is not satisfied - if (instantiable && cls.enclosingPackageClass != defn.StdLibPatchesPackage.moduleClass) { - import semantic._ - val tpl = tree.rhs.asInstanceOf[Template] - val thisRef = ThisRef(cls).ensureExists - - val paramValues = tpl.constr.termParamss.flatten.map(param => param.symbol -> Hot).toMap - - given Promoted = Promoted.empty - given Trace = Trace.empty - given Env = Env(paramValues) - - val res = eval(tpl, thisRef, cls) - res.errors.foreach(_.issue) - } - - tree + instantiable && cls.enclosingPackageClass != defn.StdLibPatchesPackage.moduleClass } } diff --git a/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala b/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala index acf5f99aa925..80b19843a1e5 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala @@ -17,6 +17,7 @@ import reporting.trace as log import Errors._ import scala.collection.mutable +import scala.annotation.tailrec class Semantic { import Semantic._ @@ -33,7 +34,7 @@ class Semantic { * │ │ │ │ * | │ │ │ * | │ │ │ - * ThisRef(C) Warm(D) Fun RefSet + * ThisRef Warm Fun RefSet * │ ▲ ▲ ▲ * │ │ │ │ * | │ │ │ @@ -65,19 +66,44 @@ class Semantic { sealed abstract class Addr extends Value { def klass: ClassSymbol def outer: Value + def objekt: Objekt + + /** Update field value of the abstract object + * + * Invariant: fields are immutable and only set once + */ + def updateField(field: Symbol, value: Value)(using Context): Unit = + objekt.updateField(field, value) + + /** Update the immediate outer of the given `klass` of the abstract object + * + * Invariant: outers are immutable and only set once + */ + def updateOuter(klass: ClassSymbol, value: Value)(using Context): Unit = + objekt.updateOuter(klass, value) } - /** A reference to the object under initialization pointed by `this` - */ - case class ThisRef(klass: ClassSymbol) extends Addr { - val outer = Hot + /** A reference to the object under initialization pointed by `this` */ + case class ThisRef(klass: ClassSymbol, outer: Value, ctor: Symbol, args: List[Value]) extends Addr { + /** Caches initialized fields */ + val objekt = Objekt(klass, fields = mutable.Map.empty, outers = mutable.Map(klass -> outer)) } /** An object with all fields initialized but reaches objects under initialization * * We need to restrict nesting levels of `outer` to finitize the domain. */ - case class Warm(klass: ClassSymbol, outer: Value, ctor: Symbol, args: List[Value]) extends Addr + case class Warm(klass: ClassSymbol, outer: Value, args: List[Value]) extends Addr { + val objekt = getCachedObject() + + private def getCachedObject() = + if heap.contains(this) then heap(this) + else { + val obj = Objekt(this.klass, fields = mutable.Map.empty, outers = mutable.Map(this.klass -> this.outer)) + heap.update(this, obj) + obj + } + } /** A function value */ case class Fun(expr: Tree, thisV: Addr, klass: ClassSymbol, env: Env) extends Value @@ -97,48 +123,45 @@ class Semantic { * * Note: Object is NOT a value. */ - case class Objekt(klass: ClassSymbol, fields: mutable.Map[Symbol, Value], outers: mutable.Map[ClassSymbol, Value]) + class Objekt(val klass: ClassSymbol, fields: mutable.Map[Symbol, Value], outers: mutable.Map[ClassSymbol, Value]) { + def field(f: Symbol): Value = fields(f) - /** Abstract heap stores abstract objects - * - * As in the OOPSLA paper, the abstract heap is monotonistic. + def outer(klass: ClassSymbol) = outers(klass) + + def hasOuter(klass: ClassSymbol) = outers.contains(klass) + + def hasField(f: Symbol) = fields.contains(f) + + def updateField(field: Symbol, value: Value)(using Context): Unit = + assert(!fields.contains(field), field.show + " already init, new = " + value + ", old = " + fields(field)) + fields(field) = value + + def updateOuter(klass: ClassSymbol, value: Value)(using Context): Unit = + assert(!outers.contains(klass), klass.show + " already init, new = " + value + ", old = " + outers(klass)) + outers(klass) = value + } + + /** Abstract heap stores abstract warm objects * + * The heap serves as cache of summaries for warm objects and is shared for checking all classes. */ object Heap { - opaque type Heap = mutable.Map[Addr, Objekt] + opaque type Heap = mutable.Map[Warm, Objekt] /** Note: don't use `val` to avoid incorrect sharing */ def empty: Heap = mutable.Map.empty extension (heap: Heap) - def contains(addr: Addr): Boolean = heap.contains(addr) - def apply(addr: Addr): Objekt = heap(addr) - def update(addr: Addr, obj: Objekt): Unit = + def contains(addr: Warm): Boolean = heap.contains(addr) + def apply(addr: Warm): Objekt = heap(addr) + def update(addr: Warm, obj: Objekt): Unit = heap(addr) = obj end extension - - extension (ref: Addr) - /** Update field value of the abstract object - * - * Invariant: fields are immutable and only set once from `init` - */ - def updateField(field: Symbol, value: Value): Contextual[Unit] = - val fields = heap(ref).fields - assert(!fields.contains(field), field.show + " already init, new = " + value + ", ref =" + ref) - fields(field) = value - - /** Update the immediate outer of the given `klass` of the abstract object - * - * Invariant: outers are immutable and only set once from `init` - */ - def updateOuter(klass: ClassSymbol, value: Value): Contextual[Unit] = - heap(ref).outers(klass) = value - end extension } type Heap = Heap.Heap import Heap._ - val heap: Heap = Heap.empty + private val heap: Heap = Heap.empty /** The environment for method parameters * @@ -225,8 +248,8 @@ class Semantic { * maps configuration to evaluation result. * * Thanks to heap monotonicity, heap is not part of the configuration. - * Which also avoid computing fix-point on the cache, as the cache is - * immutable. + * + * This class is only used for the purpose of documentation. */ case class Config(thisV: Value, expr: Tree) @@ -339,9 +362,9 @@ class Semantic { val rhs = target.defTree.asInstanceOf[ValDef].rhs eval(rhs, addr, target.owner.asClass, cacheResult = true) else - val obj = heap(addr) - if obj.fields.contains(target) then - Result(obj.fields(target), Nil) + val obj = addr.objekt + if obj.hasField(target) then + Result(obj.field(target), Nil) else if addr.isInstanceOf[Warm] then if target.is(Flags.ParamAccessor) then // possible for trait parameters @@ -422,9 +445,9 @@ class Semantic { Result(Hot, error :: checkArgs) else // method call resolves to a field - val obj = heap(addr) - if obj.fields.contains(target) then - Result(obj.fields(target), Nil) + val obj = addr.objekt + if obj.hasField(target) then + Result(obj.field(target), Nil) else value.select(target, source, needResolve = false) @@ -458,11 +481,14 @@ class Semantic { else arg.value.widenArg } - if buffer.isEmpty then Result(Hot, Errors.empty) + if buffer.isEmpty then + Result(Hot, Errors.empty) else - val value = Warm(klass, Hot, ctor, args2).ensureExists - val res = value.call(ctor, args, superType = NoType, source) - Result(value, res.errors) + val outer = Hot + val value = Warm(klass, outer, args2) + val task = ThisRef(klass, outer, ctor, args2) + this.addTask(task) + Result(value, Errors.empty) case Cold => val error = CallCold(ctor, source, trace1.toVector) @@ -472,12 +498,14 @@ class Semantic { given Trace = trace1 // widen the outer to finitize addresses val outer = addr match - case Warm(_, _: Warm, _, _) => Cold + case Warm(_, _: Warm, _) => Cold case _ => addr - val value = Warm(klass, outer, ctor, args.map(_.value).widenArgs).ensureExists - val res = value.call(ctor, args, superType = NoType, source) - Result(value, res.errors) + val argsWidened = args.map(_.value).widenArgs + val value = Warm(klass, outer, argsWidened) + val task = ThisRef(klass, outer, ctor, argsWidened) + this.addTask(task) + Result(value, Errors.empty) case Fun(body, thisV, klass, env) => report.error("unexpected tree in instantiating a function, fun = " + body.show, source) @@ -545,13 +573,13 @@ class Semantic { * objects. */ def isFullyFilled: Contextual[Boolean] = log("isFullyFilled " + addr, printer) { - val obj = heap(addr) + val obj = addr.objekt addr.klass.baseClasses.forall { klass => !klass.hasSource || { val nonInits = klass.info.decls.filter { member => !member.isOneOf(Flags.Method | Flags.Lazy | Flags.Deferred) && !member.isType - && !obj.fields.contains(member) + && !obj.hasField(member) } printer.println("nonInits = " + nonInits) nonInits.isEmpty @@ -559,13 +587,6 @@ class Semantic { } } - /** Ensure the corresponding object exists in the heap */ - def ensureExists: addr.type = - if !heap.contains(addr) then - val obj = Objekt(addr.klass, fields = mutable.Map.empty, outers = mutable.Map(addr.klass -> addr.outer)) - heap.update(addr, obj) - addr - end extension extension (thisRef: ThisRef) @@ -680,6 +701,52 @@ class Semantic { cls == defn.AnyValClass || cls == defn.ObjectClass +// ----- Work list --------------------------------------------------- + case class Task(value: ThisRef)(val trace: Trace) + + private class WorkList { + private var checkedTasks: Set[Task] = Set.empty + private var pendingTasks: List[Task] = Nil + + def addTask(task: Task): Unit = + if !checkedTasks.contains(task) then pendingTasks = task :: pendingTasks + + /** Process the worklist until done */ + @tailrec + def work()(using Context): Unit = + pendingTasks match + case task :: rest => + pendingTasks = rest + checkedTasks = checkedTasks + task + doTask(task) + work() + case _ => + + /** Check an individual class + * + * This method should only be called from the work list scheduler. + */ + private def doTask(task: Task)(using Context): Unit = { + val thisRef = task.value + val tpl = thisRef.klass.defTree.asInstanceOf[TypeDef].rhs.asInstanceOf[Template] + + given Promoted = Promoted.empty + given Trace = task.trace + given Env = Env(thisRef.ctor.defTree.asInstanceOf[DefDef], thisRef.args) + + val res = init(tpl, thisRef, thisRef.klass) + res.errors.foreach(_.issue) + } + } + + private val workList = new WorkList + + /** Add a checking task to the work list */ + def addTask(task: ThisRef)(using Trace) = workList.addTask(Task(task)(trace)) + + /** Perform check on the work list until it becomes empty */ + def check()(using Context) = workList.work() + // ----- Semantic definition -------------------------------- /** Utility definition used for better error-reporting of argument errors */ @@ -900,9 +967,6 @@ class Semantic { if tdef.isClassDef then Result(Hot, Errors.empty) else Result(Hot, checkTermUsage(tdef.rhs, thisV, klass)) - case tpl: Template => - init(tpl, thisV, klass) - case _: Import | _: Export => Result(Hot, Errors.empty) @@ -947,15 +1011,17 @@ class Semantic { else thisV match case Hot => Hot - case addr: Addr => - val obj = heap(addr) + case thisRef: ThisRef => + val obj = thisRef.objekt val outerCls = klass.owner.lexicallyEnclosingClass.asClass - if !obj.outers.contains(klass) then + if !obj.hasOuter(klass) then val error = PromoteError("outer not yet initialized, target = " + target + ", klass = " + klass, source, trace.toVector) report.error(error.show + error.stacktrace, source) Hot else - resolveThis(target, obj.outers(klass), outerCls, source) + resolveThis(target, obj.outer(klass), outerCls, source) + case warm: Warm => + ??? case RefSet(refs) => refs.map(ref => resolveThis(target, ref, klass, source)).join case fun: Fun => @@ -979,7 +1045,7 @@ class Semantic { case Hot => Hot case addr: Addr => - val obj = heap(addr) + val obj = addr.objekt val curOpt = obj.klass.baseClasses.find(cls => reachable(cls, hops)) curOpt match case Some(cur) => @@ -1011,7 +1077,7 @@ class Semantic { else cases(tref.prefix, thisV, klass, source) /** Initialize part of an abstract object in `klass` of the inheritance chain */ - def init(tpl: Template, thisV: Addr, klass: ClassSymbol): Contextual[Result] = log("init " + klass.show, printer, (_: Result).show) { + def init(tpl: Template, thisV: ThisRef, klass: ClassSymbol): Contextual[Result] = log("init " + klass.show, printer, (_: Result).show) { val errorBuffer = new mutable.ArrayBuffer[Error] val paramsMap = tpl.constr.termParamss.flatten.map { vdef => @@ -1117,11 +1183,8 @@ class Semantic { case _: MemberDef => case tree => - thisV match - case thisRef: ThisRef => - if fieldsChanged then thisRef.tryPromoteCurrentObject - fieldsChanged = false - case _ => + if fieldsChanged then thisV.tryPromoteCurrentObject + fieldsChanged = false given Env = Env.empty errorBuffer ++= eval(tree, thisV, klass).errors From 5d1fe86d9cfa9f8fe4814be951e6d55761a9c003 Mon Sep 17 00:00:00 2001 From: Fengyun Liu Date: Sat, 4 Sep 2021 08:30:45 +0200 Subject: [PATCH 03/52] Rename Addr to Ref --- .../tools/dotc/transform/init/Semantic.scala | 88 +++++++++---------- 1 file changed, 44 insertions(+), 44 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala b/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala index 80b19843a1e5..a03b18017379 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala @@ -63,7 +63,7 @@ class Semantic { /** An object with unknown initialization status */ case object Cold extends Value - sealed abstract class Addr extends Value { + sealed abstract class Ref extends Value { def klass: ClassSymbol def outer: Value def objekt: Objekt @@ -84,7 +84,7 @@ class Semantic { } /** A reference to the object under initialization pointed by `this` */ - case class ThisRef(klass: ClassSymbol, outer: Value, ctor: Symbol, args: List[Value]) extends Addr { + case class ThisRef(klass: ClassSymbol, outer: Value, ctor: Symbol, args: List[Value]) extends Ref { /** Caches initialized fields */ val objekt = Objekt(klass, fields = mutable.Map.empty, outers = mutable.Map(klass -> outer)) } @@ -93,7 +93,7 @@ class Semantic { * * We need to restrict nesting levels of `outer` to finitize the domain. */ - case class Warm(klass: ClassSymbol, outer: Value, args: List[Value]) extends Addr { + case class Warm(klass: ClassSymbol, outer: Value, args: List[Value]) extends Ref { val objekt = getCachedObject() private def getCachedObject() = @@ -106,13 +106,13 @@ class Semantic { } /** A function value */ - case class Fun(expr: Tree, thisV: Addr, klass: ClassSymbol, env: Env) extends Value + case class Fun(expr: Tree, thisV: Ref, klass: ClassSymbol, env: Env) extends Value /** A value which represents a set of addresses * * It comes from `if` expressions. */ - case class RefSet(refs: List[Fun | Addr]) extends Value + case class RefSet(refs: List[Fun | Ref]) extends Value // end of value definition @@ -152,10 +152,10 @@ class Semantic { def empty: Heap = mutable.Map.empty extension (heap: Heap) - def contains(addr: Warm): Boolean = heap.contains(addr) - def apply(addr: Warm): Objekt = heap(addr) - def update(addr: Warm, obj: Objekt): Unit = - heap(addr) = obj + def contains(ref: Warm): Boolean = heap.contains(ref) + def apply(ref: Warm): Objekt = heap(ref) + def update(ref: Warm, obj: Objekt): Unit = + heap(ref) = obj end extension } type Heap = Heap.Heap @@ -331,7 +331,7 @@ class Semantic { /** Conservatively approximate the value with `Cold` or `Hot` */ def widenArg: Value = a match - case _: Addr | _: Fun => Cold + case _: Ref | _: Fun => Cold case RefSet(refs) => refs.map(_.widenArg).join case _ => a @@ -354,18 +354,18 @@ class Semantic { val error = AccessCold(field, source, trace.toVector) Result(Hot, error :: Nil) - case addr: Addr => - val target = if needResolve then resolve(addr.klass, field) else field + case ref: Ref => + val target = if needResolve then resolve(ref.klass, field) else field val trace1 = trace.add(source) if target.is(Flags.Lazy) then given Trace = trace1 val rhs = target.defTree.asInstanceOf[ValDef].rhs - eval(rhs, addr, target.owner.asClass, cacheResult = true) + eval(rhs, ref, target.owner.asClass, cacheResult = true) else - val obj = addr.objekt + val obj = ref.objekt if obj.hasField(target) then Result(obj.field(target), Nil) - else if addr.isInstanceOf[Warm] then + else if ref.isInstanceOf[Warm] then if target.is(Flags.ParamAccessor) then // possible for trait parameters // see tests/init/neg/trait2.scala @@ -374,7 +374,7 @@ class Semantic { Result(Hot, Nil) else if target.hasSource then val rhs = target.defTree.asInstanceOf[ValOrDefDef].rhs - eval(rhs, addr, target.owner.asClass, cacheResult = true) + eval(rhs, ref, target.owner.asClass, cacheResult = true) else val error = CallUnknown(field, source, trace.toVector) Result(Hot, error :: Nil) @@ -407,15 +407,15 @@ class Semantic { val error = CallCold(meth, source, trace.toVector) Result(Hot, error :: checkArgs) - case addr: Addr => + case ref: Ref => val isLocal = !meth.owner.isClass val target = if !needResolve then meth else if superType.exists then - resolveSuper(addr.klass, superType, meth) + resolveSuper(ref.klass, superType, meth) else - resolve(addr.klass, meth) + resolve(ref.klass, meth) if target.isOneOf(Flags.Method) then val trace1 = trace.add(source) @@ -427,17 +427,17 @@ class Semantic { if target.isPrimaryConstructor then given Env = env2 val tpl = cls.defTree.asInstanceOf[TypeDef].rhs.asInstanceOf[Template] - val res = withTrace(trace.add(cls.defTree)) { eval(tpl, addr, cls, cacheResult = true) } - Result(addr, res.errors) + val res = withTrace(trace.add(cls.defTree)) { eval(tpl, ref, cls, cacheResult = true) } + Result(ref, res.errors) else if target.isConstructor then given Env = env2 - eval(ddef.rhs, addr, cls, cacheResult = true) + eval(ddef.rhs, ref, cls, cacheResult = true) else // normal method call withEnv(if isLocal then env else Env.empty) { - eval(ddef.rhs, addr, cls, cacheResult = true) ++ checkArgs + eval(ddef.rhs, ref, cls, cacheResult = true) ++ checkArgs } - else if addr.canIgnoreMethodCall(target) then + else if ref.canIgnoreMethodCall(target) then Result(Hot, Nil) else // no source code available @@ -445,7 +445,7 @@ class Semantic { Result(Hot, error :: checkArgs) else // method call resolves to a field - val obj = addr.objekt + val obj = ref.objekt if obj.hasField(target) then Result(obj.field(target), Nil) else @@ -494,12 +494,12 @@ class Semantic { val error = CallCold(ctor, source, trace1.toVector) Result(Hot, error :: Nil) - case addr: Addr => + case ref: Ref => given Trace = trace1 // widen the outer to finitize addresses - val outer = addr match + val outer = ref match case Warm(_, _: Warm, _) => Cold - case _ => addr + case _ => ref val argsWidened = args.map(_.value).widenArgs val value = Warm(klass, outer, argsWidened) @@ -549,7 +549,7 @@ class Semantic { case Cold => Result(Cold, Nil) - case addr: Addr => eval(vdef.rhs, addr, klass) + case ref: Ref => eval(vdef.rhs, ref, klass) case _ => report.error("unexpected defTree when accessing local variable, sym = " + sym.show + ", defTree = " + sym.defTree.show, source) @@ -561,7 +561,7 @@ class Semantic { end extension // ----- Promotion ---------------------------------------------------- - extension (addr: Addr) + extension (ref: Ref) /** Whether the object is fully assigned * * It means all fields and outers are set. For performance, we don't check @@ -572,9 +572,9 @@ class Semantic { * object freely, as its fields or outers may still reach uninitialized * objects. */ - def isFullyFilled: Contextual[Boolean] = log("isFullyFilled " + addr, printer) { - val obj = addr.objekt - addr.klass.baseClasses.forall { klass => + def isFullyFilled: Contextual[Boolean] = log("isFullyFilled " + ref, printer) { + val obj = ref.objekt + ref.klass.baseClasses.forall { klass => !klass.hasSource || { val nonInits = klass.info.decls.filter { member => !member.isOneOf(Flags.Method | Flags.Lazy | Flags.Deferred) @@ -690,7 +690,7 @@ class Semantic { end extension // ----- Policies ------------------------------------------------------ - extension (value: Addr) + extension (value: Ref) /** Can the method call on `value` be ignored? * * Note: assume overriding resolution has been performed. @@ -713,7 +713,7 @@ class Semantic { /** Process the worklist until done */ @tailrec - def work()(using Context): Unit = + final def work()(using Context): Unit = pendingTasks match case task :: rest => pendingTasks = rest @@ -769,7 +769,7 @@ class Semantic { * * This method only handles cache logic and delegates the work to `cases`. */ - def eval(expr: Tree, thisV: Addr, klass: ClassSymbol, cacheResult: Boolean = false): Contextual[Result] = log("evaluating " + expr.show + ", this = " + thisV.show, printer, (_: Result).show) { + def eval(expr: Tree, thisV: Ref, klass: ClassSymbol, cacheResult: Boolean = false): Contextual[Result] = log("evaluating " + expr.show + ", this = " + thisV.show, printer, (_: Result).show) { val innerMap = cache.getOrElseUpdate(thisV, new EqHashMap[Tree, Value]) if (innerMap.contains(expr)) Result(innerMap(expr), Errors.empty) else { @@ -785,11 +785,11 @@ class Semantic { } /** Evaluate a list of expressions */ - def eval(exprs: List[Tree], thisV: Addr, klass: ClassSymbol): Contextual[List[Result]] = + def eval(exprs: List[Tree], thisV: Ref, klass: ClassSymbol): Contextual[List[Result]] = exprs.map { expr => eval(expr, thisV, klass) } /** Evaluate arguments of methods */ - def evalArgs(args: List[Arg], thisV: Addr, klass: ClassSymbol): Contextual[(List[Error], List[ArgInfo])] = + def evalArgs(args: List[Arg], thisV: Ref, klass: ClassSymbol): Contextual[(List[Error], List[ArgInfo])] = val errors = new mutable.ArrayBuffer[Error] val argInfos = new mutable.ArrayBuffer[ArgInfo] args.foreach { arg => @@ -809,7 +809,7 @@ class Semantic { * * Note: Recursive call should go to `eval` instead of `cases`. */ - def cases(expr: Tree, thisV: Addr, klass: ClassSymbol): Contextual[Result] = + def cases(expr: Tree, thisV: Ref, klass: ClassSymbol): Contextual[Result] = expr match { case Ident(nme.WILDCARD) => // TODO: disallow `var x: T = _` @@ -975,7 +975,7 @@ class Semantic { } /** Handle semantics of leaf nodes */ - def cases(tp: Type, thisV: Addr, klass: ClassSymbol, source: Tree): Contextual[Result] = log("evaluating " + tp.show, printer, (_: Result).show) { + def cases(tp: Type, thisV: Ref, klass: ClassSymbol, source: Tree): Contextual[Result] = log("evaluating " + tp.show, printer, (_: Result).show) { tp match { case _: ConstantType => Result(Hot, Errors.empty) @@ -1044,8 +1044,8 @@ class Semantic { thisV match case Hot => Hot - case addr: Addr => - val obj = addr.objekt + case ref: Ref => + val obj = ref.objekt val curOpt = obj.klass.baseClasses.find(cls => reachable(cls, hops)) curOpt match case Some(cur) => @@ -1066,7 +1066,7 @@ class Semantic { } /** Compute the outer value that correspond to `tref.prefix` */ - def outerValue(tref: TypeRef, thisV: Addr, klass: ClassSymbol, source: Tree): Contextual[Result] = + def outerValue(tref: TypeRef, thisV: Ref, klass: ClassSymbol, source: Tree): Contextual[Result] = val cls = tref.classSymbol.asClass if tref.prefix == NoPrefix then val enclosing = cls.owner.lexicallyEnclosingClass.asClass @@ -1197,7 +1197,7 @@ class Semantic { * * This is intended to avoid type soundness issues in Dotty. */ - def checkTermUsage(tpt: Tree, thisV: Addr, klass: ClassSymbol): Contextual[List[Error]] = + def checkTermUsage(tpt: Tree, thisV: Ref, klass: ClassSymbol): Contextual[List[Error]] = val buf = new mutable.ArrayBuffer[Error] val traverser = new TypeTraverser { def traverse(tp: Type): Unit = tp match { From 7279f73a87ce806941027976d868c56c803ceca7 Mon Sep 17 00:00:00 2001 From: Fengyun Liu Date: Sun, 5 Sep 2021 09:36:28 +0200 Subject: [PATCH 04/52] Refactor: make state explicit --- .../tools/dotc/transform/init/Checker.scala | 23 +++---- .../tools/dotc/transform/init/Semantic.scala | 60 ++++++++++++------- 2 files changed, 52 insertions(+), 31 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/transform/init/Checker.scala b/compiler/src/dotty/tools/dotc/transform/init/Checker.scala index 9f521f4b5f0c..a63e3296b8dc 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Checker.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Checker.scala @@ -5,6 +5,7 @@ package init import dotty.tools.dotc._ import ast.tpd +import tpd._ import dotty.tools.dotc.core._ import Contexts._ @@ -15,32 +16,32 @@ import StdNames._ import dotty.tools.dotc.transform._ import Phases._ - import scala.collection.mutable +import Semantic._ class Checker extends Phase { - import tpd._ val phaseName = "initChecker" - private val semantic = new Semantic - override val runsAfter = Set(Pickler.name) override def isEnabled(using Context): Boolean = super.isEnabled && ctx.settings.YcheckInit.value override def runOn(units: List[CompilationUnit])(using Context): List[CompilationUnit] = - units.foreach { unit => traverser.traverse(unit.tpdTree) } - semantic.check() - super.runOn(units) + Semantic.withInitialState { + val traverser = new InitTreeTraverser() + units.foreach { unit => traverser.traverse(unit.tpdTree) } + Semantic.check() + super.runOn(units) + } def run(using Context): Unit = { - // ignore, we already called `semantic.check()` in `runOn` + // ignore, we already called `Semantic.check()` in `runOn` } - val traverser = new TreeTraverser { + class InitTreeTraverser(using State) extends TreeTraverser { override def traverse(tree: Tree)(using Context): Unit = traverseChildren(tree) tree match { @@ -51,13 +52,13 @@ class Checker extends Phase { mdef match case tdef: TypeDef if tdef.isClassDef => - import semantic._ val cls = tdef.symbol.asClass val ctor = cls.primaryConstructor val args = ctor.defTree.asInstanceOf[DefDef].termParamss.flatten.map(_ => Hot) val outer = Hot val thisRef = ThisRef(cls, outer, ctor, args) - if shouldCheckClass(cls) then semantic.addTask(thisRef)(using Trace.empty) + given Trace = Trace.empty + if shouldCheckClass(cls) then Semantic.addTask(thisRef) case _ => case _ => diff --git a/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala b/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala index a03b18017379..b9960cea2cd7 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala @@ -19,8 +19,7 @@ import Errors._ import scala.collection.mutable import scala.annotation.tailrec -class Semantic { - import Semantic._ +object Semantic { // ----- Domain definitions -------------------------------- @@ -93,10 +92,10 @@ class Semantic { * * We need to restrict nesting levels of `outer` to finitize the domain. */ - case class Warm(klass: ClassSymbol, outer: Value, args: List[Value]) extends Ref { + case class Warm(klass: ClassSymbol, outer: Value, args: List[Value])(using Heap) extends Ref { val objekt = getCachedObject() - private def getCachedObject() = + private def getCachedObject()(using Heap) = if heap.contains(this) then heap(this) else { val obj = Objekt(this.klass, fields = mutable.Map.empty, outers = mutable.Map(this.klass -> this.outer)) @@ -149,7 +148,7 @@ class Semantic { opaque type Heap = mutable.Map[Warm, Objekt] /** Note: don't use `val` to avoid incorrect sharing */ - def empty: Heap = mutable.Map.empty + private[Semantic] def empty: Heap = mutable.Map.empty extension (heap: Heap) def contains(ref: Warm): Boolean = heap.contains(ref) @@ -160,8 +159,9 @@ class Semantic { } type Heap = Heap.Heap + inline def heap(using h: Heap): Heap = h + import Heap._ - private val heap: Heap = Heap.empty /** The environment for method parameters * @@ -205,7 +205,7 @@ class Semantic { } type Env = Env.Env - def env(using env: Env) = env + inline def env(using env: Env) = env inline def withEnv[T](env: Env)(op: Env ?=> T): T = op(using env) import Env._ @@ -233,7 +233,7 @@ class Semantic { type Promoted = Promoted.Promoted import Promoted._ - def promoted(using p: Promoted): Promoted = p + inline def promoted(using p: Promoted): Promoted = p /** Interpreter configuration * @@ -290,8 +290,15 @@ class Semantic { value.instantiate(klass, ctor, args, source) ++ errors } +// ----- State -------------------------------------------- + /** Global state of the checker */ + class State(val heap: Heap, val workList: WorkList) + + given (using s: State): Heap = s.heap + given (using s: State): WorkList = s.workList + /** The state that threads through the interpreter */ - type Contextual[T] = (Env, Context, Trace, Promoted) ?=> T + type Contextual[T] = (Env, Context, Trace, Promoted, State) ?=> T // ----- Error Handling ----------------------------------- @@ -704,7 +711,7 @@ class Semantic { // ----- Work list --------------------------------------------------- case class Task(value: ThisRef)(val trace: Trace) - private class WorkList { + class WorkList private[Semantic]() { private var checkedTasks: Set[Task] = Set.empty private var pendingTasks: List[Task] = Nil @@ -713,7 +720,7 @@ class Semantic { /** Process the worklist until done */ @tailrec - final def work()(using Context): Unit = + final def work()(using State, Context): Unit = pendingTasks match case task :: rest => pendingTasks = rest @@ -726,7 +733,7 @@ class Semantic { * * This method should only be called from the work list scheduler. */ - private def doTask(task: Task)(using Context): Unit = { + private def doTask(task: Task)(using State, Context): Unit = { val thisRef = task.value val tpl = thisRef.klass.defTree.asInstanceOf[TypeDef].rhs.asInstanceOf[Template] @@ -738,14 +745,31 @@ class Semantic { res.errors.foreach(_.issue) } } + inline def workList(using wl: WorkList): WorkList = wl - private val workList = new WorkList +// ----- API -------------------------------- /** Add a checking task to the work list */ - def addTask(task: ThisRef)(using Trace) = workList.addTask(Task(task)(trace)) + def addTask(task: ThisRef)(using WorkList, Trace) = workList.addTask(Task(task)(trace)) + + /** Perform check on the work list until it becomes empty + * + * Should only be called once from the checker. + */ + def check()(using State, Context) = workList.work() - /** Perform check on the work list until it becomes empty */ - def check()(using Context) = workList.work() + /** Perform actions with initial checking state. + * + * Semantic.withInitialState { + * Semantic.addTask(...) + * ... + * Semantic.check() + * } + */ + def withInitialState[T](work: State ?=> T): T = { + val initialState = State(Heap.empty, new WorkList) + work(using initialState) + } // ----- Semantic definition -------------------------------- @@ -1210,10 +1234,6 @@ class Semantic { traverser.traverse(tpt.tpe) buf.toList -} - -object Semantic { - // ----- Utility methods and extractors -------------------------------- def typeRefOf(tp: Type)(using Context): TypeRef = tp.dealias.typeConstructor match { From 20ff7ca795f1699285e46c75e1da54909fbf4851 Mon Sep 17 00:00:00 2001 From: Fengyun Liu Date: Sun, 5 Sep 2021 18:04:37 +0200 Subject: [PATCH 05/52] Add constructor to Warm --- .../src/dotty/tools/dotc/transform/init/Semantic.scala | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala b/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala index b9960cea2cd7..e615fadc5d47 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala @@ -92,7 +92,7 @@ object Semantic { * * We need to restrict nesting levels of `outer` to finitize the domain. */ - case class Warm(klass: ClassSymbol, outer: Value, args: List[Value])(using Heap) extends Ref { + case class Warm(klass: ClassSymbol, outer: Value, ctor: Symbol, args: List[Value])(using Heap) extends Ref { val objekt = getCachedObject() private def getCachedObject()(using Heap) = @@ -492,7 +492,7 @@ object Semantic { Result(Hot, Errors.empty) else val outer = Hot - val value = Warm(klass, outer, args2) + val value = Warm(klass, outer, ctor, args2) val task = ThisRef(klass, outer, ctor, args2) this.addTask(task) Result(value, Errors.empty) @@ -505,11 +505,11 @@ object Semantic { given Trace = trace1 // widen the outer to finitize addresses val outer = ref match - case Warm(_, _: Warm, _) => Cold + case Warm(_, _: Warm, _, _) => Cold case _ => ref val argsWidened = args.map(_.value).widenArgs - val value = Warm(klass, outer, argsWidened) + val value = Warm(klass, outer, ctor, argsWidened) val task = ThisRef(klass, outer, ctor, argsWidened) this.addTask(task) Result(value, Errors.empty) @@ -1192,6 +1192,7 @@ object Semantic { // initialize super classes after outers are set tasks.foreach(task => task()) + end if var fieldsChanged = true From f5651b56584f548f82119a4bcac234994b3e0bb8 Mon Sep 17 00:00:00 2001 From: Fengyun Liu Date: Sun, 5 Sep 2021 23:35:33 +0200 Subject: [PATCH 06/52] Revert ThisRef to previous version --- .../dotty/tools/dotc/transform/init/Checker.scala | 5 +---- .../tools/dotc/transform/init/Semantic.scala | 15 +++++++++------ 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/transform/init/Checker.scala b/compiler/src/dotty/tools/dotc/transform/init/Checker.scala index a63e3296b8dc..772ea44a1cca 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Checker.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Checker.scala @@ -53,10 +53,7 @@ class Checker extends Phase { mdef match case tdef: TypeDef if tdef.isClassDef => val cls = tdef.symbol.asClass - val ctor = cls.primaryConstructor - val args = ctor.defTree.asInstanceOf[DefDef].termParamss.flatten.map(_ => Hot) - val outer = Hot - val thisRef = ThisRef(cls, outer, ctor, args) + val thisRef = ThisRef(cls) given Trace = Trace.empty if shouldCheckClass(cls) then Semantic.addTask(thisRef) case _ => diff --git a/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala b/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala index e615fadc5d47..6d6671831204 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala @@ -83,7 +83,8 @@ object Semantic { } /** A reference to the object under initialization pointed by `this` */ - case class ThisRef(klass: ClassSymbol, outer: Value, ctor: Symbol, args: List[Value]) extends Ref { + case class ThisRef(klass: ClassSymbol) extends Ref { + val outer = Hot /** Caches initialized fields */ val objekt = Objekt(klass, fields = mutable.Map.empty, outers = mutable.Map(klass -> outer)) } @@ -709,7 +710,7 @@ object Semantic { cls == defn.ObjectClass // ----- Work list --------------------------------------------------- - case class Task(value: ThisRef)(val trace: Trace) + type Task = ThisRef class WorkList private[Semantic]() { private var checkedTasks: Set[Task] = Set.empty @@ -734,12 +735,14 @@ object Semantic { * This method should only be called from the work list scheduler. */ private def doTask(task: Task)(using State, Context): Unit = { - val thisRef = task.value + val thisRef = task val tpl = thisRef.klass.defTree.asInstanceOf[TypeDef].rhs.asInstanceOf[Template] + val paramValues = tpl.constr.termParamss.flatten.map(param => param.symbol -> Hot).toMap + given Promoted = Promoted.empty - given Trace = task.trace - given Env = Env(thisRef.ctor.defTree.asInstanceOf[DefDef], thisRef.args) + given Trace = Trace.empty + given Env = Env(paramValues) val res = init(tpl, thisRef, thisRef.klass) res.errors.foreach(_.issue) @@ -750,7 +753,7 @@ object Semantic { // ----- API -------------------------------- /** Add a checking task to the work list */ - def addTask(task: ThisRef)(using WorkList, Trace) = workList.addTask(Task(task)(trace)) + def addTask(task: ThisRef)(using WorkList) = workList.addTask(task) /** Perform check on the work list until it becomes empty * From 676328570d89727bec3e69b6216f00a409b1caf7 Mon Sep 17 00:00:00 2001 From: Fengyun Liu Date: Sun, 5 Sep 2021 23:35:49 +0200 Subject: [PATCH 07/52] Refactor constructor call --- .../tools/dotc/transform/init/Semantic.scala | 66 ++++++++++++------- 1 file changed, 44 insertions(+), 22 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala b/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala index 6d6671831204..11930e38f086 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala @@ -432,19 +432,10 @@ object Semantic { val cls = target.owner.enclosingClass.asClass val ddef = target.defTree.asInstanceOf[DefDef] val env2 = Env(ddef, args.map(_.value).widenArgs) - if target.isPrimaryConstructor then - given Env = env2 - val tpl = cls.defTree.asInstanceOf[TypeDef].rhs.asInstanceOf[Template] - val res = withTrace(trace.add(cls.defTree)) { eval(tpl, ref, cls, cacheResult = true) } - Result(ref, res.errors) - else if target.isConstructor then - given Env = env2 - eval(ddef.rhs, ref, cls, cacheResult = true) - else - // normal method call - withEnv(if isLocal then env else Env.empty) { - eval(ddef.rhs, ref, cls, cacheResult = true) ++ checkArgs - } + // normal method call + withEnv(if isLocal then env else Env.empty) { + eval(ddef.rhs, ref, cls, cacheResult = true) ++ checkArgs + } else if ref.canIgnoreMethodCall(target) then Result(Hot, Nil) else @@ -475,6 +466,37 @@ object Semantic { } } + def callConstructor(ctor: Symbol, args: List[ArgInfo], source: Tree): Contextual[Result] = log("call " + ctor.show + ", args = " + args, printer, res => res.asInstanceOf[Result].show) { + value match { + case Hot | Cold | _: RefSet | _: Fun => + report.error("unexpected constructor call, meth = " + ctor + ", value = " + value, source) + Result(Hot, Nil) + + case ref: Ref => + val trace1 = trace.add(source) + if ctor.hasSource then + given Trace = trace1 + val cls = ctor.owner.enclosingClass.asClass + val ddef = ctor.defTree.asInstanceOf[DefDef] + val env2 = Env(ddef, args.map(_.value).widenArgs) + if ctor.isPrimaryConstructor then + given Env = env2 + val tpl = cls.defTree.asInstanceOf[TypeDef].rhs.asInstanceOf[Template] + val res = withTrace(trace.add(cls.defTree)) { eval(tpl, ref, cls, cacheResult = true) } + Result(ref, res.errors) + else + given Env = env2 + eval(ddef.rhs, ref, cls, cacheResult = true) + else if ref.canIgnoreMethodCall(ctor) then + Result(Hot, Nil) + else + // no source code available + val error = CallUnknown(ctor, source, trace.toVector) + Result(Hot, error :: Nil) + } + + } + /** Handle a new expression `new p.C` where `p` is abstracted by `value` */ def instantiate(klass: ClassSymbol, ctor: Symbol, args: List[ArgInfo], source: Tree): Contextual[Result] = log("instantiating " + klass.show + ", value = " + value + ", args = " + args, printer, (_: Result).show) { val trace1 = trace.add(source) @@ -493,10 +515,10 @@ object Semantic { Result(Hot, Errors.empty) else val outer = Hot - val value = Warm(klass, outer, ctor, args2) - val task = ThisRef(klass, outer, ctor, args2) - this.addTask(task) - Result(value, Errors.empty) + val warm = Warm(klass, outer, ctor, args2) + val argInfos2 = args.zip(args2).map { (argInfo, v) => argInfo.copy(value = v) } + val res = warm.callConstructor(ctor, argInfos2, source) + Result(warm, res.errors) case Cold => val error = CallCold(ctor, source, trace1.toVector) @@ -510,10 +532,10 @@ object Semantic { case _ => ref val argsWidened = args.map(_.value).widenArgs - val value = Warm(klass, outer, ctor, argsWidened) - val task = ThisRef(klass, outer, ctor, argsWidened) - this.addTask(task) - Result(value, Errors.empty) + val argInfos2 = args.zip(argsWidened).map { (argInfo, v) => argInfo.copy(value = v) } + val warm = Warm(klass, outer, ctor, argsWidened) + val res = warm.callConstructor(ctor, argInfos2, source) + Result(warm, res.errors) case Fun(body, thisV, klass, env) => report.error("unexpected tree in instantiating a function, fun = " + body.show, source) @@ -1132,7 +1154,7 @@ object Semantic { if cls.hasSource then tasks.append { () => printer.println("init super class " + cls.show) - val res2 = thisV.call(ctor, args, superType = NoType, source) + val res2 = thisV.callConstructor(ctor, args, source) errorBuffer ++= res2.errors () } From ab09fd2334e4142ebb5f1bdf827b255cdb6f5b90 Mon Sep 17 00:00:00 2001 From: Fengyun Liu Date: Mon, 6 Sep 2021 00:51:09 +0200 Subject: [PATCH 08/52] Make cache part of state --- .../tools/dotc/transform/init/Semantic.scala | 64 +++++++++++++------ 1 file changed, 43 insertions(+), 21 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala b/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala index 11930e38f086..d27bfe4f4caa 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala @@ -267,8 +267,31 @@ object Semantic { * statement body. Macros may also create incorrect locations. * */ - type Cache = mutable.Map[Value, EqHashMap[Tree, Value]] - val cache: Cache = mutable.Map.empty[Value, EqHashMap[Tree, Value]] + + class Cache(val in: Cache.CacheIn, var out: Cache.CacheOut) { + var changed: Boolean = false + } + + object Cache { + opaque type CacheIn = mutable.Map[Value, EqHashMap[Tree, Value]] + opaque type CacheOut = mutable.Map[Value, EqHashMap[Tree, Value]] + + val empty: Cache = new Cache(mutable.Map.empty, mutable.Map.empty) + + extension (cache: CacheIn | CacheOut) + def contains(value: Value, expr: Tree): Boolean = cache.contains(value) && cache(value).contains(expr) + def get(value: Value, expr: Tree): Value = cache(value)(expr) + def put(value: Value, expr: Tree, result: Value): Unit = { + val innerMap = cache.getOrElseUpdate(value, new EqHashMap[Tree, Value]) + innerMap(expr) = result + } + end extension + } + + import Cache._ + + inline def cache(using c: Cache): Cache = c + /** Result of abstract interpretation */ case class Result(value: Value, errors: Seq[Error]) { @@ -293,9 +316,10 @@ object Semantic { // ----- State -------------------------------------------- /** Global state of the checker */ - class State(val heap: Heap, val workList: WorkList) + class State(val cache: Cache, val heap: Heap, val workList: WorkList) given (using s: State): Heap = s.heap + given (using s: State): Cache = s.cache given (using s: State): WorkList = s.workList /** The state that threads through the interpreter */ @@ -368,7 +392,7 @@ object Semantic { if target.is(Flags.Lazy) then given Trace = trace1 val rhs = target.defTree.asInstanceOf[ValDef].rhs - eval(rhs, ref, target.owner.asClass, cacheResult = true) + eval(rhs, ref, target.owner.asClass) else val obj = ref.objekt if obj.hasField(target) then @@ -382,7 +406,7 @@ object Semantic { Result(Hot, Nil) else if target.hasSource then val rhs = target.defTree.asInstanceOf[ValOrDefDef].rhs - eval(rhs, ref, target.owner.asClass, cacheResult = true) + eval(rhs, ref, target.owner.asClass) else val error = CallUnknown(field, source, trace.toVector) Result(Hot, error :: Nil) @@ -434,7 +458,7 @@ object Semantic { val env2 = Env(ddef, args.map(_.value).widenArgs) // normal method call withEnv(if isLocal then env else Env.empty) { - eval(ddef.rhs, ref, cls, cacheResult = true) ++ checkArgs + eval(ddef.rhs, ref, cls) ++ checkArgs } else if ref.canIgnoreMethodCall(target) then Result(Hot, Nil) @@ -455,7 +479,7 @@ object Semantic { if meth.name.toString == "tupled" then Result(value, Nil) // a call like `fun.tupled` else withEnv(env) { - eval(body, thisV, klass, cacheResult = true) ++ checkArgs + eval(body, thisV, klass) ++ checkArgs } case RefSet(refs) => @@ -482,11 +506,11 @@ object Semantic { if ctor.isPrimaryConstructor then given Env = env2 val tpl = cls.defTree.asInstanceOf[TypeDef].rhs.asInstanceOf[Template] - val res = withTrace(trace.add(cls.defTree)) { eval(tpl, ref, cls, cacheResult = true) } + val res = withTrace(trace.add(cls.defTree)) { eval(tpl, ref, cls) } Result(ref, res.errors) else given Env = env2 - eval(ddef.rhs, ref, cls, cacheResult = true) + eval(ddef.rhs, ref, cls) else if ref.canIgnoreMethodCall(ctor) then Result(Hot, Nil) else @@ -792,7 +816,7 @@ object Semantic { * } */ def withInitialState[T](work: State ?=> T): T = { - val initialState = State(Heap.empty, new WorkList) + val initialState = State(Cache.empty, Heap.empty, new WorkList) work(using initialState) } @@ -818,17 +842,15 @@ object Semantic { * * This method only handles cache logic and delegates the work to `cases`. */ - def eval(expr: Tree, thisV: Ref, klass: ClassSymbol, cacheResult: Boolean = false): Contextual[Result] = log("evaluating " + expr.show + ", this = " + thisV.show, printer, (_: Result).show) { - val innerMap = cache.getOrElseUpdate(thisV, new EqHashMap[Tree, Value]) - if (innerMap.contains(expr)) Result(innerMap(expr), Errors.empty) + def eval(expr: Tree, thisV: Ref, klass: ClassSymbol): Contextual[Result] = log("evaluating " + expr.show + ", this = " + thisV.show, printer, (_: Result).show) { + if (cache.out.contains(thisV, expr)) Result(cache.out.get(thisV, expr), Errors.empty) else { - // no need to compute fix-point, because - // 1. the result is decided by `cfg` for a legal program - // (heap change is irrelevant thanks to monotonicity) - // 2. errors will have been reported for an illegal program - innerMap(expr) = Hot + val assumeValue = if (cache.in.contains(thisV, expr)) cache.in.get(thisV, expr) else Hot + cache.out.put(thisV, expr, assumeValue) val res = cases(expr, thisV, klass) - if cacheResult then innerMap(expr) = res.value else innerMap.remove(expr) + if res.value != assumeValue then + cache.changed = true + cache.out.put(thisV, expr, res.value) res } } @@ -1005,7 +1027,7 @@ object Semantic { case vdef : ValDef => // local val definition // TODO: support explicit @cold annotation for local definitions - eval(vdef.rhs, thisV, klass, cacheResult = true) + eval(vdef.rhs, thisV, klass) case ddef : DefDef => // local method @@ -1225,7 +1247,7 @@ object Semantic { tpl.body.foreach { case vdef : ValDef if !vdef.symbol.is(Flags.Lazy) && !vdef.rhs.isEmpty => given Env = Env.empty - val res = eval(vdef.rhs, thisV, klass, cacheResult = true) + val res = eval(vdef.rhs, thisV, klass) errorBuffer ++= res.errors thisV.updateField(vdef.symbol, res.value) fieldsChanged = true From 98f9f7e2a336bda249b911dc9c6d8bc1b13e7afa Mon Sep 17 00:00:00 2001 From: Fengyun Liu Date: Mon, 6 Sep 2021 01:08:40 +0200 Subject: [PATCH 09/52] Handle cache in work list --- .../tools/dotc/transform/init/Semantic.scala | 29 +++++++++++++++---- 1 file changed, 23 insertions(+), 6 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala b/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala index d27bfe4f4caa..4b7bcc81174f 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala @@ -268,14 +268,25 @@ object Semantic { * */ - class Cache(val in: Cache.CacheIn, var out: Cache.CacheOut) { - var changed: Boolean = false - } - object Cache { opaque type CacheIn = mutable.Map[Value, EqHashMap[Tree, Value]] opaque type CacheOut = mutable.Map[Value, EqHashMap[Tree, Value]] + class Cache(val in: CacheIn, var out: CacheOut) { + var changed: Boolean = false + + /** Copy out to in and reset out to empty */ + def update() = { + out.foreach { (v, m) => + m.iterator.foreach { (e, res) => + in.put(v, e, res) + } + } + + out = mutable.Map.empty + } + } + val empty: Cache = new Cache(mutable.Map.empty, mutable.Map.empty) extension (cache: CacheIn | CacheOut) @@ -770,9 +781,15 @@ object Semantic { final def work()(using State, Context): Unit = pendingTasks match case task :: rest => - pendingTasks = rest - checkedTasks = checkedTasks + task doTask(task) + + if cache.changed then + // discard heap changes and copy cache.out to cache.in + cache.update() + else + pendingTasks = rest + checkedTasks = checkedTasks + task + work() case _ => From fb0c347ff48f7fcaef97af609a5fba56d958234d Mon Sep 17 00:00:00 2001 From: Fengyun Liu Date: Mon, 6 Sep 2021 01:12:32 +0200 Subject: [PATCH 10/52] Don't iterate if errors found --- .../tools/dotc/transform/init/Semantic.scala | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala b/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala index 4b7bcc81174f..afd7e1a65a50 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala @@ -781,14 +781,15 @@ object Semantic { final def work()(using State, Context): Unit = pendingTasks match case task :: rest => - doTask(task) + val res = doTask(task) + res.errors.foreach(_.issue) - if cache.changed then - // discard heap changes and copy cache.out to cache.in - cache.update() - else + if res.errors.nonEmpty then pendingTasks = rest checkedTasks = checkedTasks + task + else + // discard heap changes and copy cache.out to cache.in + cache.update() work() case _ => @@ -797,7 +798,7 @@ object Semantic { * * This method should only be called from the work list scheduler. */ - private def doTask(task: Task)(using State, Context): Unit = { + private def doTask(task: Task)(using State, Context): Result = { val thisRef = task val tpl = thisRef.klass.defTree.asInstanceOf[TypeDef].rhs.asInstanceOf[Template] @@ -807,8 +808,7 @@ object Semantic { given Trace = Trace.empty given Env = Env(paramValues) - val res = init(tpl, thisRef, thisRef.klass) - res.errors.foreach(_.issue) + init(tpl, thisRef, thisRef.klass) } } inline def workList(using wl: WorkList): WorkList = wl From 69e314e4136d6a703f071ca81dfcccebdd0c83e7 Mon Sep 17 00:00:00 2001 From: Fengyun Liu Date: Mon, 6 Sep 2021 01:31:40 +0200 Subject: [PATCH 11/52] Revert heap changes if cache has changed --- .../tools/dotc/transform/init/Semantic.scala | 73 ++++++++++--------- 1 file changed, 38 insertions(+), 35 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala b/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala index afd7e1a65a50..8623a2cc0790 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala @@ -65,28 +65,43 @@ object Semantic { sealed abstract class Ref extends Value { def klass: ClassSymbol def outer: Value - def objekt: Objekt + def objekt(using Heap): Objekt = heap(this) + + def ensureObjectExists()(using Heap) = + if heap.contains(this) then heap(this) + else { + val obj = Objekt(this.klass, fields = Map.empty, outers = Map(this.klass -> this.outer)) + heap.update(this, obj) + obj + } + /** Update field value of the abstract object * * Invariant: fields are immutable and only set once */ - def updateField(field: Symbol, value: Value)(using Context): Unit = - objekt.updateField(field, value) + def updateField(field: Symbol, value: Value)(using Heap, Context): Unit = + val obj = objekt + assert(!obj.hasField(field), field.show + " already init, new = " + value + ", old = " + obj.field(field)) + val obj2 = obj.copy(fields = obj.fields.updated(field, value)) + heap.update(this, obj2) /** Update the immediate outer of the given `klass` of the abstract object * * Invariant: outers are immutable and only set once */ - def updateOuter(klass: ClassSymbol, value: Value)(using Context): Unit = - objekt.updateOuter(klass, value) + def updateOuter(klass: ClassSymbol, value: Value)(using Heap, Context): Unit = + val obj = objekt + assert(!obj.hasOuter(klass), klass.show + " already init, new = " + value + ", old = " + obj.outer(klass)) + val obj2 = obj.copy(outers = obj.outers.updated(klass, value)) + heap.update(this, obj2) } /** A reference to the object under initialization pointed by `this` */ - case class ThisRef(klass: ClassSymbol) extends Ref { + case class ThisRef(klass: ClassSymbol)(using Heap) extends Ref { val outer = Hot - /** Caches initialized fields */ - val objekt = Objekt(klass, fields = mutable.Map.empty, outers = mutable.Map(klass -> outer)) + + ensureObjectExists() } /** An object with all fields initialized but reaches objects under initialization @@ -94,15 +109,7 @@ object Semantic { * We need to restrict nesting levels of `outer` to finitize the domain. */ case class Warm(klass: ClassSymbol, outer: Value, ctor: Symbol, args: List[Value])(using Heap) extends Ref { - val objekt = getCachedObject() - - private def getCachedObject()(using Heap) = - if heap.contains(this) then heap(this) - else { - val obj = Objekt(this.klass, fields = mutable.Map.empty, outers = mutable.Map(this.klass -> this.outer)) - heap.update(this, obj) - obj - } + ensureObjectExists() } /** A function value */ @@ -123,7 +130,7 @@ object Semantic { * * Note: Object is NOT a value. */ - class Objekt(val klass: ClassSymbol, fields: mutable.Map[Symbol, Value], outers: mutable.Map[ClassSymbol, Value]) { + case class Objekt(val klass: ClassSymbol, val fields: Map[Symbol, Value], val outers: Map[ClassSymbol, Value]) { def field(f: Symbol): Value = fields(f) def outer(klass: ClassSymbol) = outers(klass) @@ -131,14 +138,6 @@ object Semantic { def hasOuter(klass: ClassSymbol) = outers.contains(klass) def hasField(f: Symbol) = fields.contains(f) - - def updateField(field: Symbol, value: Value)(using Context): Unit = - assert(!fields.contains(field), field.show + " already init, new = " + value + ", old = " + fields(field)) - fields(field) = value - - def updateOuter(klass: ClassSymbol, value: Value)(using Context): Unit = - assert(!outers.contains(klass), klass.show + " already init, new = " + value + ", old = " + outers(klass)) - outers(klass) = value } /** Abstract heap stores abstract warm objects @@ -146,17 +145,18 @@ object Semantic { * The heap serves as cache of summaries for warm objects and is shared for checking all classes. */ object Heap { - opaque type Heap = mutable.Map[Warm, Objekt] + class Heap(private var map: Map[Ref, Objekt]) { + def contains(ref: Ref): Boolean = map.contains(ref) + def apply(ref: Ref): Objekt = map(ref) + def update(ref: Ref, obj: Objekt): Unit = + map = map.updated(ref, obj) + + def snapshot(): Heap = new Heap(map) + def restore(h: Heap) = this.map = h.map + } /** Note: don't use `val` to avoid incorrect sharing */ - private[Semantic] def empty: Heap = mutable.Map.empty - - extension (heap: Heap) - def contains(ref: Warm): Boolean = heap.contains(ref) - def apply(ref: Warm): Objekt = heap(ref) - def update(ref: Warm, obj: Objekt): Unit = - heap(ref) = obj - end extension + private[Semantic] def empty: Heap = new Heap(Map.empty) } type Heap = Heap.Heap @@ -784,12 +784,15 @@ object Semantic { val res = doTask(task) res.errors.foreach(_.issue) + val heapBefore = heap.snapshot() + if res.errors.nonEmpty then pendingTasks = rest checkedTasks = checkedTasks + task else // discard heap changes and copy cache.out to cache.in cache.update() + heap.restore(heapBefore) work() case _ => From db11295e5ea21d894f613bf8121b9afabd47a070 Mon Sep 17 00:00:00 2001 From: Fengyun Liu Date: Mon, 6 Sep 2021 01:43:49 +0200 Subject: [PATCH 12/52] Restore cacheResult option for eval --- .../tools/dotc/transform/init/Semantic.scala | 52 ++++++++++--------- 1 file changed, 28 insertions(+), 24 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala b/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala index 8623a2cc0790..949901009984 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala @@ -82,7 +82,7 @@ object Semantic { */ def updateField(field: Symbol, value: Value)(using Heap, Context): Unit = val obj = objekt - assert(!obj.hasField(field), field.show + " already init, new = " + value + ", old = " + obj.field(field)) + assert(!obj.hasField(field), field.show + " already init, new = " + value + ", old = " + obj.field(field) + ", ref = " + this) val obj2 = obj.copy(fields = obj.fields.updated(field, value)) heap.update(this, obj2) @@ -92,7 +92,7 @@ object Semantic { */ def updateOuter(klass: ClassSymbol, value: Value)(using Heap, Context): Unit = val obj = objekt - assert(!obj.hasOuter(klass), klass.show + " already init, new = " + value + ", old = " + obj.outer(klass)) + assert(!obj.hasOuter(klass), klass.show + " already has outer, new = " + value + ", old = " + obj.outer(klass) + ", ref = " + this) val obj2 = obj.copy(outers = obj.outers.updated(klass, value)) heap.update(this, obj2) } @@ -292,6 +292,7 @@ object Semantic { extension (cache: CacheIn | CacheOut) def contains(value: Value, expr: Tree): Boolean = cache.contains(value) && cache(value).contains(expr) def get(value: Value, expr: Tree): Value = cache(value)(expr) + def remove(value: Value, expr: Tree) = cache(value).remove(expr) def put(value: Value, expr: Tree, result: Value): Unit = { val innerMap = cache.getOrElseUpdate(value, new EqHashMap[Tree, Value]) innerMap(expr) = result @@ -403,7 +404,7 @@ object Semantic { if target.is(Flags.Lazy) then given Trace = trace1 val rhs = target.defTree.asInstanceOf[ValDef].rhs - eval(rhs, ref, target.owner.asClass) + eval(rhs, ref, target.owner.asClass, cacheResult = true) else val obj = ref.objekt if obj.hasField(target) then @@ -417,7 +418,7 @@ object Semantic { Result(Hot, Nil) else if target.hasSource then val rhs = target.defTree.asInstanceOf[ValOrDefDef].rhs - eval(rhs, ref, target.owner.asClass) + eval(rhs, ref, target.owner.asClass, cacheResult = true) else val error = CallUnknown(field, source, trace.toVector) Result(Hot, error :: Nil) @@ -469,7 +470,7 @@ object Semantic { val env2 = Env(ddef, args.map(_.value).widenArgs) // normal method call withEnv(if isLocal then env else Env.empty) { - eval(ddef.rhs, ref, cls) ++ checkArgs + eval(ddef.rhs, ref, cls, cacheResult = true) ++ checkArgs } else if ref.canIgnoreMethodCall(target) then Result(Hot, Nil) @@ -490,7 +491,7 @@ object Semantic { if meth.name.toString == "tupled" then Result(value, Nil) // a call like `fun.tupled` else withEnv(env) { - eval(body, thisV, klass) ++ checkArgs + eval(body, thisV, klass, cacheResult = true) ++ checkArgs } case RefSet(refs) => @@ -517,11 +518,11 @@ object Semantic { if ctor.isPrimaryConstructor then given Env = env2 val tpl = cls.defTree.asInstanceOf[TypeDef].rhs.asInstanceOf[Template] - val res = withTrace(trace.add(cls.defTree)) { eval(tpl, ref, cls) } + val res = withTrace(trace.add(cls.defTree)) { eval(tpl, ref, cls, cacheResult = true) } Result(ref, res.errors) else given Env = env2 - eval(ddef.rhs, ref, cls) + eval(ddef.rhs, ref, cls, cacheResult = true) else if ref.canIgnoreMethodCall(ctor) then Result(Hot, Nil) else @@ -770,25 +771,22 @@ object Semantic { type Task = ThisRef class WorkList private[Semantic]() { - private var checkedTasks: Set[Task] = Set.empty private var pendingTasks: List[Task] = Nil def addTask(task: Task): Unit = - if !checkedTasks.contains(task) then pendingTasks = task :: pendingTasks + pendingTasks = task :: pendingTasks /** Process the worklist until done */ @tailrec final def work()(using State, Context): Unit = pendingTasks match case task :: rest => + val heapBefore = heap.snapshot() val res = doTask(task) res.errors.foreach(_.issue) - val heapBefore = heap.snapshot() - - if res.errors.nonEmpty then + if res.errors.nonEmpty || !cache.changed then pendingTasks = rest - checkedTasks = checkedTasks + task else // discard heap changes and copy cache.out to cache.in cache.update() @@ -811,7 +809,7 @@ object Semantic { given Trace = Trace.empty given Env = Env(paramValues) - init(tpl, thisRef, thisRef.klass) + eval(tpl, thisRef, thisRef.klass) } } inline def workList(using wl: WorkList): WorkList = wl @@ -862,7 +860,7 @@ object Semantic { * * This method only handles cache logic and delegates the work to `cases`. */ - def eval(expr: Tree, thisV: Ref, klass: ClassSymbol): Contextual[Result] = log("evaluating " + expr.show + ", this = " + thisV.show, printer, (_: Result).show) { + def eval(expr: Tree, thisV: Ref, klass: ClassSymbol, cacheResult: Boolean = false): Contextual[Result] = log("evaluating " + expr.show + ", this = " + thisV.show, printer, (_: Result).show) { if (cache.out.contains(thisV, expr)) Result(cache.out.get(thisV, expr), Errors.empty) else { val assumeValue = if (cache.in.contains(thisV, expr)) cache.in.get(thisV, expr) else Hot @@ -870,7 +868,9 @@ object Semantic { val res = cases(expr, thisV, klass) if res.value != assumeValue then cache.changed = true - cache.out.put(thisV, expr, res.value) + cache.out.put(thisV, expr, res.value) // must put in cache for termination + else + if !cacheResult then cache.out.remove(thisV, expr) res } } @@ -1058,6 +1058,9 @@ object Semantic { if tdef.isClassDef then Result(Hot, Errors.empty) else Result(Hot, checkTermUsage(tdef.rhs, thisV, klass)) + case tpl: Template => + init(tpl, thisV, klass) + case _: Import | _: Export => Result(Hot, Errors.empty) @@ -1102,8 +1105,8 @@ object Semantic { else thisV match case Hot => Hot - case thisRef: ThisRef => - val obj = thisRef.objekt + case ref: Ref => + val obj = ref.objekt val outerCls = klass.owner.lexicallyEnclosingClass.asClass if !obj.hasOuter(klass) then val error = PromoteError("outer not yet initialized, target = " + target + ", klass = " + klass, source, trace.toVector) @@ -1111,8 +1114,6 @@ object Semantic { Hot else resolveThis(target, obj.outer(klass), outerCls, source) - case warm: Warm => - ??? case RefSet(refs) => refs.map(ref => resolveThis(target, ref, klass, source)).join case fun: Fun => @@ -1168,7 +1169,7 @@ object Semantic { else cases(tref.prefix, thisV, klass, source) /** Initialize part of an abstract object in `klass` of the inheritance chain */ - def init(tpl: Template, thisV: ThisRef, klass: ClassSymbol): Contextual[Result] = log("init " + klass.show, printer, (_: Result).show) { + def init(tpl: Template, thisV: Ref, klass: ClassSymbol): Contextual[Result] = log("init " + klass.show, printer, (_: Result).show) { val errorBuffer = new mutable.ArrayBuffer[Error] val paramsMap = tpl.constr.termParamss.flatten.map { vdef => @@ -1275,8 +1276,11 @@ object Semantic { case _: MemberDef => case tree => - if fieldsChanged then thisV.tryPromoteCurrentObject - fieldsChanged = false + thisV match + case thisRef: ThisRef => + if fieldsChanged then thisRef.tryPromoteCurrentObject + fieldsChanged = false + case _ => given Env = Env.empty errorBuffer ++= eval(tree, thisV, klass).errors From 111ac870914134706129fa4400523942b45dae9c Mon Sep 17 00:00:00 2001 From: Fengyun Liu Date: Mon, 6 Sep 2021 08:13:30 +0200 Subject: [PATCH 13/52] Handle callConstructor --- .../tools/dotc/transform/init/Semantic.scala | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala b/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala index 949901009984..bc1b873e2798 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala @@ -140,9 +140,12 @@ object Semantic { def hasField(f: Symbol) = fields.contains(f) } - /** Abstract heap stores abstract warm objects + /** Abstract heap stores abstract objects * * The heap serves as cache of summaries for warm objects and is shared for checking all classes. + * + * The fact that objects of `ThisRef` are stored in heap is just an engineering convenience. + * Technically, we can also store the object directly in `ThisRef`. */ object Heap { class Heap(private var map: Map[Ref, Objekt]) { @@ -322,6 +325,9 @@ object Semantic { def call(meth: Symbol, args: List[ArgInfo], superType: Type, source: Tree): Contextual[Result] = value.call(meth, args, superType, source) ++ errors + def callConstructor(ctor: Symbol, args: List[ArgInfo], source: Tree): Contextual[Result] = + value.callConstructor(ctor, args, source) ++ errors + def instantiate(klass: ClassSymbol, ctor: Symbol, args: List[ArgInfo], source: Tree): Contextual[Result] = value.instantiate(klass, ctor, args, source) ++ errors } @@ -934,7 +940,10 @@ object Semantic { case Select(qual, _) => val res = eval(qual, thisV, klass) ++ errors - res.call(ref.symbol, args, superType = NoType, source = expr) + if ref.symbol.isConstructor then + res.callConstructor(ref.symbol, args, source = expr) + else + res.call(ref.symbol, args, superType = NoType, source = expr) case id: Ident => id.tpe match @@ -946,7 +955,10 @@ object Semantic { thisValue2.call(id.symbol, args, superType = NoType, expr, needResolve = false) case TermRef(prefix, _) => val res = cases(prefix, thisV, klass, id) ++ errors - res.call(id.symbol, args, superType = NoType, source = expr) + if id.symbol.isConstructor then + res.callConstructor(id.symbol, args, source = expr) + else + res.call(id.symbol, args, superType = NoType, source = expr) case Select(qualifier, name) => val qualRes = eval(qualifier, thisV, klass) From 29bdb5df4d04226970fd7b9086a8b488d7e7af0f Mon Sep 17 00:00:00 2001 From: Fengyun Liu Date: Mon, 6 Sep 2021 08:21:23 +0200 Subject: [PATCH 14/52] Reset Cache.changed after each iteration --- compiler/src/dotty/tools/dotc/transform/init/Semantic.scala | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala b/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala index bc1b873e2798..e5cb270e820b 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala @@ -798,6 +798,9 @@ object Semantic { cache.update() heap.restore(heapBefore) + // reset change + cache.changed = false + work() case _ => @@ -873,6 +876,7 @@ object Semantic { cache.out.put(thisV, expr, assumeValue) val res = cases(expr, thisV, klass) if res.value != assumeValue then + // println("changed: old = " + assumeValue + ", res = " + res.value) cache.changed = true cache.out.put(thisV, expr, res.value) // must put in cache for termination else From 0b1b21f484facd10c5dee8c4e84a41f180d2129f Mon Sep 17 00:00:00 2001 From: Fengyun Liu Date: Mon, 6 Sep 2021 08:44:02 +0200 Subject: [PATCH 15/52] Tweak log printing of errors --- compiler/src/dotty/tools/dotc/transform/init/Errors.scala | 2 ++ compiler/src/dotty/tools/dotc/transform/init/Semantic.scala | 3 ++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/compiler/src/dotty/tools/dotc/transform/init/Errors.scala b/compiler/src/dotty/tools/dotc/transform/init/Errors.scala index 4b423b9b1ae3..0aebf5b1afa0 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Errors.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Errors.scala @@ -55,6 +55,8 @@ object Errors { case unsafe: UnsafePromotion => unsafe.errors.flatMap(_.flatten) case _ => this :: Nil } + + override def toString() = this.getClass.getName } /** Access non-initialized field */ diff --git a/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala b/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala index e5cb270e820b..a00b7be4fef2 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala @@ -1387,9 +1387,10 @@ object Semantic { extension (symbol: Symbol) def hasSource(using Context): Boolean = !symbol.defTree.isEmpty - def resolve(cls: ClassSymbol, sym: Symbol)(using Context): Symbol = + def resolve(cls: ClassSymbol, sym: Symbol)(using Context): Symbol = log("resove " + cls + ", " + sym, printer, _.asInstanceOf[Symbol].show) { if (sym.isEffectivelyFinal || sym.isConstructor) sym else sym.matchingMember(cls.appliedRef) + } def resolveSuper(cls: ClassSymbol, superType: Type, sym: Symbol)(using Context): Symbol = { import annotation.tailrec From c5daf1d43b83a233b13b9b3dc6642f5de7b18ecf Mon Sep 17 00:00:00 2001 From: Fengyun Liu Date: Mon, 6 Sep 2021 20:43:41 +0200 Subject: [PATCH 16/52] Fix context for init check Otherwise, the symbol denotations are not update to date: some synthetic members are missing, thus static method resolution produces incorrect result. --- compiler/src/dotty/tools/dotc/transform/init/Checker.scala | 2 ++ 1 file changed, 2 insertions(+) diff --git a/compiler/src/dotty/tools/dotc/transform/init/Checker.scala b/compiler/src/dotty/tools/dotc/transform/init/Checker.scala index 772ea44a1cca..d74ab46abdc2 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Checker.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Checker.scala @@ -30,9 +30,11 @@ class Checker extends Phase { super.isEnabled && ctx.settings.YcheckInit.value override def runOn(units: List[CompilationUnit])(using Context): List[CompilationUnit] = + val checkCtx = ctx.fresh.setPhase(this.start) Semantic.withInitialState { val traverser = new InitTreeTraverser() units.foreach { unit => traverser.traverse(unit.tpdTree) } + given Context = checkCtx Semantic.check() super.runOn(units) } From 4a71470e0177545687b0f15f51470e78cd26a9df Mon Sep 17 00:00:00 2001 From: Fengyun Liu Date: Mon, 6 Sep 2021 22:45:34 +0200 Subject: [PATCH 17/52] Introduce global cache which holds fixed point value --- .../tools/dotc/transform/init/Semantic.scala | 58 +++++++++++++++---- 1 file changed, 46 insertions(+), 12 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala b/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala index a00b7be4fef2..1e924656b05f 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala @@ -272,12 +272,46 @@ object Semantic { */ object Cache { - opaque type CacheIn = mutable.Map[Value, EqHashMap[Tree, Value]] - opaque type CacheOut = mutable.Map[Value, EqHashMap[Tree, Value]] + opaque type CacheStore = mutable.Map[Value, EqHashMap[Tree, Value]] - class Cache(val in: CacheIn, var out: CacheOut) { + class Cache { + private val in: CacheStore = mutable.Map.empty + private var out: CacheStore = mutable.Map.empty + private val global: CacheStore = mutable.Map.empty var changed: Boolean = false + def contains(value: Value, expr: Tree) = + out.contains(value, expr) || global.contains(value, expr) + + def apply(value: Value, expr: Tree) = + if out.contains(value, expr) then out(value)(expr) + else global(value)(expr) + + def assume(value: Value, expr: Tree) = + val assumeValue = if (in.contains(value, expr)) in.get(value, expr) else Hot + out.put(value, expr, assumeValue) + assumeValue + + def update(value: Value, expr: Tree, result: Value) = + out.put(value, expr, result) + + def remove(value: Value, expr: Tree) = + out.remove(value, expr) + + /** Commit out cache to global cache. + * + * Precondition: the out cache reaches fixed point. + */ + def commit() = { + out.foreach { (v, m) => + m.iterator.foreach { (e, res) => + global.put(v, e, res) + } + } + + out = mutable.Map.empty + } + /** Copy out to in and reset out to empty */ def update() = { out.foreach { (v, m) => @@ -290,10 +324,10 @@ object Semantic { } } - val empty: Cache = new Cache(mutable.Map.empty, mutable.Map.empty) + val empty: Cache = new Cache() - extension (cache: CacheIn | CacheOut) - def contains(value: Value, expr: Tree): Boolean = cache.contains(value) && cache(value).contains(expr) + extension (cache: CacheStore) + def contains(value: Value, expr: Tree) = cache.contains(value) && cache(value).contains(expr) def get(value: Value, expr: Tree): Value = cache(value)(expr) def remove(value: Value, expr: Tree) = cache(value).remove(expr) def put(value: Value, expr: Tree, result: Value): Unit = { @@ -793,6 +827,7 @@ object Semantic { if res.errors.nonEmpty || !cache.changed then pendingTasks = rest + cache.commit() else // discard heap changes and copy cache.out to cache.in cache.update() @@ -808,7 +843,7 @@ object Semantic { * * This method should only be called from the work list scheduler. */ - private def doTask(task: Task)(using State, Context): Result = { + private def doTask(task: Task)(using State, Context): Result = log("checking " + task) { val thisRef = task val tpl = thisRef.klass.defTree.asInstanceOf[TypeDef].rhs.asInstanceOf[Template] @@ -870,17 +905,16 @@ object Semantic { * This method only handles cache logic and delegates the work to `cases`. */ def eval(expr: Tree, thisV: Ref, klass: ClassSymbol, cacheResult: Boolean = false): Contextual[Result] = log("evaluating " + expr.show + ", this = " + thisV.show, printer, (_: Result).show) { - if (cache.out.contains(thisV, expr)) Result(cache.out.get(thisV, expr), Errors.empty) + if (cache.contains(thisV, expr)) Result(cache(thisV, expr), Errors.empty) else { - val assumeValue = if (cache.in.contains(thisV, expr)) cache.in.get(thisV, expr) else Hot - cache.out.put(thisV, expr, assumeValue) + val assumeValue = cache.assume(thisV, expr) val res = cases(expr, thisV, klass) if res.value != assumeValue then // println("changed: old = " + assumeValue + ", res = " + res.value) cache.changed = true - cache.out.put(thisV, expr, res.value) // must put in cache for termination + cache.update(thisV, expr, res.value) // must put in cache for termination else - if !cacheResult then cache.out.remove(thisV, expr) + if !cacheResult then cache.remove(thisV, expr) res } } From 375e0e58b8cb51174101e3c79d8f862af1e4ef5d Mon Sep 17 00:00:00 2001 From: Fengyun Liu Date: Tue, 7 Sep 2021 08:06:25 +0200 Subject: [PATCH 18/52] Update tests --- tests/init/neg/cycle-structure.check | 8 ++++---- tests/init/neg/enum-desugared.check | 4 ++-- tests/init/neg/inner-loop.scala | 2 +- tests/init/neg/local-warm4.check | 5 +++-- 4 files changed, 10 insertions(+), 9 deletions(-) diff --git a/tests/init/neg/cycle-structure.check b/tests/init/neg/cycle-structure.check index 77f48adf766f..b370b16f4279 100644 --- a/tests/init/neg/cycle-structure.check +++ b/tests/init/neg/cycle-structure.check @@ -1,8 +1,8 @@ --- Error: tests/init/neg/cycle-structure.scala:3:14 -------------------------------------------------------------------- -3 | val x = B(this) // error - | ^^^^ - | 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 | ^^^^ | Cannot prove that the value is fully initialized. May only use initialized value as arguments. +-- Error: tests/init/neg/cycle-structure.scala:3:14 -------------------------------------------------------------------- +3 | val x = B(this) // error + | ^^^^ + | Cannot prove that the value is fully initialized. May only use initialized value as arguments. diff --git a/tests/init/neg/enum-desugared.check b/tests/init/neg/enum-desugared.check index 2b090378e877..567b8104c154 100644 --- a/tests/init/neg/enum-desugared.check +++ b/tests/init/neg/enum-desugared.check @@ -13,6 +13,6 @@ | Cannot prove that the value is fully-initialized. May only use initialized value as method arguments. | | The unsafe promotion may cause the following problem: - | Calling the external method method name may cause initialization errors. Calling trace: + | Calling the external method method ordinal may cause initialization errors. Calling trace: | -> Array(this.LazyErrorId, this.NoExplanationID) // error // error [ enum-desugared.scala:17 ] - | -> override def productPrefix: String = this.name() [ enum-desugared.scala:29 ] + | -> def errorNumber: Int = this.ordinal() - 2 [ enum-desugared.scala:8 ] diff --git a/tests/init/neg/inner-loop.scala b/tests/init/neg/inner-loop.scala index c6d5c615580c..51361f261450 100644 --- a/tests/init/neg/inner-loop.scala +++ b/tests/init/neg/inner-loop.scala @@ -1,6 +1,6 @@ class Outer { outer => class Inner extends Outer { - val x = 5 + outer.n + val x = 5 + outer.n // error } val inner = new Inner val n = 6 // error diff --git a/tests/init/neg/local-warm4.check b/tests/init/neg/local-warm4.check index 664900b3cffa..fda1ee1b928c 100644 --- a/tests/init/neg/local-warm4.check +++ b/tests/init/neg/local-warm4.check @@ -6,5 +6,6 @@ | -> class A(x: Int) extends Foo(x) { [ local-warm4.scala:6 ] | -> val b = new B(y) [ local-warm4.scala:10 ] | -> class B(x: Int) extends A(x) { [ local-warm4.scala:13 ] - | -> if y < 10 then increment() [ local-warm4.scala:23 ] - | -> updateA() [ local-warm4.scala:21 ] + | -> class A(x: Int) extends Foo(x) { [ local-warm4.scala:6 ] + | -> increment() [ local-warm4.scala:9 ] + | -> updateA() [ local-warm4.scala:21 ] From 6188e8e03181c5acc4596d544f2b8f9a53d3ebcc Mon Sep 17 00:00:00 2001 From: Fengyun Liu Date: Tue, 7 Sep 2021 22:44:58 +0200 Subject: [PATCH 19/52] Avoid asInstanceOf in logging --- compiler/src/dotty/tools/dotc/transform/init/Semantic.scala | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala b/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala index 1e924656b05f..4fbccf1f442d 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala @@ -542,7 +542,7 @@ object Semantic { } } - def callConstructor(ctor: Symbol, args: List[ArgInfo], source: Tree): Contextual[Result] = log("call " + ctor.show + ", args = " + args, printer, res => res.asInstanceOf[Result].show) { + def callConstructor(ctor: Symbol, args: List[ArgInfo], source: Tree): Contextual[Result] = log("call " + ctor.show + ", args = " + args, printer, (_: Result).show) { value match { case Hot | Cold | _: RefSet | _: Fun => report.error("unexpected constructor call, meth = " + ctor + ", value = " + value, source) @@ -1421,7 +1421,7 @@ object Semantic { extension (symbol: Symbol) def hasSource(using Context): Boolean = !symbol.defTree.isEmpty - def resolve(cls: ClassSymbol, sym: Symbol)(using Context): Symbol = log("resove " + cls + ", " + sym, printer, _.asInstanceOf[Symbol].show) { + def resolve(cls: ClassSymbol, sym: Symbol)(using Context): Symbol = log("resove " + cls + ", " + sym, printer, (_: Symbol).show) { if (sym.isEffectivelyFinal || sym.isConstructor) sym else sym.matchingMember(cls.appliedRef) } From 5bdd898c915ccc14301aa161d7e42b6dfdf48566 Mon Sep 17 00:00:00 2001 From: Fengyun Liu Date: Wed, 8 Sep 2021 08:04:36 +0200 Subject: [PATCH 20/52] Refactor cache --- .../tools/dotc/transform/init/Semantic.scala | 50 +++++++++---------- 1 file changed, 24 insertions(+), 26 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala b/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala index 4fbccf1f442d..f2c8e377ba35 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala @@ -278,7 +278,9 @@ object Semantic { private val in: CacheStore = mutable.Map.empty private var out: CacheStore = mutable.Map.empty private val global: CacheStore = mutable.Map.empty - var changed: Boolean = false + private var changed: Boolean = false + + def hasChanged = changed def contains(value: Value, expr: Tree) = out.contains(value, expr) || global.contains(value, expr) @@ -293,32 +295,31 @@ object Semantic { assumeValue def update(value: Value, expr: Tree, result: Value) = + this.changed = true + in.put(value, expr, result) out.put(value, expr, result) def remove(value: Value, expr: Tree) = out.remove(value, expr) - /** Commit out cache to global cache. + /** Prepare cache for the next iteration + * + * - Commit out cache to global cache if unchanged. + * - Reset changed flag + * - Reset out cache * * Precondition: the out cache reaches fixed point. */ - def commit() = { - out.foreach { (v, m) => - m.iterator.foreach { (e, res) => - global.put(v, e, res) + def iterate() = { + if !changed then + out.foreach { (v, m) => + m.iterator.foreach { (e, res) => + global.put(v, e, res) + } } - } + end if - out = mutable.Map.empty - } - - /** Copy out to in and reset out to empty */ - def update() = { - out.foreach { (v, m) => - m.iterator.foreach { (e, res) => - in.put(v, e, res) - } - } + changed = false out = mutable.Map.empty } @@ -825,16 +826,13 @@ object Semantic { val res = doTask(task) res.errors.foreach(_.issue) - if res.errors.nonEmpty || !cache.changed then - pendingTasks = rest - cache.commit() - else + if cache.hasChanged && res.errors.isEmpty then // discard heap changes and copy cache.out to cache.in - cache.update() heap.restore(heapBefore) + else + pendingTasks = rest - // reset change - cache.changed = false + cache.iterate() work() case _ => @@ -911,7 +909,6 @@ object Semantic { val res = cases(expr, thisV, klass) if res.value != assumeValue then // println("changed: old = " + assumeValue + ", res = " + res.value) - cache.changed = true cache.update(thisV, expr, res.value) // must put in cache for termination else if !cacheResult then cache.remove(thisV, expr) @@ -1336,7 +1333,8 @@ object Semantic { errorBuffer ++= eval(tree, thisV, klass).errors } - Result(thisV, errorBuffer.toList) + // The result value is ignored, use Hot to avoid futile fixed point computation + Result(Hot, errorBuffer.toList) } /** Check that path in path-dependent types are initialized From 9ea3624505c9fae130ee749270a68bc08fb97ea0 Mon Sep 17 00:00:00 2001 From: Fengyun Liu Date: Wed, 8 Sep 2021 08:11:38 +0200 Subject: [PATCH 21/52] Cache default value in the input cache --- .../src/dotty/tools/dotc/transform/init/Semantic.scala | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala b/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala index f2c8e377ba35..ec09dce4f829 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala @@ -290,7 +290,13 @@ object Semantic { else global(value)(expr) def assume(value: Value, expr: Tree) = - val assumeValue = if (in.contains(value, expr)) in.get(value, expr) else Hot + + val assumeValue = + if in.contains(value, expr) then + in.get(value, expr) + else + in.put(value, expr, Hot) + Hot out.put(value, expr, assumeValue) assumeValue From d0e71a232f1a3ff77d37b93ff75a3ae492a74bae Mon Sep 17 00:00:00 2001 From: Fengyun Liu Date: Wed, 8 Sep 2021 08:17:00 +0200 Subject: [PATCH 22/52] Rename global cache to stable cache --- .../src/dotty/tools/dotc/transform/init/Semantic.scala | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala b/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala index ec09dce4f829..af1e8b2d2076 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala @@ -277,17 +277,17 @@ object Semantic { class Cache { private val in: CacheStore = mutable.Map.empty private var out: CacheStore = mutable.Map.empty - private val global: CacheStore = mutable.Map.empty + private val stable: CacheStore = mutable.Map.empty private var changed: Boolean = false def hasChanged = changed def contains(value: Value, expr: Tree) = - out.contains(value, expr) || global.contains(value, expr) + out.contains(value, expr) || stable.contains(value, expr) def apply(value: Value, expr: Tree) = if out.contains(value, expr) then out(value)(expr) - else global(value)(expr) + else stable(value)(expr) def assume(value: Value, expr: Tree) = @@ -310,7 +310,7 @@ object Semantic { /** Prepare cache for the next iteration * - * - Commit out cache to global cache if unchanged. + * - Commit out cache to stable cache if unchanged. * - Reset changed flag * - Reset out cache * @@ -320,7 +320,7 @@ object Semantic { if !changed then out.foreach { (v, m) => m.iterator.foreach { (e, res) => - global.put(v, e, res) + stable.put(v, e, res) } } end if From fe3930d2ccef94d18949c441739c91d53272ed18 Mon Sep 17 00:00:00 2001 From: Fengyun Liu Date: Wed, 8 Sep 2021 08:26:12 +0200 Subject: [PATCH 23/52] Better encapsulate Cache.assume --- .../tools/dotc/transform/init/Semantic.scala | 26 +++++++------------ 1 file changed, 9 insertions(+), 17 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala b/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala index af1e8b2d2076..46c1825025d7 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala @@ -289,8 +289,7 @@ object Semantic { if out.contains(value, expr) then out(value)(expr) else stable(value)(expr) - def assume(value: Value, expr: Tree) = - + def assume(value: Value, expr: Tree)(fun: => Result): Result = val assumeValue = if in.contains(value, expr) then in.get(value, expr) @@ -298,12 +297,14 @@ object Semantic { in.put(value, expr, Hot) Hot out.put(value, expr, assumeValue) - assumeValue - def update(value: Value, expr: Tree, result: Value) = - this.changed = true - in.put(value, expr, result) - out.put(value, expr, result) + val actual = fun + if actual.value != assumeValue then + this.changed = true + in.put(value, expr, actual.value) + out.put(value, expr, actual.value) + + actual def remove(value: Value, expr: Tree) = out.remove(value, expr) @@ -910,16 +911,7 @@ object Semantic { */ def eval(expr: Tree, thisV: Ref, klass: ClassSymbol, cacheResult: Boolean = false): Contextual[Result] = log("evaluating " + expr.show + ", this = " + thisV.show, printer, (_: Result).show) { if (cache.contains(thisV, expr)) Result(cache(thisV, expr), Errors.empty) - else { - val assumeValue = cache.assume(thisV, expr) - val res = cases(expr, thisV, klass) - if res.value != assumeValue then - // println("changed: old = " + assumeValue + ", res = " + res.value) - cache.update(thisV, expr, res.value) // must put in cache for termination - else - if !cacheResult then cache.remove(thisV, expr) - res - } + else cache.assume(thisV, expr) { cases(expr, thisV, klass) } } /** Evaluate a list of expressions */ From 5bd83ae58cb451e93d0a12aa6a426e671db91db6 Mon Sep 17 00:00:00 2001 From: Fengyun Liu Date: Wed, 8 Sep 2021 08:31:25 +0200 Subject: [PATCH 24/52] Rename in/out to last/current --- .../tools/dotc/transform/init/Semantic.scala | 38 +++++++++---------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala b/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala index 46c1825025d7..253c9fe88f67 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala @@ -275,51 +275,51 @@ object Semantic { opaque type CacheStore = mutable.Map[Value, EqHashMap[Tree, Value]] class Cache { - private val in: CacheStore = mutable.Map.empty - private var out: CacheStore = mutable.Map.empty + private val last: CacheStore = mutable.Map.empty + private var current: CacheStore = mutable.Map.empty private val stable: CacheStore = mutable.Map.empty private var changed: Boolean = false def hasChanged = changed def contains(value: Value, expr: Tree) = - out.contains(value, expr) || stable.contains(value, expr) + current.contains(value, expr) || stable.contains(value, expr) def apply(value: Value, expr: Tree) = - if out.contains(value, expr) then out(value)(expr) + if current.contains(value, expr) then current(value)(expr) else stable(value)(expr) - def assume(value: Value, expr: Tree)(fun: => Result): Result = + def assume(value: Value, expr: Tree, cacheResult: Boolean)(fun: => Result): Result = val assumeValue = - if in.contains(value, expr) then - in.get(value, expr) + if last.contains(value, expr) then + last.get(value, expr) else - in.put(value, expr, Hot) + last.put(value, expr, Hot) Hot - out.put(value, expr, assumeValue) + current.put(value, expr, assumeValue) val actual = fun if actual.value != assumeValue then this.changed = true - in.put(value, expr, actual.value) - out.put(value, expr, actual.value) + last.put(value, expr, actual.value) + current.put(value, expr, actual.value) actual def remove(value: Value, expr: Tree) = - out.remove(value, expr) + current.remove(value, expr) /** Prepare cache for the next iteration * - * - Commit out cache to stable cache if unchanged. + * - Commit current cache to stable cache if unchanged. * - Reset changed flag - * - Reset out cache + * - Reset current cache (last cache already synced in `assume`) * - * Precondition: the out cache reaches fixed point. + * Precondition: the current cache reaches fixed point. */ def iterate() = { if !changed then - out.foreach { (v, m) => + current.foreach { (v, m) => m.iterator.foreach { (e, res) => stable.put(v, e, res) } @@ -328,7 +328,7 @@ object Semantic { changed = false - out = mutable.Map.empty + current = mutable.Map.empty } } @@ -834,7 +834,7 @@ object Semantic { res.errors.foreach(_.issue) if cache.hasChanged && res.errors.isEmpty then - // discard heap changes and copy cache.out to cache.in + // discard heap changes heap.restore(heapBefore) else pendingTasks = rest @@ -911,7 +911,7 @@ object Semantic { */ def eval(expr: Tree, thisV: Ref, klass: ClassSymbol, cacheResult: Boolean = false): Contextual[Result] = log("evaluating " + expr.show + ", this = " + thisV.show, printer, (_: Result).show) { if (cache.contains(thisV, expr)) Result(cache(thisV, expr), Errors.empty) - else cache.assume(thisV, expr) { cases(expr, thisV, klass) } + else cache.assume(thisV, expr, cacheResult) { cases(expr, thisV, klass) } } /** Evaluate a list of expressions */ From 25898240a2ac64648facb022c72ca983d5d8fa8f Mon Sep 17 00:00:00 2001 From: Fengyun Liu Date: Wed, 8 Sep 2021 22:30:46 +0200 Subject: [PATCH 25/52] Mark arguments as constructor only --- compiler/src/dotty/tools/dotc/transform/init/Semantic.scala | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala b/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala index 253c9fe88f67..2c70da6363dc 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala @@ -18,6 +18,7 @@ import Errors._ import scala.collection.mutable import scala.annotation.tailrec +import scala.annotation.constructorOnly object Semantic { @@ -98,7 +99,7 @@ object Semantic { } /** A reference to the object under initialization pointed by `this` */ - case class ThisRef(klass: ClassSymbol)(using Heap) extends Ref { + case class ThisRef(klass: ClassSymbol)(using @constructorOnly h: Heap) extends Ref { val outer = Hot ensureObjectExists() @@ -108,7 +109,7 @@ object Semantic { * * We need to restrict nesting levels of `outer` to finitize the domain. */ - case class Warm(klass: ClassSymbol, outer: Value, ctor: Symbol, args: List[Value])(using Heap) extends Ref { + case class Warm(klass: ClassSymbol, outer: Value, ctor: Symbol, args: List[Value])(using @constructorOnly h: Heap) extends Ref { ensureObjectExists() } From 284905220f9d37da535c24f890f682d95fe61619 Mon Sep 17 00:00:00 2001 From: Fengyun Liu Date: Wed, 8 Sep 2021 22:44:52 +0200 Subject: [PATCH 26/52] Add note for tempting but unsound optimization --- .../src/dotty/tools/dotc/transform/init/Semantic.scala | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala b/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala index 2c70da6363dc..de2a09de63c9 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala @@ -304,6 +304,14 @@ object Semantic { this.changed = true last.put(value, expr, actual.value) current.put(value, expr, actual.value) + else + // It's tempting to cache the value in stable, but it's unsound. + // The reason is that the current value may depend on other values + // which might change. + // + // stable.put(value, expr, actual) + () + end if actual From 8a4fe78a744e8fa15f382cf2c3df3be4618cea50 Mon Sep 17 00:00:00 2001 From: Fengyun Liu Date: Wed, 8 Sep 2021 22:54:41 +0200 Subject: [PATCH 27/52] Commit cache to stable on error This is important because we don't revert the heap on error. Therefore, the stable cache has to be updated. Otherwise, a warm object might be initialized twice, violating invariants. --- .../tools/dotc/transform/init/Semantic.scala | 24 ++++++++----------- 1 file changed, 10 insertions(+), 14 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala b/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala index de2a09de63c9..40cd72a5228b 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala @@ -314,29 +314,24 @@ object Semantic { end if actual + end assume - def remove(value: Value, expr: Tree) = - current.remove(value, expr) + /** Commit current cache to stable cache. */ + def commit() = + current.foreach { (v, m) => + m.iterator.foreach { (e, res) => + stable.put(v, e, res) + } + } + current = mutable.Map.empty /** Prepare cache for the next iteration * - * - Commit current cache to stable cache if unchanged. * - Reset changed flag * - Reset current cache (last cache already synced in `assume`) - * - * Precondition: the current cache reaches fixed point. */ def iterate() = { - if !changed then - current.foreach { (v, m) => - m.iterator.foreach { (e, res) => - stable.put(v, e, res) - } - } - end if - changed = false - current = mutable.Map.empty } } @@ -846,6 +841,7 @@ object Semantic { // discard heap changes heap.restore(heapBefore) else + cache.commit() pendingTasks = rest cache.iterate() From 5b00b1f91e65a50f6357dee26842af82e11e5a1a Mon Sep 17 00:00:00 2001 From: Fengyun Liu Date: Wed, 8 Sep 2021 23:18:10 +0200 Subject: [PATCH 28/52] Make sure the object of a reference assumption value exists --- .../tools/dotc/transform/init/Semantic.scala | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala b/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala index 40cd72a5228b..39890b3f39af 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala @@ -290,13 +290,17 @@ object Semantic { if current.contains(value, expr) then current(value)(expr) else stable(value)(expr) - def assume(value: Value, expr: Tree, cacheResult: Boolean)(fun: => Result): Result = - val assumeValue = + def assume(value: Value, expr: Tree, cacheResult: Boolean)(fun: => Result)(using Heap): Result = + val assumeValue: Value = if last.contains(value, expr) then - last.get(value, expr) + // Due to heap reverting, the object corresponding to a reference may not exist in the heap. + last.get(value, expr) match + case ref: Ref => ref.ensureObjectExists(); ref + case v => v else last.put(value, expr, Hot) Hot + end if current.put(value, expr, assumeValue) val actual = fun @@ -316,7 +320,10 @@ object Semantic { actual end assume - /** Commit current cache to stable cache. */ + /** Commit current cache to stable cache. + * + * TODO: It's useless to cache value for ThisRef. + */ def commit() = current.foreach { (v, m) => m.iterator.foreach { (e, res) => From 04aa72bb5f1f76cfa230d070235a0d96019c5732 Mon Sep 17 00:00:00 2001 From: Fengyun Liu Date: Wed, 8 Sep 2021 23:50:13 +0200 Subject: [PATCH 29/52] Add back arguments to ThisRef --- .../tools/dotc/transform/init/Checker.scala | 5 +++- .../tools/dotc/transform/init/Semantic.scala | 25 +++++++++++++------ 2 files changed, 21 insertions(+), 9 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/transform/init/Checker.scala b/compiler/src/dotty/tools/dotc/transform/init/Checker.scala index d74ab46abdc2..1a191564d924 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Checker.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Checker.scala @@ -55,7 +55,10 @@ class Checker extends Phase { mdef match case tdef: TypeDef if tdef.isClassDef => val cls = tdef.symbol.asClass - val thisRef = ThisRef(cls) + val ctor = cls.primaryConstructor + val args = ctor.defTree.asInstanceOf[DefDef].termParamss.flatten.map(_ => Hot) + val outer = Hot + val thisRef = ThisRef(cls, outer, ctor, args) given Trace = Trace.empty if shouldCheckClass(cls) then Semantic.addTask(thisRef) case _ => diff --git a/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala b/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala index 39890b3f39af..6aeccda2d9fa 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala @@ -55,6 +55,10 @@ object Semantic { */ sealed abstract class Value { def show: String = this.toString() + + def isHot = this == Hot + def isCold = this == Cold + def isWarm = this.isInstanceOf[Warm] } /** A transitively initialized object */ @@ -99,9 +103,7 @@ object Semantic { } /** A reference to the object under initialization pointed by `this` */ - case class ThisRef(klass: ClassSymbol)(using @constructorOnly h: Heap) extends Ref { - val outer = Hot - + case class ThisRef(klass: ClassSymbol, outer: Value, ctor: Symbol, args: List[Value])(using @constructorOnly h: Heap) extends Ref { ensureObjectExists() } @@ -447,7 +449,7 @@ object Semantic { def widenArgs: List[Value] = values.map(_.widenArg).toList extension (value: Value) - def select(field: Symbol, source: Tree, needResolve: Boolean = true): Contextual[Result] = log("select " + field.show, printer, (_: Result).show) { + def select(field: Symbol, source: Tree, needResolve: Boolean = true): Contextual[Result] = log("select " + field.show + ", this = " + value, printer, (_: Result).show) { if promoted.isCurrentObjectPromoted then Result(Hot, Nil) else value match { case Hot => @@ -613,6 +615,8 @@ object Semantic { val warm = Warm(klass, outer, ctor, args2) val argInfos2 = args.zip(args2).map { (argInfo, v) => argInfo.copy(value = v) } val res = warm.callConstructor(ctor, argInfos2, source) + val task = ThisRef(klass, outer, ctor, args2) + this.addTask(task) Result(warm, res.errors) case Cold => @@ -630,6 +634,8 @@ object Semantic { val argInfos2 = args.zip(argsWidened).map { (argInfo, v) => argInfo.copy(value = v) } val warm = Warm(klass, outer, ctor, argsWidened) val res = warm.callConstructor(ctor, argInfos2, source) + val task = ThisRef(klass, outer, ctor, argsWidened) + this.addTask(task) Result(warm, res.errors) case Fun(body, thisV, klass, env) => @@ -827,19 +833,22 @@ object Semantic { cls == defn.ObjectClass // ----- Work list --------------------------------------------------- - type Task = ThisRef + case class Task(value: ThisRef)(val trace: Trace) class WorkList private[Semantic]() { private var pendingTasks: List[Task] = Nil + private var checkedTasks: Set[Task] = Set.empty def addTask(task: Task): Unit = - pendingTasks = task :: pendingTasks + if !checkedTasks.contains(task) then pendingTasks = task :: pendingTasks /** Process the worklist until done */ @tailrec final def work()(using State, Context): Unit = pendingTasks match case task :: rest => + checkedTasks = checkedTasks + task + val heapBefore = heap.snapshot() val res = doTask(task) res.errors.foreach(_.issue) @@ -861,7 +870,7 @@ object Semantic { * This method should only be called from the work list scheduler. */ private def doTask(task: Task)(using State, Context): Result = log("checking " + task) { - val thisRef = task + val thisRef = task.value val tpl = thisRef.klass.defTree.asInstanceOf[TypeDef].rhs.asInstanceOf[Template] val paramValues = tpl.constr.termParamss.flatten.map(param => param.symbol -> Hot).toMap @@ -878,7 +887,7 @@ object Semantic { // ----- API -------------------------------- /** Add a checking task to the work list */ - def addTask(task: ThisRef)(using WorkList) = workList.addTask(task) + def addTask(thisRef: ThisRef)(using WorkList, Trace) = workList.addTask(Task(thisRef)(trace)) /** Perform check on the work list until it becomes empty * From ad503fcaa6a7510d8ae8540be5b68abcd40877ab Mon Sep 17 00:00:00 2001 From: Fengyun Liu Date: Thu, 9 Sep 2021 00:11:06 +0200 Subject: [PATCH 30/52] Populate parameters of Warm objects by reusing init --- .../tools/dotc/transform/init/Semantic.scala | 33 ++++++++++++++----- 1 file changed, 25 insertions(+), 8 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala b/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala index 6aeccda2d9fa..10baa5a4f9de 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala @@ -569,7 +569,26 @@ object Semantic { report.error("unexpected constructor call, meth = " + ctor + ", value = " + value, source) Result(Hot, Nil) - case ref: Ref => + case ref: Warm => + val trace1 = trace.add(source) + if ctor.hasSource then + given Trace = trace1 + val cls = ctor.owner.enclosingClass.asClass + val ddef = ctor.defTree.asInstanceOf[DefDef] + given Env = Env(ddef, args.map(_.value).widenArgs) + if ctor.isPrimaryConstructor then + val tpl = cls.defTree.asInstanceOf[TypeDef].rhs.asInstanceOf[Template] + init(tpl, ref, cls) + else + val initCall = ddef.rhs match + case Block(call :: _, _) => call + case call => call + eval(initCall, ref, cls) + end if + else + Result(Hot, Nil) + + case ref: ThisRef => val trace1 = trace.add(source) if ctor.hasSource then given Trace = trace1 @@ -871,6 +890,7 @@ object Semantic { */ private def doTask(task: Task)(using State, Context): Result = log("checking " + task) { val thisRef = task.value + thisRef.ensureObjectExists() val tpl = thisRef.klass.defTree.asInstanceOf[TypeDef].rhs.asInstanceOf[Template] val paramValues = tpl.constr.termParamss.flatten.map(param => param.symbol -> Hot).toMap @@ -1260,7 +1280,7 @@ object Semantic { thisV.updateOuter(cls, res.value) // follow constructor - if cls.hasSource then + if cls.hasSource && !thisV.isWarm then tasks.append { () => printer.println("init super class " + cls.show) val res2 = thisV.callConstructor(ctor, args, source) @@ -1331,7 +1351,7 @@ object Semantic { var fieldsChanged = true // class body - tpl.body.foreach { + if (!thisV.isWarm) tpl.body.foreach { case vdef : ValDef if !vdef.symbol.is(Flags.Lazy) && !vdef.rhs.isEmpty => given Env = Env.empty val res = eval(vdef.rhs, thisV, klass) @@ -1342,11 +1362,8 @@ object Semantic { case _: MemberDef => case tree => - thisV match - case thisRef: ThisRef => - if fieldsChanged then thisRef.tryPromoteCurrentObject - fieldsChanged = false - case _ => + if fieldsChanged then thisV.asInstanceOf[ThisRef].tryPromoteCurrentObject + fieldsChanged = false given Env = Env.empty errorBuffer ++= eval(tree, thisV, klass).errors From fb3b6cab909f0f54067b91e736f303b7cdc4f0bc Mon Sep 17 00:00:00 2001 From: Fengyun Liu Date: Thu, 9 Sep 2021 00:40:52 +0200 Subject: [PATCH 31/52] Make sure the class parameters and outers of warm objects are populated --- .../tools/dotc/transform/init/Semantic.scala | 41 +++++++++++++------ 1 file changed, 29 insertions(+), 12 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala b/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala index 10baa5a4f9de..9022a77f0aba 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala @@ -72,12 +72,12 @@ object Semantic { def outer: Value def objekt(using Heap): Objekt = heap(this) - def ensureObjectExists()(using Heap) = - if heap.contains(this) then heap(this) + def ensureObjectExists()(using Heap): this.type = + if heap.contains(this) then this else { val obj = Objekt(this.klass, fields = Map.empty, outers = Map(this.klass -> this.outer)) heap.update(this, obj) - obj + this } @@ -113,6 +113,22 @@ object Semantic { */ case class Warm(klass: ClassSymbol, outer: Value, ctor: Symbol, args: List[Value])(using @constructorOnly h: Heap) extends Ref { ensureObjectExists() + + /** Ensure that outers and class parameters are initialized. + * + * Fields in class body are not initialized. + */ + def ensureInit(): Contextual[this.type] = + // Somehow Dotty uses the one in the class parameters + given Heap = state.heap + if objekt.outers.size <= 1 then + val tpl = klass.defTree.asInstanceOf[TypeDef].rhs.asInstanceOf[Template] + val termParamss = ctor.defTree.asInstanceOf[DefDef].termParamss + val paramValues = termParamss.flatten.zip(args).map((param, v) => param.symbol -> v).toMap + given Env = Env(paramValues) + init(tpl, this, klass) + end if + this } /** A function value */ @@ -292,12 +308,13 @@ object Semantic { if current.contains(value, expr) then current(value)(expr) else stable(value)(expr) - def assume(value: Value, expr: Tree, cacheResult: Boolean)(fun: => Result)(using Heap): Result = + def assume(value: Value, expr: Tree, cacheResult: Boolean)(fun: => Result): Contextual[Result] = val assumeValue: Value = if last.contains(value, expr) then // Due to heap reverting, the object corresponding to a reference may not exist in the heap. last.get(value, expr) match - case ref: Ref => ref.ensureObjectExists(); ref + case ref: ThisRef => ref.ensureObjectExists() + case ref: Warm => ref.ensureObjectExists().ensureInit() case v => v else last.put(value, expr, Hot) @@ -395,6 +412,8 @@ object Semantic { given (using s: State): Cache = s.cache given (using s: State): WorkList = s.workList + inline def state(using s: State) = s + /** The state that threads through the interpreter */ type Contextual[T] = (Env, Context, Trace, Promoted, State) ?=> T @@ -631,12 +650,11 @@ object Semantic { Result(Hot, Errors.empty) else val outer = Hot - val warm = Warm(klass, outer, ctor, args2) + val warm = Warm(klass, outer, ctor, args2).ensureInit() val argInfos2 = args.zip(args2).map { (argInfo, v) => argInfo.copy(value = v) } - val res = warm.callConstructor(ctor, argInfos2, source) val task = ThisRef(klass, outer, ctor, args2) this.addTask(task) - Result(warm, res.errors) + Result(warm, Errors.empty) case Cold => val error = CallCold(ctor, source, trace1.toVector) @@ -651,11 +669,10 @@ object Semantic { val argsWidened = args.map(_.value).widenArgs val argInfos2 = args.zip(argsWidened).map { (argInfo, v) => argInfo.copy(value = v) } - val warm = Warm(klass, outer, ctor, argsWidened) - val res = warm.callConstructor(ctor, argInfos2, source) + val warm = Warm(klass, outer, ctor, argsWidened).ensureInit() val task = ThisRef(klass, outer, ctor, argsWidened) this.addTask(task) - Result(warm, res.errors) + Result(warm, Errors.empty) case Fun(body, thisV, klass, env) => report.error("unexpected tree in instantiating a function, fun = " + body.show, source) @@ -1280,7 +1297,7 @@ object Semantic { thisV.updateOuter(cls, res.value) // follow constructor - if cls.hasSource && !thisV.isWarm then + if cls.hasSource then tasks.append { () => printer.println("init super class " + cls.show) val res2 = thisV.callConstructor(ctor, args, source) From bc98ce8f5ec1a2b217606911fc80ca0e1f2484a9 Mon Sep 17 00:00:00 2001 From: Fengyun Liu Date: Thu, 9 Sep 2021 00:41:55 +0200 Subject: [PATCH 32/52] Use correct trace --- compiler/src/dotty/tools/dotc/transform/init/Semantic.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala b/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala index 9022a77f0aba..e7ffc3ccf3c4 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala @@ -913,7 +913,7 @@ object Semantic { val paramValues = tpl.constr.termParamss.flatten.map(param => param.symbol -> Hot).toMap given Promoted = Promoted.empty - given Trace = Trace.empty + given Trace = task.trace given Env = Env(paramValues) eval(tpl, thisRef, thisRef.klass) From b27d58bbefb2d0d4d0e0b7099fcec3afc079ea7e Mon Sep 17 00:00:00 2001 From: Fengyun Liu Date: Thu, 9 Sep 2021 01:55:02 +0200 Subject: [PATCH 33/52] Populate class parameters for warm objects in a heap prepare step --- .../tools/dotc/transform/init/Semantic.scala | 39 ++++++++++++++----- 1 file changed, 30 insertions(+), 9 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala b/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala index e7ffc3ccf3c4..c4c17c358d7a 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala @@ -174,7 +174,28 @@ object Semantic { map = map.updated(ref, obj) def snapshot(): Heap = new Heap(map) - def restore(h: Heap) = this.map = h.map + + /** Recompute the newly created warm objects with the updated cache. + * + * The computation only covers class parameters and outers. Class fields are ignored and + * are lazily evaluated and cached. + * + * The method must be called after the call `Cache.prepare()`. + */ + def prepare(heapBefore: Heap)(using State, Context) = + this.map.keys.foreach { + case thisRef: ThisRef => + this.map = this.map - thisRef + case warm: Warm if !heapBefore.contains(warm) => + this.map = this.map - warm + given Env = Env.empty + given Trace = Trace.empty + given Promoted = Promoted.empty + warm.ensureObjectExists().ensureInit() + case _ => + } + + override def toString() = map.toString() } /** Note: don't use `val` to avoid incorrect sharing */ @@ -311,10 +332,9 @@ object Semantic { def assume(value: Value, expr: Tree, cacheResult: Boolean)(fun: => Result): Contextual[Result] = val assumeValue: Value = if last.contains(value, expr) then - // Due to heap reverting, the object corresponding to a reference may not exist in the heap. + // The object corresponding to ThisRef may not exist in the heap. See `Heap.prepare`. last.get(value, expr) match case ref: ThisRef => ref.ensureObjectExists() - case ref: Warm => ref.ensureObjectExists().ensureInit() case v => v else last.put(value, expr, Hot) @@ -356,7 +376,7 @@ object Semantic { * - Reset changed flag * - Reset current cache (last cache already synced in `assume`) */ - def iterate() = { + def prepare() = { changed = false current = mutable.Map.empty } @@ -885,18 +905,20 @@ object Semantic { case task :: rest => checkedTasks = checkedTasks + task + // must be before heap snapshot + task.value.ensureObjectExists() val heapBefore = heap.snapshot() val res = doTask(task) res.errors.foreach(_.issue) if cache.hasChanged && res.errors.isEmpty then - // discard heap changes - heap.restore(heapBefore) + // must call cache.prepare() first + cache.prepare() + heap.prepare(heapBefore) else cache.commit() pendingTasks = rest - - cache.iterate() + cache.prepare() work() case _ => @@ -907,7 +929,6 @@ object Semantic { */ private def doTask(task: Task)(using State, Context): Result = log("checking " + task) { val thisRef = task.value - thisRef.ensureObjectExists() val tpl = thisRef.klass.defTree.asInstanceOf[TypeDef].rhs.asInstanceOf[Template] val paramValues = tpl.constr.termParamss.flatten.map(param => param.symbol -> Hot).toMap From 99df102e8098bf4eb102e382eb24a958188493bb Mon Sep 17 00:00:00 2001 From: Fengyun Liu Date: Thu, 9 Sep 2021 03:09:02 +0200 Subject: [PATCH 34/52] Try to check warm values immediately --- .../tools/dotc/transform/init/Semantic.scala | 61 +++++++++++-------- 1 file changed, 36 insertions(+), 25 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala b/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala index c4c17c358d7a..c2de48a09cfd 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala @@ -122,11 +122,10 @@ object Semantic { // Somehow Dotty uses the one in the class parameters given Heap = state.heap if objekt.outers.size <= 1 then - val tpl = klass.defTree.asInstanceOf[TypeDef].rhs.asInstanceOf[Template] - val termParamss = ctor.defTree.asInstanceOf[DefDef].termParamss - val paramValues = termParamss.flatten.zip(args).map((param, v) => param.symbol -> v).toMap - given Env = Env(paramValues) - init(tpl, this, klass) + state.populateWarm { + val tpl = klass.defTree.asInstanceOf[TypeDef].rhs.asInstanceOf[Template] + this.callConstructor(ctor, args.map(arg => ArgInfo(arg, EmptyTree)), tpl) + } end if this } @@ -184,8 +183,6 @@ object Semantic { */ def prepare(heapBefore: Heap)(using State, Context) = this.map.keys.foreach { - case thisRef: ThisRef => - this.map = this.map - thisRef case warm: Warm if !heapBefore.contains(warm) => this.map = this.map - warm given Env = Env.empty @@ -195,6 +192,13 @@ object Semantic { case _ => } + // ThisRef might be used in `ensureInit` + this.map.keys.foreach { + case thisRef: ThisRef => + this.map = this.map - thisRef + case _ => + } + override def toString() = map.toString() } @@ -426,7 +430,16 @@ object Semantic { // ----- State -------------------------------------------- /** Global state of the checker */ - class State(val cache: Cache, val heap: Heap, val workList: WorkList) + class State(val cache: Cache, val heap: Heap, val workList: WorkList, var isPopulatingWarm: Boolean = false) { + // TODO: problematic for nested `init`. + def populateWarm[T](fun: State ?=> T) = { + val last = isPopulatingWarm + isPopulatingWarm = true + val res = fun(using this) + isPopulatingWarm = last + res + } + } given (using s: State): Heap = s.heap given (using s: State): Cache = s.cache @@ -608,7 +621,7 @@ object Semantic { report.error("unexpected constructor call, meth = " + ctor + ", value = " + value, source) Result(Hot, Nil) - case ref: Warm => + case ref: Warm if state.isPopulatingWarm => val trace1 = trace.add(source) if ctor.hasSource then given Trace = trace1 @@ -627,20 +640,18 @@ object Semantic { else Result(Hot, Nil) - case ref: ThisRef => + case ref: Ref => val trace1 = trace.add(source) if ctor.hasSource then given Trace = trace1 val cls = ctor.owner.enclosingClass.asClass val ddef = ctor.defTree.asInstanceOf[DefDef] - val env2 = Env(ddef, args.map(_.value).widenArgs) + given Env= Env(ddef, args.map(_.value).widenArgs) if ctor.isPrimaryConstructor then - given Env = env2 val tpl = cls.defTree.asInstanceOf[TypeDef].rhs.asInstanceOf[Template] val res = withTrace(trace.add(cls.defTree)) { eval(tpl, ref, cls, cacheResult = true) } Result(ref, res.errors) else - given Env = env2 eval(ddef.rhs, ref, cls, cacheResult = true) else if ref.canIgnoreMethodCall(ctor) then Result(Hot, Nil) @@ -670,11 +681,10 @@ object Semantic { Result(Hot, Errors.empty) else val outer = Hot - val warm = Warm(klass, outer, ctor, args2).ensureInit() + val warm = Warm(klass, outer, ctor, args2) val argInfos2 = args.zip(args2).map { (argInfo, v) => argInfo.copy(value = v) } - val task = ThisRef(klass, outer, ctor, args2) - this.addTask(task) - Result(warm, Errors.empty) + val res = warm.callConstructor(ctor, argInfos2, source) + Result(warm, res.errors) case Cold => val error = CallCold(ctor, source, trace1.toVector) @@ -682,17 +692,18 @@ object Semantic { case ref: Ref => given Trace = trace1 - // widen the outer to finitize addresses + // widen the outer to finitize the domain val outer = ref match - case Warm(_, _: Warm, _, _) => Cold + case warm @ Warm(_, _: Ref, _, _) => + // the widened warm object might not exist in the heap + warm.copy(outer = Cold).ensureObjectExists().ensureInit() case _ => ref val argsWidened = args.map(_.value).widenArgs val argInfos2 = args.zip(argsWidened).map { (argInfo, v) => argInfo.copy(value = v) } - val warm = Warm(klass, outer, ctor, argsWidened).ensureInit() - val task = ThisRef(klass, outer, ctor, argsWidened) - this.addTask(task) - Result(warm, Errors.empty) + val warm = Warm(klass, outer, ctor, argsWidened) + val res = warm.callConstructor(ctor, argInfos2, source) + Result(warm, res.errors) case Fun(body, thisV, klass, env) => report.error("unexpected tree in instantiating a function, fun = " + body.show, source) @@ -1389,7 +1400,7 @@ object Semantic { var fieldsChanged = true // class body - if (!thisV.isWarm) tpl.body.foreach { + if !state.isPopulatingWarm then tpl.body.foreach { case vdef : ValDef if !vdef.symbol.is(Flags.Lazy) && !vdef.rhs.isEmpty => given Env = Env.empty val res = eval(vdef.rhs, thisV, klass) @@ -1400,7 +1411,7 @@ object Semantic { case _: MemberDef => case tree => - if fieldsChanged then thisV.asInstanceOf[ThisRef].tryPromoteCurrentObject + if fieldsChanged && !thisV.isWarm then thisV.asInstanceOf[ThisRef].tryPromoteCurrentObject fieldsChanged = false given Env = Env.empty From 4163fd34ab5298cdc22d0d7205b45224f16af105 Mon Sep 17 00:00:00 2001 From: Fengyun Liu Date: Thu, 9 Sep 2021 03:33:48 +0200 Subject: [PATCH 35/52] Fix crash: make sure object fresh before calling init The problem is that all warm objects are populated with outers and class parameters in Heap.prepare(). If we call `init` on those objects, the only-set-once invariant will be violated and crash. --- .../dotty/tools/dotc/transform/init/Semantic.scala | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala b/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala index c2de48a09cfd..bd9ef1cf787f 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala @@ -80,6 +80,10 @@ object Semantic { this } + def ensureFresh()(using Heap): this.type = + val obj = Objekt(this.klass, fields = Map.empty, outers = Map(this.klass -> this.outer)) + heap.update(this, obj) + this /** Update field value of the abstract object * @@ -681,7 +685,7 @@ object Semantic { Result(Hot, Errors.empty) else val outer = Hot - val warm = Warm(klass, outer, ctor, args2) + val warm = Warm(klass, outer, ctor, args2).ensureFresh() val argInfos2 = args.zip(args2).map { (argInfo, v) => argInfo.copy(value = v) } val res = warm.callConstructor(ctor, argInfos2, source) Result(warm, res.errors) @@ -701,7 +705,7 @@ object Semantic { val argsWidened = args.map(_.value).widenArgs val argInfos2 = args.zip(argsWidened).map { (argInfo, v) => argInfo.copy(value = v) } - val warm = Warm(klass, outer, ctor, argsWidened) + val warm = Warm(klass, outer, ctor, argsWidened).ensureFresh() val res = warm.callConstructor(ctor, argInfos2, source) Result(warm, res.errors) @@ -916,8 +920,7 @@ object Semantic { case task :: rest => checkedTasks = checkedTasks + task - // must be before heap snapshot - task.value.ensureObjectExists() + task.value.ensureFresh() val heapBefore = heap.snapshot() val res = doTask(task) res.errors.foreach(_.issue) From 683d537f0db3759a38a7f126c26c91a51373b01f Mon Sep 17 00:00:00 2001 From: Fengyun Liu Date: Thu, 9 Sep 2021 03:39:00 +0200 Subject: [PATCH 36/52] Be more sensitive for values Warm(outer = ThisRef) --- compiler/src/dotty/tools/dotc/transform/init/Semantic.scala | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala b/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala index bd9ef1cf787f..79465d674c25 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala @@ -188,11 +188,10 @@ object Semantic { def prepare(heapBefore: Heap)(using State, Context) = this.map.keys.foreach { case warm: Warm if !heapBefore.contains(warm) => - this.map = this.map - warm given Env = Env.empty given Trace = Trace.empty given Promoted = Promoted.empty - warm.ensureObjectExists().ensureInit() + warm.ensureFresh().ensureInit() case _ => } @@ -698,7 +697,7 @@ object Semantic { given Trace = trace1 // widen the outer to finitize the domain val outer = ref match - case warm @ Warm(_, _: Ref, _, _) => + case warm @ Warm(_, _: Warm, _, _) => // the widened warm object might not exist in the heap warm.copy(outer = Cold).ensureObjectExists().ensureInit() case _ => ref From ccbb355d240f6eb1fa5d6daeeb2dc26540325320 Mon Sep 17 00:00:00 2001 From: Fengyun Liu Date: Thu, 9 Sep 2021 03:54:37 +0200 Subject: [PATCH 37/52] Fix non-determinism in test --- .../src/dotty/tools/dotc/transform/init/Semantic.scala | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala b/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala index 79465d674c25..f73287ca0bd5 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala @@ -204,9 +204,6 @@ object Semantic { override def toString() = map.toString() } - - /** Note: don't use `val` to avoid incorrect sharing */ - private[Semantic] def empty: Heap = new Heap(Map.empty) } type Heap = Heap.Heap @@ -389,8 +386,6 @@ object Semantic { } } - val empty: Cache = new Cache() - extension (cache: CacheStore) def contains(value: Value, expr: Tree) = cache.contains(value) && cache(value).contains(expr) def get(value: Value, expr: Tree): Value = cache(value)(expr) @@ -975,7 +970,7 @@ object Semantic { * } */ def withInitialState[T](work: State ?=> T): T = { - val initialState = State(Cache.empty, Heap.empty, new WorkList) + val initialState = State(new Cache, new Heap(Map.empty), new WorkList) work(using initialState) } From fc7654962e364d8353198c5581edbdd43b8b6bac Mon Sep 17 00:00:00 2001 From: Fengyun Liu Date: Wed, 15 Sep 2021 21:45:32 +0200 Subject: [PATCH 38/52] Make isPopulatingParams a flag in Warm --- .../tools/dotc/transform/init/Semantic.scala | 144 +++++++++--------- 1 file changed, 71 insertions(+), 73 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala b/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala index f73287ca0bd5..480d9a4223df 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala @@ -74,11 +74,7 @@ object Semantic { def ensureObjectExists()(using Heap): this.type = if heap.contains(this) then this - else { - val obj = Objekt(this.klass, fields = Map.empty, outers = Map(this.klass -> this.outer)) - heap.update(this, obj) - this - } + else ensureFresh() def ensureFresh()(using Heap): this.type = val obj = Objekt(this.klass, fields = Map.empty, outers = Map(this.klass -> this.outer)) @@ -107,31 +103,40 @@ object Semantic { } /** A reference to the object under initialization pointed by `this` */ - case class ThisRef(klass: ClassSymbol, outer: Value, ctor: Symbol, args: List[Value])(using @constructorOnly h: Heap) extends Ref { - ensureObjectExists() - } + case class ThisRef(klass: ClassSymbol, outer: Value, ctor: Symbol, args: List[Value]) extends Ref /** An object with all fields initialized but reaches objects under initialization * * We need to restrict nesting levels of `outer` to finitize the domain. */ - case class Warm(klass: ClassSymbol, outer: Value, ctor: Symbol, args: List[Value])(using @constructorOnly h: Heap) extends Ref { - ensureObjectExists() + case class Warm(klass: ClassSymbol, outer: Value, ctor: Symbol, args: List[Value]) extends Ref { + + /** If a warm value is in the process of populating parameters, class bodies are not executed. */ + private var populatingParams: Boolean = false + + def isPopulatingParams = populatingParams /** Ensure that outers and class parameters are initialized. * * Fields in class body are not initialized. */ - def ensureInit(): Contextual[this.type] = + def populateParams(): Contextual[this.type] = log("populating parameters", printer, (_: Warm).objekt.toString) { + assert(!populatingParams, "the object is already populating parameters") // Somehow Dotty uses the one in the class parameters + populatingParams = true given Heap = state.heap - if objekt.outers.size <= 1 then - state.populateWarm { - val tpl = klass.defTree.asInstanceOf[TypeDef].rhs.asInstanceOf[Template] - this.callConstructor(ctor, args.map(arg => ArgInfo(arg, EmptyTree)), tpl) - } - end if + val tpl = klass.defTree.asInstanceOf[TypeDef].rhs.asInstanceOf[Template] + this.callConstructor(ctor, args.map(arg => ArgInfo(arg, EmptyTree)), tpl) + populatingParams = false this + } + + def ensureObjectExistsAndPopulated(): Contextual[this.type] = + if heap.contains(this) then this + else ensureFresh().populateParams() + + def ensureObjectFreshAndPopulated(): Contextual[this.type] = + ensureFresh().populateParams() } /** A function value */ @@ -187,15 +192,20 @@ object Semantic { */ def prepare(heapBefore: Heap)(using State, Context) = this.map.keys.foreach { - case warm: Warm if !heapBefore.contains(warm) => - given Env = Env.empty - given Trace = Trace.empty - given Promoted = Promoted.empty - warm.ensureFresh().ensureInit() + case warm: Warm => + if heapBefore.contains(warm) then + assert(heapBefore(warm) == this(warm)) + else + // We cannot simply remove the object, as the values in the + // updated cache may refer to the warm object. + given Env = Env.empty + given Trace = Trace.empty + given Promoted = Promoted.empty + warm.ensureObjectFreshAndPopulated() case _ => } - // ThisRef might be used in `ensureInit` + // ThisRef might be used in `populateParams` this.map.keys.foreach { case thisRef: ThisRef => this.map = this.map - thisRef @@ -336,10 +346,7 @@ object Semantic { def assume(value: Value, expr: Tree, cacheResult: Boolean)(fun: => Result): Contextual[Result] = val assumeValue: Value = if last.contains(value, expr) then - // The object corresponding to ThisRef may not exist in the heap. See `Heap.prepare`. - last.get(value, expr) match - case ref: ThisRef => ref.ensureObjectExists() - case v => v + last.get(value, expr) else last.put(value, expr, Hot) Hot @@ -428,16 +435,7 @@ object Semantic { // ----- State -------------------------------------------- /** Global state of the checker */ - class State(val cache: Cache, val heap: Heap, val workList: WorkList, var isPopulatingWarm: Boolean = false) { - // TODO: problematic for nested `init`. - def populateWarm[T](fun: State ?=> T) = { - val last = isPopulatingWarm - isPopulatingWarm = true - val res = fun(using this) - isPopulatingWarm = last - res - } - } + class State(val cache: Cache, val heap: Heap, val workList: WorkList) given (using s: State): Heap = s.heap given (using s: State): Cache = s.cache @@ -619,44 +617,44 @@ object Semantic { report.error("unexpected constructor call, meth = " + ctor + ", value = " + value, source) Result(Hot, Nil) - case ref: Warm if state.isPopulatingWarm => - val trace1 = trace.add(source) - if ctor.hasSource then - given Trace = trace1 - val cls = ctor.owner.enclosingClass.asClass - val ddef = ctor.defTree.asInstanceOf[DefDef] - given Env = Env(ddef, args.map(_.value).widenArgs) - if ctor.isPrimaryConstructor then - val tpl = cls.defTree.asInstanceOf[TypeDef].rhs.asInstanceOf[Template] - init(tpl, ref, cls) - else - val initCall = ddef.rhs match - case Block(call :: _, _) => call - case call => call - eval(initCall, ref, cls) - end if + case ref: Warm if ref.isPopulatingParams => + val trace1 = trace.add(source) + if ctor.hasSource then + given Trace = trace1 + val cls = ctor.owner.enclosingClass.asClass + val ddef = ctor.defTree.asInstanceOf[DefDef] + given Env = Env(ddef, args.map(_.value).widenArgs) + if ctor.isPrimaryConstructor then + val tpl = cls.defTree.asInstanceOf[TypeDef].rhs.asInstanceOf[Template] + init(tpl, ref, cls) else - Result(Hot, Nil) + val initCall = ddef.rhs match + case Block(call :: _, _) => call + case call => call + eval(initCall, ref, cls) + end if + else + Result(Hot, Nil) case ref: Ref => - val trace1 = trace.add(source) - if ctor.hasSource then - given Trace = trace1 - val cls = ctor.owner.enclosingClass.asClass - val ddef = ctor.defTree.asInstanceOf[DefDef] - given Env= Env(ddef, args.map(_.value).widenArgs) - if ctor.isPrimaryConstructor then - val tpl = cls.defTree.asInstanceOf[TypeDef].rhs.asInstanceOf[Template] - val res = withTrace(trace.add(cls.defTree)) { eval(tpl, ref, cls, cacheResult = true) } - Result(ref, res.errors) - else - eval(ddef.rhs, ref, cls, cacheResult = true) - else if ref.canIgnoreMethodCall(ctor) then - Result(Hot, Nil) + val trace1 = trace.add(source) + if ctor.hasSource then + given Trace = trace1 + val cls = ctor.owner.enclosingClass.asClass + val ddef = ctor.defTree.asInstanceOf[DefDef] + given Env= Env(ddef, args.map(_.value).widenArgs) + if ctor.isPrimaryConstructor then + val tpl = cls.defTree.asInstanceOf[TypeDef].rhs.asInstanceOf[Template] + val res = withTrace(trace.add(cls.defTree)) { eval(tpl, ref, cls, cacheResult = true) } + Result(ref, res.errors) else - // no source code available - val error = CallUnknown(ctor, source, trace.toVector) - Result(Hot, error :: Nil) + eval(ddef.rhs, ref, cls, cacheResult = true) + else if ref.canIgnoreMethodCall(ctor) then + Result(Hot, Nil) + else + // no source code available + val error = CallUnknown(ctor, source, trace.toVector) + Result(Hot, error :: Nil) } } @@ -694,7 +692,7 @@ object Semantic { val outer = ref match case warm @ Warm(_, _: Warm, _, _) => // the widened warm object might not exist in the heap - warm.copy(outer = Cold).ensureObjectExists().ensureInit() + warm.copy(outer = Cold).ensureObjectExistsAndPopulated() case _ => ref val argsWidened = args.map(_.value).widenArgs @@ -1397,7 +1395,7 @@ object Semantic { var fieldsChanged = true // class body - if !state.isPopulatingWarm then tpl.body.foreach { + if !thisV.isWarm || !thisV.asInstanceOf[Warm].isPopulatingParams then tpl.body.foreach { case vdef : ValDef if !vdef.symbol.is(Flags.Lazy) && !vdef.rhs.isEmpty => given Env = Env.empty val res = eval(vdef.rhs, thisV, klass) From 191877a58a62167cfcefba22dc11550d7e18407e Mon Sep 17 00:00:00 2001 From: Fengyun Liu Date: Thu, 16 Sep 2021 10:22:52 +0200 Subject: [PATCH 39/52] Make heap as part of cache Two motivations: First, it's close to the essence of its functionality. Second, it's easier to update the cache state in fixed-pointed computation. --- .../tools/dotc/transform/init/Semantic.scala | 211 +++++++++--------- 1 file changed, 102 insertions(+), 109 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala b/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala index 480d9a4223df..dc9a0b4d065a 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala @@ -70,36 +70,6 @@ object Semantic { sealed abstract class Ref extends Value { def klass: ClassSymbol def outer: Value - def objekt(using Heap): Objekt = heap(this) - - def ensureObjectExists()(using Heap): this.type = - if heap.contains(this) then this - else ensureFresh() - - def ensureFresh()(using Heap): this.type = - val obj = Objekt(this.klass, fields = Map.empty, outers = Map(this.klass -> this.outer)) - heap.update(this, obj) - this - - /** Update field value of the abstract object - * - * Invariant: fields are immutable and only set once - */ - def updateField(field: Symbol, value: Value)(using Heap, Context): Unit = - val obj = objekt - assert(!obj.hasField(field), field.show + " already init, new = " + value + ", old = " + obj.field(field) + ", ref = " + this) - val obj2 = obj.copy(fields = obj.fields.updated(field, value)) - heap.update(this, obj2) - - /** Update the immediate outer of the given `klass` of the abstract object - * - * Invariant: outers are immutable and only set once - */ - def updateOuter(klass: ClassSymbol, value: Value)(using Heap, Context): Unit = - val obj = objekt - assert(!obj.hasOuter(klass), klass.show + " already has outer, new = " + value + ", old = " + obj.outer(klass) + ", ref = " + this) - val obj2 = obj.copy(outers = obj.outers.updated(klass, value)) - heap.update(this, obj2) } /** A reference to the object under initialization pointed by `this` */ @@ -122,9 +92,7 @@ object Semantic { */ def populateParams(): Contextual[this.type] = log("populating parameters", printer, (_: Warm).objekt.toString) { assert(!populatingParams, "the object is already populating parameters") - // Somehow Dotty uses the one in the class parameters populatingParams = true - given Heap = state.heap val tpl = klass.defTree.asInstanceOf[TypeDef].rhs.asInstanceOf[Template] this.callConstructor(ctor, args.map(arg => ArgInfo(arg, EmptyTree)), tpl) populatingParams = false @@ -132,11 +100,11 @@ object Semantic { } def ensureObjectExistsAndPopulated(): Contextual[this.type] = - if heap.contains(this) then this - else ensureFresh().populateParams() + if cache.containsObject(this) then this + else this.ensureFresh().populateParams() def ensureObjectFreshAndPopulated(): Contextual[this.type] = - ensureFresh().populateParams() + this.ensureFresh().populateParams() } /** A function value */ @@ -167,60 +135,6 @@ object Semantic { def hasField(f: Symbol) = fields.contains(f) } - /** Abstract heap stores abstract objects - * - * The heap serves as cache of summaries for warm objects and is shared for checking all classes. - * - * The fact that objects of `ThisRef` are stored in heap is just an engineering convenience. - * Technically, we can also store the object directly in `ThisRef`. - */ - object Heap { - class Heap(private var map: Map[Ref, Objekt]) { - def contains(ref: Ref): Boolean = map.contains(ref) - def apply(ref: Ref): Objekt = map(ref) - def update(ref: Ref, obj: Objekt): Unit = - map = map.updated(ref, obj) - - def snapshot(): Heap = new Heap(map) - - /** Recompute the newly created warm objects with the updated cache. - * - * The computation only covers class parameters and outers. Class fields are ignored and - * are lazily evaluated and cached. - * - * The method must be called after the call `Cache.prepare()`. - */ - def prepare(heapBefore: Heap)(using State, Context) = - this.map.keys.foreach { - case warm: Warm => - if heapBefore.contains(warm) then - assert(heapBefore(warm) == this(warm)) - else - // We cannot simply remove the object, as the values in the - // updated cache may refer to the warm object. - given Env = Env.empty - given Trace = Trace.empty - given Promoted = Promoted.empty - warm.ensureObjectFreshAndPopulated() - case _ => - } - - // ThisRef might be used in `populateParams` - this.map.keys.foreach { - case thisRef: ThisRef => - this.map = this.map - thisRef - case _ => - } - - override def toString() = map.toString() - } - } - type Heap = Heap.Heap - - inline def heap(using h: Heap): Heap = h - - import Heap._ - /** The environment for method parameters * * For performance and usability, we restrict parameters to be either `Cold` @@ -327,6 +241,7 @@ object Semantic { object Cache { opaque type CacheStore = mutable.Map[Value, EqHashMap[Tree, Value]] + private type Heap = Map[Ref, Objekt] class Cache { private val last: CacheStore = mutable.Map.empty @@ -334,6 +249,18 @@ object Semantic { private val stable: CacheStore = mutable.Map.empty private var changed: Boolean = false + /** Abstract heap stores abstract objects + * + * The heap serves as cache of summaries for warm objects and is shared for checking all classes. + * + * The fact that objects of `ThisRef` are stored in heap is just an engineering convenience. + * Technically, we can also store the object directly in `ThisRef`. + */ + private var heap: Heap = Map.empty + + /** Used to easily revert heap changes. */ + private var heapBefore: Heap = Map.empty + def hasChanged = changed def contains(value: Value, expr: Tree) = @@ -370,13 +297,11 @@ object Semantic { actual end assume - /** Commit current cache to stable cache. - * - * TODO: It's useless to cache value for ThisRef. - */ - def commit() = + /** Commit current cache to stable cache. */ + private def commitToStableCache() = current.foreach { (v, m) => - m.iterator.foreach { (e, res) => + // It's useless to cache value for ThisRef. + if v.isWarm then m.iterator.foreach { (e, res) => stable.put(v, e, res) } } @@ -384,13 +309,53 @@ object Semantic { /** Prepare cache for the next iteration * - * - Reset changed flag - * - Reset current cache (last cache already synced in `assume`) + * 1. Reset changed flag + * + * 2. Reset current cache (last cache already synced in `assume`) + * + * 3. Recompute the newly created warm objects with the updated cache. + * + * The computation only covers class parameters and outers. Class + * fields are ignored and are lazily evaluated and cached. + * + * This step should be after the first two steps so that the populated + * parameter are re-computed from the updated input cache. + * */ - def prepare() = { + def prepareForNextIteration(isStable: Boolean)(using State, Context) = + if isStable then this.commitToStableCache() + changed = false current = mutable.Map.empty - } + + if !isStable then revertHeapChanges() + heapBefore = this.heap + + def revertHeapChanges()(using State, Context) = + this.heap.keys.foreach { + case warm: Warm => + if heapBefore.contains(warm) then + this.heap = heap.updated(warm, heapBefore(warm)) + else + // We cannot simply remove the object, as the values in the + // updated cache may refer to the warm object. + given Env = Env.empty + given Trace = Trace.empty + given Promoted = Promoted.empty + warm.ensureObjectFreshAndPopulated() + case _ => + } + + // ThisRef objects are not reachable, thus it's fine to leave them in + // the heap + end revertHeapChanges + + def updateObject(ref: Ref, obj: Objekt) = + this.heap = this.heap.updated(ref, obj) + + def containsObject(ref: Ref) = heap.contains(ref) + + def getObject(ref: Ref) = heap(ref) } extension (cache: CacheStore) @@ -408,7 +373,6 @@ object Semantic { inline def cache(using c: Cache): Cache = c - /** Result of abstract interpretation */ case class Result(value: Value, errors: Seq[Error]) { def show(using Context) = value.show + ", errors = " + errors.map(_.toString) @@ -435,9 +399,8 @@ object Semantic { // ----- State -------------------------------------------- /** Global state of the checker */ - class State(val cache: Cache, val heap: Heap, val workList: WorkList) + class State(val cache: Cache, val workList: WorkList) - given (using s: State): Heap = s.heap given (using s: State): Cache = s.cache given (using s: State): WorkList = s.workList @@ -496,6 +459,40 @@ object Semantic { def widenArgs: List[Value] = values.map(_.widenArg).toList + + extension (ref: Ref) + def objekt(using Cache): Objekt = cache.getObject(ref) + + def ensureObjectExists()(using Cache): ref.type = + if cache.containsObject(ref) then ref + else ensureFresh() + + def ensureFresh()(using Cache): ref.type = + val obj = Objekt(ref.klass, fields = Map.empty, outers = Map(ref.klass -> ref.outer)) + cache.updateObject(ref, obj) + ref + + /** Update field value of the abstract object + * + * Invariant: fields are immutable and only set once + */ + def updateField(field: Symbol, value: Value)(using Cache, Context): Unit = + val obj = objekt + assert(!obj.hasField(field), field.show + " already init, new = " + value + ", old = " + obj.field(field) + ", ref = " + ref) + val obj2 = obj.copy(fields = obj.fields.updated(field, value)) + cache.updateObject(ref, obj2) + + /** Update the immediate outer of the given `klass` of the abstract object + * + * Invariant: outers are immutable and only set once + */ + def updateOuter(klass: ClassSymbol, value: Value)(using Cache, Context): Unit = + val obj = objekt + assert(!obj.hasOuter(klass), klass.show + " already has outer, new = " + value + ", old = " + obj.outer(klass) + ", ref = " + ref) + val obj2 = obj.copy(outers = obj.outers.updated(klass, value)) + cache.updateObject(ref, obj2) + end extension + extension (value: Value) def select(field: Symbol, source: Tree, needResolve: Boolean = true): Contextual[Result] = log("select " + field.show + ", this = " + value, printer, (_: Result).show) { if promoted.isCurrentObjectPromoted then Result(Hot, Nil) @@ -913,18 +910,14 @@ object Semantic { checkedTasks = checkedTasks + task task.value.ensureFresh() - val heapBefore = heap.snapshot() val res = doTask(task) res.errors.foreach(_.issue) if cache.hasChanged && res.errors.isEmpty then - // must call cache.prepare() first - cache.prepare() - heap.prepare(heapBefore) + cache.prepareForNextIteration(isStable = false) else - cache.commit() + cache.prepareForNextIteration(isStable = true) pendingTasks = rest - cache.prepare() work() case _ => @@ -968,7 +961,7 @@ object Semantic { * } */ def withInitialState[T](work: State ?=> T): T = { - val initialState = State(new Cache, new Heap(Map.empty), new WorkList) + val initialState = State(new Cache, new WorkList) work(using initialState) } From f5540f5e3a0588fb0c3e297b7ed0a6ccafa3d065 Mon Sep 17 00:00:00 2001 From: Fengyun Liu Date: Mon, 20 Sep 2021 21:25:02 +0200 Subject: [PATCH 40/52] Update test --- tests/init/neg/inner-loop.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/init/neg/inner-loop.scala b/tests/init/neg/inner-loop.scala index 51361f261450..c6d5c615580c 100644 --- a/tests/init/neg/inner-loop.scala +++ b/tests/init/neg/inner-loop.scala @@ -1,6 +1,6 @@ class Outer { outer => class Inner extends Outer { - val x = 5 + outer.n // error + val x = 5 + outer.n } val inner = new Inner val n = 6 // error From a15a06c66c3df0ff4286202b1487dc03a6fa871d Mon Sep 17 00:00:00 2001 From: Fengyun Liu Date: Mon, 20 Sep 2021 22:00:04 +0200 Subject: [PATCH 41/52] Add more debugging information --- .../src/dotty/tools/dotc/transform/init/Semantic.scala | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala b/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala index dc9a0b4d065a..ffd1f359d7a4 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala @@ -332,6 +332,7 @@ object Semantic { heapBefore = this.heap def revertHeapChanges()(using State, Context) = + printer.println("reverting heap changes") this.heap.keys.foreach { case warm: Warm => if heapBefore.contains(warm) then @@ -342,6 +343,7 @@ object Semantic { given Env = Env.empty given Trace = Trace.empty given Promoted = Promoted.empty + printer.println("resetting " + warm) warm.ensureObjectFreshAndPopulated() case _ => } @@ -469,6 +471,7 @@ object Semantic { def ensureFresh()(using Cache): ref.type = val obj = Objekt(ref.klass, fields = Map.empty, outers = Map(ref.klass -> ref.outer)) + printer.println("reset object " + ref) cache.updateObject(ref, obj) ref @@ -476,21 +479,23 @@ object Semantic { * * Invariant: fields are immutable and only set once */ - def updateField(field: Symbol, value: Value)(using Cache, Context): Unit = + def updateField(field: Symbol, value: Value)(using Cache, Context): Unit = log("set field " + field + " of " + ref + " to " + value) { val obj = objekt assert(!obj.hasField(field), field.show + " already init, new = " + value + ", old = " + obj.field(field) + ", ref = " + ref) val obj2 = obj.copy(fields = obj.fields.updated(field, value)) cache.updateObject(ref, obj2) + } /** Update the immediate outer of the given `klass` of the abstract object * * Invariant: outers are immutable and only set once */ - def updateOuter(klass: ClassSymbol, value: Value)(using Cache, Context): Unit = + def updateOuter(klass: ClassSymbol, value: Value)(using Cache, Context): Unit = log("set outer " + klass + " of " + ref + " to " + value) { val obj = objekt assert(!obj.hasOuter(klass), klass.show + " already has outer, new = " + value + ", old = " + obj.outer(klass) + ", ref = " + ref) val obj2 = obj.copy(outers = obj.outers.updated(klass, value)) cache.updateObject(ref, obj2) + } end extension extension (value: Value) From 46793f130983a841492cbab1058fedab03edccc3 Mon Sep 17 00:00:00 2001 From: Fengyun Liu Date: Mon, 20 Sep 2021 23:12:27 +0200 Subject: [PATCH 42/52] Lazily populate warm objects --- .../tools/dotc/transform/init/Semantic.scala | 71 ++++++++----------- 1 file changed, 30 insertions(+), 41 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala b/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala index ffd1f359d7a4..ee1756f05746 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala @@ -90,7 +90,7 @@ object Semantic { * * Fields in class body are not initialized. */ - def populateParams(): Contextual[this.type] = log("populating parameters", printer, (_: Warm).objekt.toString) { + private def populateParams(): Contextual[this.type] = log("populating parameters", printer, (_: Warm).objekt.toString) { assert(!populatingParams, "the object is already populating parameters") populatingParams = true val tpl = klass.defTree.asInstanceOf[TypeDef].rhs.asInstanceOf[Template] @@ -244,7 +244,7 @@ object Semantic { private type Heap = Map[Ref, Objekt] class Cache { - private val last: CacheStore = mutable.Map.empty + private var last: CacheStore = mutable.Map.empty private var current: CacheStore = mutable.Map.empty private val stable: CacheStore = mutable.Map.empty private var changed: Boolean = false @@ -258,8 +258,8 @@ object Semantic { */ private var heap: Heap = Map.empty - /** Used to easily revert heap changes. */ - private var heapBefore: Heap = Map.empty + /** Used to revert heap to last stable heap. */ + private var heapStable: Heap = Map.empty def hasChanged = changed @@ -305,7 +305,6 @@ object Semantic { stable.put(v, e, res) } } - current = mutable.Map.empty /** Prepare cache for the next iteration * @@ -323,34 +322,16 @@ object Semantic { * */ def prepareForNextIteration(isStable: Boolean)(using State, Context) = - if isStable then this.commitToStableCache() + if isStable then + this.commitToStableCache() + this.heapStable = this.heap + // If the current iteration is not stable, we need to use `last` for the next iteration, + // which already contains the updated value from the current iteration. + this.last = mutable.Map.empty - changed = false - current = mutable.Map.empty - - if !isStable then revertHeapChanges() - heapBefore = this.heap - - def revertHeapChanges()(using State, Context) = - printer.println("reverting heap changes") - this.heap.keys.foreach { - case warm: Warm => - if heapBefore.contains(warm) then - this.heap = heap.updated(warm, heapBefore(warm)) - else - // We cannot simply remove the object, as the values in the - // updated cache may refer to the warm object. - given Env = Env.empty - given Trace = Trace.empty - given Promoted = Promoted.empty - printer.println("resetting " + warm) - warm.ensureObjectFreshAndPopulated() - case _ => - } - - // ThisRef objects are not reachable, thus it's fine to leave them in - // the heap - end revertHeapChanges + this.changed = false + this.current = mutable.Map.empty + this.heap = this.heapStable def updateObject(ref: Ref, obj: Objekt) = this.heap = this.heap.updated(ref, obj) @@ -463,11 +444,19 @@ object Semantic { extension (ref: Ref) - def objekt(using Cache): Objekt = cache.getObject(ref) + def objekt: Contextual[Objekt] = + // TODO: improve performance + ref match + case warm: Warm => warm.ensureObjectExistsAndPopulated() + case _ => + cache.getObject(ref) def ensureObjectExists()(using Cache): ref.type = - if cache.containsObject(ref) then ref - else ensureFresh() + if cache.containsObject(ref) then + printer.println("object " + ref + " already exists") + ref + else + ensureFresh() def ensureFresh()(using Cache): ref.type = val obj = Objekt(ref.klass, fields = Map.empty, outers = Map(ref.klass -> ref.outer)) @@ -479,9 +468,9 @@ object Semantic { * * Invariant: fields are immutable and only set once */ - def updateField(field: Symbol, value: Value)(using Cache, Context): Unit = log("set field " + field + " of " + ref + " to " + value) { + def updateField(field: Symbol, value: Value): Contextual[Unit] = log("set field " + field + " of " + ref + " to " + value) { val obj = objekt - assert(!obj.hasField(field), field.show + " already init, new = " + value + ", old = " + obj.field(field) + ", ref = " + ref) + assert(!obj.hasField(field) || field.is(Flags.ParamAccessor) && obj.field(field) == value, field.show + " already init, new = " + value + ", old = " + obj.field(field) + ", ref = " + ref) val obj2 = obj.copy(fields = obj.fields.updated(field, value)) cache.updateObject(ref, obj2) } @@ -490,9 +479,9 @@ object Semantic { * * Invariant: outers are immutable and only set once */ - def updateOuter(klass: ClassSymbol, value: Value)(using Cache, Context): Unit = log("set outer " + klass + " of " + ref + " to " + value) { + def updateOuter(klass: ClassSymbol, value: Value): Contextual[Unit] = log("set outer " + klass + " of " + ref + " to " + value) { val obj = objekt - assert(!obj.hasOuter(klass), klass.show + " already has outer, new = " + value + ", old = " + obj.outer(klass) + ", ref = " + ref) + assert(!obj.hasOuter(klass) || obj.outer(klass) == value, klass.show + " already has outer, new = " + value + ", old = " + obj.outer(klass) + ", ref = " + ref) val obj2 = obj.copy(outers = obj.outers.updated(klass, value)) cache.updateObject(ref, obj2) } @@ -679,7 +668,7 @@ object Semantic { Result(Hot, Errors.empty) else val outer = Hot - val warm = Warm(klass, outer, ctor, args2).ensureFresh() + val warm = Warm(klass, outer, ctor, args2).ensureObjectExists() val argInfos2 = args.zip(args2).map { (argInfo, v) => argInfo.copy(value = v) } val res = warm.callConstructor(ctor, argInfos2, source) Result(warm, res.errors) @@ -699,7 +688,7 @@ object Semantic { val argsWidened = args.map(_.value).widenArgs val argInfos2 = args.zip(argsWidened).map { (argInfo, v) => argInfo.copy(value = v) } - val warm = Warm(klass, outer, ctor, argsWidened).ensureFresh() + val warm = Warm(klass, outer, ctor, argsWidened).ensureObjectExists() val res = warm.callConstructor(ctor, argInfos2, source) Result(warm, res.errors) From 8bbe3842b6714924576025f9b7ff3515163503d9 Mon Sep 17 00:00:00 2001 From: Fengyun Liu Date: Mon, 20 Sep 2021 23:33:01 +0200 Subject: [PATCH 43/52] Add comment --- .../tools/dotc/transform/init/Semantic.scala | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala b/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala index ee1756f05746..6325986536e5 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala @@ -312,13 +312,7 @@ object Semantic { * * 2. Reset current cache (last cache already synced in `assume`) * - * 3. Recompute the newly created warm objects with the updated cache. - * - * The computation only covers class parameters and outers. Class - * fields are ignored and are lazily evaluated and cached. - * - * This step should be after the first two steps so that the populated - * parameter are re-computed from the updated input cache. + * 3. Revert heap if instable. * */ def prepareForNextIteration(isStable: Boolean)(using State, Context) = @@ -470,6 +464,12 @@ object Semantic { */ def updateField(field: Symbol, value: Value): Contextual[Unit] = log("set field " + field + " of " + ref + " to " + value) { val obj = objekt + // We may reset the outers or params of a populated warm object. + // This is the case if we need access the field of a warm object, which + // requires population of parameters and outers; and later create an + // instance of the exact warm object, which requires initialization check. + // + // See tests/init/neg/unsound1.scala assert(!obj.hasField(field) || field.is(Flags.ParamAccessor) && obj.field(field) == value, field.show + " already init, new = " + value + ", old = " + obj.field(field) + ", ref = " + ref) val obj2 = obj.copy(fields = obj.fields.updated(field, value)) cache.updateObject(ref, obj2) @@ -481,6 +481,7 @@ object Semantic { */ def updateOuter(klass: ClassSymbol, value: Value): Contextual[Unit] = log("set outer " + klass + " of " + ref + " to " + value) { val obj = objekt + // See the comment in `updateField` for setting the value twice. assert(!obj.hasOuter(klass) || obj.outer(klass) == value, klass.show + " already has outer, new = " + value + ", old = " + obj.outer(klass) + ", ref = " + ref) val obj2 = obj.copy(outers = obj.outers.updated(klass, value)) cache.updateObject(ref, obj2) From dab8181acdcfe11a27aee2a41f95c6bbfb1a4b86 Mon Sep 17 00:00:00 2001 From: Fengyun Liu Date: Tue, 21 Sep 2021 22:00:13 +0200 Subject: [PATCH 44/52] Fix non-termination The following test explodes: tests/init/pos/local-warm5.scala. Ideally we should use an actual set in RefSet, which we don't do for both performance and determinism. The performance concern might be a premature optimization. --- .../tools/dotc/transform/init/Semantic.scala | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala b/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala index 6325986536e5..fcc5e1aeb56b 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala @@ -414,12 +414,20 @@ object Semantic { case (Cold, _) => Cold case (_, Cold) => Cold - case (a: (Fun | Warm | ThisRef), b: (Fun | Warm | ThisRef)) => RefSet(a :: b :: Nil) + case (a: (Fun | Warm | ThisRef), b: (Fun | Warm | ThisRef)) => + if a == b then a else RefSet(a :: b :: Nil) - case (a: (Fun | Warm | ThisRef), RefSet(refs)) => RefSet(a :: refs) - case (RefSet(refs), b: (Fun | Warm | ThisRef)) => RefSet(b :: refs) + case (a: (Fun | Warm | ThisRef), RefSet(refs)) => + if refs.exists(_ == a) then b + else RefSet(a :: refs) - case (RefSet(refs1), RefSet(refs2)) => RefSet(refs1 ++ refs2) + case (RefSet(refs), b: (Fun | Warm | ThisRef)) => + if refs.exists(_ == b) then a + else RefSet(b :: refs) + + case (RefSet(refs1), RefSet(refs2)) => + val diff = refs2.filter(ref => refs1.forall(_ != ref)) + RefSet(refs1 ++ diff) /** Conservatively approximate the value with `Cold` or `Hot` */ def widenArg: Value = From 1ab53166c04d21cd8022fe8976c798d7fc4085b3 Mon Sep 17 00:00:00 2001 From: Fengyun Liu Date: Wed, 22 Sep 2021 08:17:24 +0200 Subject: [PATCH 45/52] Fix pickling test --- compiler/src/dotty/tools/dotc/transform/init/Semantic.scala | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala b/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala index fcc5e1aeb56b..dbb6fac0c0f6 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala @@ -418,11 +418,11 @@ object Semantic { if a == b then a else RefSet(a :: b :: Nil) case (a: (Fun | Warm | ThisRef), RefSet(refs)) => - if refs.exists(_ == a) then b + if refs.exists(_ == a) then b: Value // fix pickling test else RefSet(a :: refs) case (RefSet(refs), b: (Fun | Warm | ThisRef)) => - if refs.exists(_ == b) then a + if refs.exists(_ == b) then a: Value // fix pickling test else RefSet(b :: refs) case (RefSet(refs1), RefSet(refs2)) => From 1e802c9f274f1a961368f532e6d07ff589c49c27 Mon Sep 17 00:00:00 2001 From: Fengyun Liu Date: Wed, 22 Sep 2021 21:15:19 +0200 Subject: [PATCH 46/52] Remove unused parameters of ThisRef --- compiler/src/dotty/tools/dotc/transform/init/Checker.scala | 5 +---- compiler/src/dotty/tools/dotc/transform/init/Semantic.scala | 4 +++- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/transform/init/Checker.scala b/compiler/src/dotty/tools/dotc/transform/init/Checker.scala index 1a191564d924..d74ab46abdc2 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Checker.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Checker.scala @@ -55,10 +55,7 @@ class Checker extends Phase { mdef match case tdef: TypeDef if tdef.isClassDef => val cls = tdef.symbol.asClass - val ctor = cls.primaryConstructor - val args = ctor.defTree.asInstanceOf[DefDef].termParamss.flatten.map(_ => Hot) - val outer = Hot - val thisRef = ThisRef(cls, outer, ctor, args) + val thisRef = ThisRef(cls) given Trace = Trace.empty if shouldCheckClass(cls) then Semantic.addTask(thisRef) case _ => diff --git a/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala b/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala index dbb6fac0c0f6..acb20bbb8d53 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala @@ -73,7 +73,9 @@ object Semantic { } /** A reference to the object under initialization pointed by `this` */ - case class ThisRef(klass: ClassSymbol, outer: Value, ctor: Symbol, args: List[Value]) extends Ref + case class ThisRef(klass: ClassSymbol) extends Ref { + val outer = Hot + } /** An object with all fields initialized but reaches objects under initialization * From 8088217d911a03d7f071239a8a51a165a2289bd2 Mon Sep 17 00:00:00 2001 From: Fengyun Liu Date: Wed, 22 Sep 2021 21:47:46 +0200 Subject: [PATCH 47/52] Fix compilation of scodec Use correct klass for evaluating rhs of local variables. --- .../tools/dotc/transform/init/Semantic.scala | 6 ++-- tests/init/neg/scodec.scala | 30 +++++++++++++++++++ 2 files changed, 33 insertions(+), 3 deletions(-) create mode 100644 tests/init/neg/scodec.scala diff --git a/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala b/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala index acb20bbb8d53..ffad12a44a6e 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala @@ -745,7 +745,7 @@ object Semantic { case Cold => Result(Cold, Nil) - case ref: Ref => eval(vdef.rhs, ref, klass) + case ref: Ref => eval(vdef.rhs, ref, enclosingClass) case _ => report.error("unexpected defTree when accessing local variable, sym = " + sym.show + ", defTree = " + sym.defTree.show, source) @@ -992,7 +992,7 @@ object Semantic { * * This method only handles cache logic and delegates the work to `cases`. */ - def eval(expr: Tree, thisV: Ref, klass: ClassSymbol, cacheResult: Boolean = false): Contextual[Result] = log("evaluating " + expr.show + ", this = " + thisV.show, printer, (_: Result).show) { + def eval(expr: Tree, thisV: Ref, klass: ClassSymbol, cacheResult: Boolean = false): Contextual[Result] = log("evaluating " + expr.show + ", this = " + thisV.show + " in " + klass.show, printer, (_: Result).show) { if (cache.contains(thisV, expr)) Result(cache(thisV, expr), Errors.empty) else cache.assume(thisV, expr, cacheResult) { cases(expr, thisV, klass) } } @@ -1237,7 +1237,7 @@ object Semantic { val obj = ref.objekt val outerCls = klass.owner.lexicallyEnclosingClass.asClass if !obj.hasOuter(klass) then - val error = PromoteError("outer not yet initialized, target = " + target + ", klass = " + klass, source, trace.toVector) + val error = PromoteError("outer not yet initialized, target = " + target + ", klass = " + klass + ", object = " + obj, source, trace.toVector) report.error(error.show + error.stacktrace, source) Hot else diff --git a/tests/init/neg/scodec.scala b/tests/init/neg/scodec.scala new file mode 100644 index 000000000000..19c2983b589d --- /dev/null +++ b/tests/init/neg/scodec.scala @@ -0,0 +1,30 @@ +trait Codec[A] { self => + final def withContext(context: String): Codec[A] = + class X extends Codec[A] { + def decode(bits: String) = 10 + override def toString = s"$self" + } + new X + + def decode(bits: String): Int + + def decodeOnly[AA >: A]: Codec[AA] = { + val sup = this.decodeOnly[AA] + class Y extends Codec[AA] { + def decode(bits: String) = sup.decode(bits) + } + new Y + } + +} + +object codecs { + class Z extends Codec[String] { + override def decode(bits: String): Int = 0 + } + val codec = new Z + + println(codec) // error + + val n = 10 // prevent early promotion +} From 6566756af3565f75c2c9ac30a397cf564964b730 Mon Sep 17 00:00:00 2001 From: Fengyun Liu Date: Thu, 7 Oct 2021 22:23:13 +0200 Subject: [PATCH 48/52] Apply suggestions from code review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Ondřej Lhoták Co-authored-by: Natsu Kagami --- .../tools/dotc/transform/init/Semantic.scala | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala b/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala index ffad12a44a6e..60a74aa0464a 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala @@ -257,11 +257,24 @@ object Semantic { * * The fact that objects of `ThisRef` are stored in heap is just an engineering convenience. * Technically, we can also store the object directly in `ThisRef`. + * + * The heap contains objects of two conceptually distinct kinds. + * - Objects that are also in `heapStable` + * are flow-insensitive views of already initialized objects that are cached for reuse in analysis of later + * classes. These objects and their fields should never change; this is enforced using assertions. + * - Objects that are not (yet) in `heapStable` are the flow-sensitive abstract state of objects being analyzed + * in the current iteration of the analysis of the current class. Their fields do change flow-sensitively: more + * fields are added as fields become initialized. These objects are valid only within the current iteration and + * are removed when moving to a new iteration of analyzing the current class. When the analysis of a class + * reaches a fixed point, these now stable flow-sensitive views of the object at the end of the constructor + * of the analyzed class now become the flow-insensitive views of already initialized objects and can therefore + * be added to `heapStable`. */ private var heap: Heap = Map.empty /** Used to revert heap to last stable heap. */ private var heapStable: Heap = Map.empty + def stableHeapContains(ref: Ref) = heapStable.contains(ref) def hasChanged = changed @@ -272,6 +285,11 @@ object Semantic { if current.contains(value, expr) then current(value)(expr) else stable(value)(expr) + /** Copy the value of `(value, expr)` from the last cache to the current cache + * (assuming it's `Hot` if it doesn't exist in the cache). + * + * Then, runs `fun` and update the caches if the values change. + */ def assume(value: Value, expr: Tree, cacheResult: Boolean)(fun: => Result): Contextual[Result] = val assumeValue: Value = if last.contains(value, expr) then @@ -330,6 +348,7 @@ object Semantic { this.heap = this.heapStable def updateObject(ref: Ref, obj: Objekt) = + assert(!this.heapStable.contains(ref)) this.heap = this.heap.updated(ref, obj) def containsObject(ref: Ref) = heap.contains(ref) From 15a51d718dde00459d5be4e897fd6719fa65bfa5 Mon Sep 17 00:00:00 2001 From: Fengyun Liu Date: Thu, 7 Oct 2021 22:43:58 +0200 Subject: [PATCH 49/52] Address review comments --- .../tools/dotc/transform/init/Checker.scala | 3 +- .../tools/dotc/transform/init/Semantic.scala | 106 +++++++++--------- 2 files changed, 54 insertions(+), 55 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/transform/init/Checker.scala b/compiler/src/dotty/tools/dotc/transform/init/Checker.scala index d74ab46abdc2..2aba63052c1d 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Checker.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Checker.scala @@ -43,7 +43,7 @@ class Checker extends Phase { // ignore, we already called `Semantic.check()` in `runOn` } - class InitTreeTraverser(using State) extends TreeTraverser { + class InitTreeTraverser(using WorkList) extends TreeTraverser { override def traverse(tree: Tree)(using Context): Unit = traverseChildren(tree) tree match { @@ -56,7 +56,6 @@ class Checker extends Phase { case tdef: TypeDef if tdef.isClassDef => val cls = tdef.symbol.asClass val thisRef = ThisRef(cls) - given Trace = Trace.empty if shouldCheckClass(cls) then Semantic.addTask(thisRef) case _ => diff --git a/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala b/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala index 60a74aa0464a..af4fa67f83af 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala @@ -59,6 +59,7 @@ object Semantic { def isHot = this == Hot def isCold = this == Cold def isWarm = this.isInstanceOf[Warm] + def isThisRef = this.isInstanceOf[ThisRef] } /** A transitively initialized object */ @@ -104,9 +105,6 @@ object Semantic { def ensureObjectExistsAndPopulated(): Contextual[this.type] = if cache.containsObject(this) then this else this.ensureFresh().populateParams() - - def ensureObjectFreshAndPopulated(): Contextual[this.type] = - this.ensureFresh().populateParams() } /** A function value */ @@ -328,25 +326,37 @@ object Semantic { /** Prepare cache for the next iteration * - * 1. Reset changed flag + * 1. Reset changed flag. * - * 2. Reset current cache (last cache already synced in `assume`) + * 2. Reset current cache (last cache already synced in `assume`). * * 3. Revert heap if instable. * */ - def prepareForNextIteration(isStable: Boolean)(using State, Context) = - if isStable then - this.commitToStableCache() - this.heapStable = this.heap - // If the current iteration is not stable, we need to use `last` for the next iteration, - // which already contains the updated value from the current iteration. - this.last = mutable.Map.empty - + def prepareForNextIteration()(using Context) = this.changed = false this.current = mutable.Map.empty this.heap = this.heapStable + /** Prepare for checking next class + * + * 1. Reset changed flag. + * + * 2. Commit current cache to stable cache if not changed. + * + * 3. Update stable heap if not changed. + * + * 4. Reset last cache. + */ + def prepareForNextClass()(using Context) = + if this.changed then + this.changed = false + else + this.commitToStableCache() + this.heapStable = this.heap + + this.last = mutable.Map.empty + def updateObject(ref: Ref, obj: Objekt) = assert(!this.heapStable.contains(ref)) this.heap = this.heap.updated(ref, obj) @@ -395,17 +405,10 @@ object Semantic { value.instantiate(klass, ctor, args, source) ++ errors } -// ----- State -------------------------------------------- - /** Global state of the checker */ - class State(val cache: Cache, val workList: WorkList) - - given (using s: State): Cache = s.cache - given (using s: State): WorkList = s.workList - - inline def state(using s: State) = s +// ----- Checker State ----------------------------------- /** The state that threads through the interpreter */ - type Contextual[T] = (Env, Context, Trace, Promoted, State) ?=> T + type Contextual[T] = (Env, Context, Trace, Promoted, Cache) ?=> T // ----- Error Handling ----------------------------------- @@ -917,50 +920,48 @@ object Semantic { cls == defn.ObjectClass // ----- Work list --------------------------------------------------- - case class Task(value: ThisRef)(val trace: Trace) + case class Task(value: ThisRef) class WorkList private[Semantic]() { private var pendingTasks: List[Task] = Nil - private var checkedTasks: Set[Task] = Set.empty def addTask(task: Task): Unit = - if !checkedTasks.contains(task) then pendingTasks = task :: pendingTasks + if !pendingTasks.contains(task) then pendingTasks = task :: pendingTasks /** Process the worklist until done */ - @tailrec - final def work()(using State, Context): Unit = - pendingTasks match - case task :: rest => - checkedTasks = checkedTasks + task - - task.value.ensureFresh() - val res = doTask(task) - res.errors.foreach(_.issue) - - if cache.hasChanged && res.errors.isEmpty then - cache.prepareForNextIteration(isStable = false) - else - cache.prepareForNextIteration(isStable = true) - pendingTasks = rest - - work() - case _ => + final def work()(using Cache, Context): Unit = + for task <- pendingTasks + do doTask(task) /** Check an individual class * * This method should only be called from the work list scheduler. */ - private def doTask(task: Task)(using State, Context): Result = log("checking " + task) { + private def doTask(task: Task)(using Cache, Context): Unit = log("checking " + task) { val thisRef = task.value val tpl = thisRef.klass.defTree.asInstanceOf[TypeDef].rhs.asInstanceOf[Template] val paramValues = tpl.constr.termParamss.flatten.map(param => param.symbol -> Hot).toMap given Promoted = Promoted.empty - given Trace = task.trace + given Trace = Trace.empty given Env = Env(paramValues) - eval(tpl, thisRef, thisRef.klass) + @tailrec + def iterate(): Unit = { + thisRef.ensureFresh() + val res = eval(tpl, thisRef, thisRef.klass) + res.errors.foreach(_.issue) + + if cache.hasChanged && res.errors.isEmpty then + // code to prepare cache and heap for next iteration + cache.prepareForNextIteration() + iterate() + else + cache.prepareForNextClass() + } + + iterate() } } inline def workList(using wl: WorkList): WorkList = wl @@ -968,13 +969,13 @@ object Semantic { // ----- API -------------------------------- /** Add a checking task to the work list */ - def addTask(thisRef: ThisRef)(using WorkList, Trace) = workList.addTask(Task(thisRef)(trace)) + def addTask(thisRef: ThisRef)(using WorkList) = workList.addTask(Task(thisRef)) /** Perform check on the work list until it becomes empty * * Should only be called once from the checker. */ - def check()(using State, Context) = workList.work() + def check()(using Cache, WorkList, Context) = workList.work() /** Perform actions with initial checking state. * @@ -984,9 +985,8 @@ object Semantic { * Semantic.check() * } */ - def withInitialState[T](work: State ?=> T): T = { - val initialState = State(new Cache, new WorkList) - work(using initialState) + def withInitialState[T](work: (Cache, WorkList) ?=> T): T = { + work(using new Cache, new WorkList) } // ----- Semantic definition -------------------------------- @@ -1412,7 +1412,7 @@ object Semantic { var fieldsChanged = true // class body - if !thisV.isWarm || !thisV.asInstanceOf[Warm].isPopulatingParams then tpl.body.foreach { + if thisV.isThisRef || !thisV.asInstanceOf[Warm].isPopulatingParams then tpl.body.foreach { case vdef : ValDef if !vdef.symbol.is(Flags.Lazy) && !vdef.rhs.isEmpty => given Env = Env.empty val res = eval(vdef.rhs, thisV, klass) @@ -1423,7 +1423,7 @@ object Semantic { case _: MemberDef => case tree => - if fieldsChanged && !thisV.isWarm then thisV.asInstanceOf[ThisRef].tryPromoteCurrentObject + if fieldsChanged && thisV.isThisRef then thisV.asInstanceOf[ThisRef].tryPromoteCurrentObject fieldsChanged = false given Env = Env.empty From 7ee58a36f20abe54ff07fd66329ebdb5ea82dffb Mon Sep 17 00:00:00 2001 From: Fengyun Liu Date: Thu, 7 Oct 2021 23:24:09 +0200 Subject: [PATCH 50/52] Address review: Add comment to populateParams --- .../tools/dotc/transform/init/Semantic.scala | 26 ++++++++++++++----- 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala b/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala index af4fa67f83af..2f00e447c93d 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala @@ -92,6 +92,19 @@ object Semantic { /** Ensure that outers and class parameters are initialized. * * Fields in class body are not initialized. + * + * We need to populate class parameters and outers for warm values for the + * following cases: + * + * - Widen an already checked warm value to another warm value without + * corresponding object + * + * - Using a warm value from the cache, whose corresponding object from + * the last iteration have been remove due to heap reversion + * {@see Cache.prepareForNextIteration} + * + * After populating class parameters and outers, it is possible to lazily + * compute the field values in class bodies when they are accessed. */ private def populateParams(): Contextual[this.type] = log("populating parameters", printer, (_: Warm).objekt.toString) { assert(!populatingParams, "the object is already populating parameters") @@ -255,11 +268,13 @@ object Semantic { * * The fact that objects of `ThisRef` are stored in heap is just an engineering convenience. * Technically, we can also store the object directly in `ThisRef`. - * - * The heap contains objects of two conceptually distinct kinds. - * - Objects that are also in `heapStable` - * are flow-insensitive views of already initialized objects that are cached for reuse in analysis of later - * classes. These objects and their fields should never change; this is enforced using assertions. + * + * The heap contains objects of two conceptually distinct kinds. + * + * - Objects that are also in `heapStable` are flow-insensitive views of already initialized objects that are + * cached for reuse in analysis of later classes. These objects and their fields should never change; this is + * enforced using assertions. + * * - Objects that are not (yet) in `heapStable` are the flow-sensitive abstract state of objects being analyzed * in the current iteration of the analysis of the current class. Their fields do change flow-sensitively: more * fields are added as fields become initialized. These objects are valid only within the current iteration and @@ -272,7 +287,6 @@ object Semantic { /** Used to revert heap to last stable heap. */ private var heapStable: Heap = Map.empty - def stableHeapContains(ref: Ref) = heapStable.contains(ref) def hasChanged = changed From c2bfee91c7a7cbb5752a0fc3a002b82cdba494ff Mon Sep 17 00:00:00 2001 From: Fengyun Liu Date: Thu, 7 Oct 2021 23:29:37 +0200 Subject: [PATCH 51/52] Trial: Test performance --- bench/profiles/projects.yml | 4 ++-- bench/scripts/scalap | 2 +- bench/scripts/stdlib213 | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/bench/profiles/projects.yml b/bench/profiles/projects.yml index 2d96fb732910..9ebf429fd05e 100644 --- a/bench/profiles/projects.yml +++ b/bench/profiles/projects.yml @@ -26,10 +26,10 @@ charts: scripts: dotty: - - measure -with-compiler $(find $PROG_HOME/dotty/compiler/src/dotty -name *.scala -o -name *.java) + - measure -with-compiler -Ysafe-init $(find $PROG_HOME/dotty/compiler/src/dotty -name *.scala -o -name *.java) re2s: - - measure $(find $PROG_HOME/tests/re2s/src -name *.scala) + - measure -Ysafe-init $(find $PROG_HOME/tests/re2s/src -name *.scala) # scalapb: # - source $PROG_HOME/dotty/bench/scripts/scalapb diff --git a/bench/scripts/scalap b/bench/scripts/scalap index f9b25632e8ae..697d4cbb9172 100644 --- a/bench/scripts/scalap +++ b/bench/scripts/scalap @@ -2,4 +2,4 @@ set -e path=$(cs fetch -p org.scala-lang:scala-compiler:2.13.0) -measure -source 3.0-migration -classpath "$path" $(find $PROG_HOME/dotty/community-build/community-projects/scalap/src/scalap -name "*.scala") +measure -source 3.0-migration -Ysafe-init -classpath "$path" $(find $PROG_HOME/dotty/community-build/community-projects/scalap/src/scalap -name "*.scala") diff --git a/bench/scripts/stdlib213 b/bench/scripts/stdlib213 index b70010b15644..f35ab2384e1a 100644 --- a/bench/scripts/stdlib213 +++ b/bench/scripts/stdlib213 @@ -3,5 +3,5 @@ set -e pattern="! -name AnyVal.scala ! -name language.scala -name *.scala -o -name *.java" stdlib213=$(find $PROG_HOME/dotty/community-build/community-projects/stdLib213/src/library/scala $pattern) cd $PROG_HOME/dotty -measure -language:implicitConversions $stdlib213 +measure -language:implicitConversions -Ysafe-init $stdlib213 From e905cd545fd4ac8c8d7c89e2f1bb776c1bc63a1b Mon Sep 17 00:00:00 2001 From: Fengyun Liu Date: Thu, 7 Oct 2021 23:39:07 +0200 Subject: [PATCH 52/52] Fix compilation --- .../tools/dotc/transform/init/Semantic.scala | 26 ++++++++++--------- 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala b/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala index 2f00e447c93d..4c88fd80bc3a 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala @@ -363,13 +363,15 @@ object Semantic { * 4. Reset last cache. */ def prepareForNextClass()(using Context) = - if this.changed then - this.changed = false - else - this.commitToStableCache() - this.heapStable = this.heap + if this.changed then + this.changed = false + this.heap = this.heapStable + else + this.commitToStableCache() + this.heapStable = this.heap - this.last = mutable.Map.empty + this.last = mutable.Map.empty + this.current = mutable.Map.empty def updateObject(ref: Ref, obj: Objekt) = assert(!this.heapStable.contains(ref)) @@ -951,20 +953,20 @@ object Semantic { * * This method should only be called from the work list scheduler. */ - private def doTask(task: Task)(using Cache, Context): Unit = log("checking " + task) { + private def doTask(task: Task)(using Cache, Context): Unit = { val thisRef = task.value val tpl = thisRef.klass.defTree.asInstanceOf[TypeDef].rhs.asInstanceOf[Template] val paramValues = tpl.constr.termParamss.flatten.map(param => param.symbol -> Hot).toMap - given Promoted = Promoted.empty - given Trace = Trace.empty - given Env = Env(paramValues) - @tailrec def iterate(): Unit = { + given Promoted = Promoted.empty + given Trace = Trace.empty + given Env = Env(paramValues) + thisRef.ensureFresh() - val res = eval(tpl, thisRef, thisRef.klass) + val res = log("checking " + task) { eval(tpl, thisRef, thisRef.klass) } res.errors.foreach(_.issue) if cache.hasChanged && res.errors.isEmpty then