From 12def6bd456f9dad2dd3fb416aa1befd24c66df2 Mon Sep 17 00:00:00 2001 From: Liu Fengyun Date: Fri, 27 Nov 2020 15:04:23 +0100 Subject: [PATCH 01/34] Remove unused file --- .../dotc/transform/init/SetDefTree.scala | 33 ------------------- 1 file changed, 33 deletions(-) delete mode 100644 compiler/src/dotty/tools/dotc/transform/init/SetDefTree.scala diff --git a/compiler/src/dotty/tools/dotc/transform/init/SetDefTree.scala b/compiler/src/dotty/tools/dotc/transform/init/SetDefTree.scala deleted file mode 100644 index 8b8ceacc053f..000000000000 --- a/compiler/src/dotty/tools/dotc/transform/init/SetDefTree.scala +++ /dev/null @@ -1,33 +0,0 @@ -package dotty.tools.dotc -package transform -package init - -import MegaPhase._ -import ast.tpd -import core.Contexts._ - -/** Set the `defTree` property of symbols */ -class SetDefTree extends MiniPhase { - import tpd._ - - override val phaseName: String = SetDefTree.name - 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] = { - val ctx2 = ctx.fresh.setSetting(ctx.settings.YretainTrees, true) - super.runOn(units)(using ctx2) - } - - override def transformValDef(tree: ValDef)(using Context): Tree = tree.setDefTree - - override def transformDefDef(tree: DefDef)(using Context): Tree = tree.setDefTree - - override def transformTypeDef(tree: TypeDef)(using Context): Tree = tree.setDefTree -} - -object SetDefTree { - val name: String = "SetDefTree" -} From b7ccd173e414d0ec7290cdf69983ffd86218196c Mon Sep 17 00:00:00 2001 From: Liu Fengyun Date: Fri, 27 Nov 2020 15:30:01 +0100 Subject: [PATCH 02/34] Add definitions to support check global objects --- .../tools/dotc/transform/init/Effects.scala | 7 +++++++ .../tools/dotc/transform/init/Potentials.scala | 16 +++++++++++++++- 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/compiler/src/dotty/tools/dotc/transform/init/Effects.scala b/compiler/src/dotty/tools/dotc/transform/init/Effects.scala index b08bad00592d..49e6bf870dc7 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Effects.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Effects.scala @@ -56,6 +56,11 @@ object Effects { def show(using Context): String = potential.show + "." + method.name.show + "!" } + /** Accessing a global object */ + case class AccessGlobal(potential: Global)(val source: Tree) extends Effect { + def show(using Context): String = potential.tmref.name.show + "!" + } + // ------------------ operations on effects ------------------ extension (eff: Effect) def toEffs: Effects = Effects.empty + eff @@ -73,6 +78,8 @@ object Effects { case MethodCall(pot, sym) => val pot1 = Potentials.asSeenFrom(pot, thisValue) MethodCall(pot1, sym)(eff.source) + + case _: AccessGlobal => eff } } def asSeenFrom(effs: Effects, thisValue: Potential)(implicit env: Env): Effects = diff --git a/compiler/src/dotty/tools/dotc/transform/init/Potentials.scala b/compiler/src/dotty/tools/dotc/transform/init/Potentials.scala index 282e5344f7a3..56834b0a6e37 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Potentials.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Potentials.scala @@ -144,6 +144,20 @@ object Potentials { def show(using Context): String = "Cold" } + /** Reference to a global object */ + case class Global(tmref: TermRef)(val source: Tree) extends Potential { + def show(using Context): String = tmref.show + } + + /** The potential of a locally hot object + * + * A locally hot object may potentially reference a global object which is + * potentially under initialization + */ + case class LocalHot(classSymbol: ClassSymbol)(val source: Tree) extends Potential { + def show(using Context): String = "LocalHot[" + classSymbol.name.show + "]" + } + /** A function when called will produce the `effects` and return the `potentials` */ case class Fun(potentials: Potentials, effects: Effects)(val source: Tree) extends Potential { override def size: Int = 1 @@ -216,7 +230,7 @@ object Potentials { val outer3 = asSeenFrom(outer2, thisValue2) Warm(cls, outer3)(pot.source) - case _: Cold => + case _: Cold | _: LocalHot | _: Global => pot case SuperRef(potThis, supercls) => From 62665c997151c4b38dbc491012ea77dd9227b28b Mon Sep 17 00:00:00 2001 From: Liu Fengyun Date: Fri, 27 Nov 2020 16:33:36 +0100 Subject: [PATCH 03/34] Analyzing global effects and potentials --- .../tools/dotc/transform/init/Effects.scala | 4 +++- .../dotty/tools/dotc/transform/init/Env.scala | 2 ++ .../dotc/transform/init/Summarization.scala | 18 ++++++++++++++++-- 3 files changed, 21 insertions(+), 3 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/transform/init/Effects.scala b/compiler/src/dotty/tools/dotc/transform/init/Effects.scala index 49e6bf870dc7..5173fe489a95 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Effects.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Effects.scala @@ -57,7 +57,9 @@ object Effects { } /** Accessing a global object */ - case class AccessGlobal(potential: Global)(val source: Tree) extends Effect { + case class AccessGlobal(potential: Global) extends Effect { + val source = potential.source + def show(using Context): String = potential.tmref.name.show + "!" } diff --git a/compiler/src/dotty/tools/dotc/transform/init/Env.scala b/compiler/src/dotty/tools/dotc/transform/init/Env.scala index cd36dc1d867d..11d572b782af 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Env.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Env.scala @@ -36,6 +36,8 @@ case class Env(ctx: Context) { def withOwner(owner: Symbol) = this.copy(ctx = this.ctx.withOwner(owner)) + def checkGlobal: Boolean = true + /** Whether values of a given type is always fully initialized? * * It's true for primitive values diff --git a/compiler/src/dotty/tools/dotc/transform/init/Summarization.scala b/compiler/src/dotty/tools/dotc/transform/init/Summarization.scala index 82293c63452e..e67b4b92b045 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Summarization.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Summarization.scala @@ -98,7 +98,10 @@ object Summarization { } else { val (pots, effs) = analyze(tref.prefix, expr) - if (pots.isEmpty) Summary.empty.withEffs(effs) + if pots.isEmpty then + val summary = Summary.empty.withEffs(effs) + if env.checkGlobal then summary + LocalHot(cls)(expr) + else summary else { assert(pots.size == 1) (Warm(cls, pots.head)(expr).toPots, effs) @@ -218,6 +221,12 @@ object Summarization { case tmref: TermRef if tmref.prefix == NoPrefix => Summary.empty + case tmref: TermRef + if env.checkGlobal && tmref.symbol.is(Flags.Module) && tmref.symbol.isStatic => + val cls = tmref.symbol.moduleClass + val pot = Global(tmref)(source) + Summary.empty + pot + AccessGlobal(pot) + MethodCall(pot, cls.primaryConstructor)(source) + case tmref: TermRef => val (pots, effs) = analyze(tmref.prefix, source) if (env.ignoredMethods.contains(tmref.symbol)) (Potentials.empty, effs) @@ -266,7 +275,12 @@ object Summarization { else { val enclosing = cur.owner.lexicallyEnclosingClass.asClass // Dotty uses O$.this outside of the object O - if (enclosing.is(Flags.Package) && cls.is(Flags.Module)) return Summary.empty + if (enclosing.is(Flags.Package) && cls.is(Flags.Module)) + if env.checkGlobal then + val pot = Global(cls.sourceModule.termRef)(source) + Summary.empty + pot + AccessGlobal(pot) + MethodCall(pot, cls.primaryConstructor)(source) + else + return Summary.empty assert(!enclosing.is(Flags.Package), "enclosing = " + enclosing.show + ", cls = " + cls.show + ", pot = " + pot.show + ", cur = " + cur.show) val pot2 = Outer(pot, cur)(pot.source) From 9ede51a728c58e9db5c5e640c805d63d7c4a1fe6 Mon Sep 17 00:00:00 2001 From: Liu Fengyun Date: Fri, 27 Nov 2020 16:59:16 +0100 Subject: [PATCH 04/34] WIP - check global effects --- .../tools/dotc/transform/init/Checking.scala | 37 ++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-) diff --git a/compiler/src/dotty/tools/dotc/transform/init/Checking.scala b/compiler/src/dotty/tools/dotc/transform/init/Checking.scala index 90741d86124f..52abb71c15f7 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Checking.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Checking.scala @@ -35,9 +35,12 @@ object Checking { thisClass: ClassSymbol, // the concrete class of `this` fieldsInited: mutable.Set[Symbol], parentsInited: mutable.Set[ClassSymbol], - env: Env + env: Env, + superCallFinished: Boolean = false // whether super call has finished, used in singleton objects ) { + def isGlobalObject: Boolean = thisClass.is(Flags.Module) && thisClass.isStatic + def withVisited(eff: Effect): State = { visited = visited + eff copy(path = this.path :+ eff.source) @@ -147,6 +150,9 @@ object Checking { checkConstructor(cls.primaryConstructor, ref.tpe, ref) } + if cls == state.thisClass then + state.superCallFinished = true + // check class body tpl.body.foreach { checkClassBodyStat(_) } } @@ -185,6 +191,14 @@ object Checking { else Errors.empty + case Global(tmref) => + if !state.isGlobalObject then Errors.empty + else ??? // check all public members + + case LocalHot(cls) => + if !state.isGlobalObject then Errors.empty + else ??? // check all public members + case pot => val (pots, effs) = expand(pot) val effs2 = pots.map(Promote(_)(eff.source)) @@ -212,6 +226,18 @@ object Checking { if (target.is(Flags.Lazy)) check(MethodCall(pot, target)(eff.source)) else Errors.empty + case Global(tmref) => + if !state.isGlobalObject || tmref.symbol != state.thisClass then + Errors.empty + else + val target = resolve(state.thisClass, field) + if (target.is(Flags.Lazy)) check(MethodCall(pot, target)(eff.source)) + else if (!state.fieldsInited.contains(target)) AccessNonInit(target, state2.path).toErrors + else Errors.empty + + case _: LocalHot => + Errors.empty + case _: Cold => AccessCold(field, eff.source, state2.path).toErrors @@ -271,6 +297,15 @@ object Checking { val effs2 = pots.map(MethodCall(_, sym)(eff.source)) (effs2 ++ effs).flatMap(check(_)) } + + case AccessGlobal(pot) => + if !state.isGlobalObject || tmref.symbol != state.thisClass then + Errors.empty + else if state.superCallFinished then + Errors.empty + else + AccessNonInit(pot.tmref.symbol, state2.path).toErrors + } } From 70cdf2db03cdfab80a72bb34cd5399e315442dbe Mon Sep 17 00:00:00 2001 From: Liu Fengyun Date: Fri, 27 Nov 2020 17:59:03 +0100 Subject: [PATCH 05/34] WIP - check global effects except promotion --- .../tools/dotc/transform/init/Checking.scala | 36 +++++++++++-- .../dotc/transform/init/Potentials.scala | 52 ++++++++----------- 2 files changed, 54 insertions(+), 34 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/transform/init/Checking.scala b/compiler/src/dotty/tools/dotc/transform/init/Checking.scala index 52abb71c15f7..d1c34544263d 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Checking.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Checking.scala @@ -227,7 +227,7 @@ object Checking { else Errors.empty case Global(tmref) => - if !state.isGlobalObject || tmref.symbol != state.thisClass then + if !state.isGlobalObject || tmref.symbol.moduleClass != state.thisClass then Errors.empty else val target = resolve(state.thisClass, field) @@ -280,8 +280,36 @@ object Checking { val effs = warm.effectsOf(target) effs.flatMap { check(_) } } - else if (!sym.isConstructor) CallUnknown(target, eff.source, state2.path).toErrors - else Errors.empty + else if (!sym.isConstructor) + CallUnknown(target, eff.source, state2.path).toErrors + else + Errors.empty + + case pot @ Global(tmref) => + if !state.isGlobalObject then + Errors.empty + else + val target = resolve(tmref.symbol.modueClass, sym) + if (!target.is(Flags.Method)) + check(FieldAccess(pot, target)(eff.source)) + else if (target.isInternal) { + val effs = pot.effectsOf(target) + effs.flatMap { check(_) } + } + else CallUnknown(target, eff.source, state2.path).toErrors + + case _: LocalHot => + if !state.isGlobalObject then + Errors.empty + else + val target = resolve(tmref.symbol.modueClass, sym) + if (!target.is(Flags.Method)) + check(FieldAccess(pot, target)(eff.source)) + else if (target.isInternal) { + val effs = pot.effectsOf(target) + effs.flatMap { check(_) } + } + else CallUnknown(target, eff.source, state2.path).toErrors case _: Cold => CallCold(sym, eff.source, state2.path).toErrors @@ -299,7 +327,7 @@ object Checking { } case AccessGlobal(pot) => - if !state.isGlobalObject || tmref.symbol != state.thisClass then + if !state.isGlobalObject || tmref.symbol.modueClass != state.thisClass then Errors.empty else if state.superCallFinished then Errors.empty diff --git a/compiler/src/dotty/tools/dotc/transform/init/Potentials.scala b/compiler/src/dotty/tools/dotc/transform/init/Potentials.scala index 56834b0a6e37..e54a92e81462 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Potentials.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Potentials.scala @@ -32,25 +32,37 @@ object Potentials { def source: Tree } - /** The object pointed by `this` */ - case class ThisRef()(val source: Tree) extends Potential { - def show(using Context): String = "this" - + sealed trait Refinable extends Potential { /** Effects of a method call or a lazy val access + * + * The method performs prefix substitution */ - def effectsOf(sym: Symbol)(implicit env: Env): Effects = trace("effects of " + sym.show, init, r => Effects.show(r.asInstanceOf)) { + def effectsOf(sym: Symbol)(implicit env: Env): Effects = trace("effects of " + sym.show, init, r => Effects.show(r.asInstanceOf)) { val cls = sym.owner.asClass - env.summaryOf(cls).effectsOf(sym) + val effs = env.summaryOf(cls).effectsOf(sym) + this match + case _: ThisRef => effs + case _ => Effects.asSeenFrom(effs, this) } /** Potentials of a field, a method call or a lazy val access + * + * The method performs prefix substitution */ def potentialsOf(sym: Symbol)(implicit env: Env): Potentials = trace("potentials of " + sym.show, init, r => Potentials.show(r.asInstanceOf)) { val cls = sym.owner.asClass - env.summaryOf(cls).potentialsOf(sym) + val pots = env.summaryOf(cls).potentialsOf(sym) + this match + case _: ThisRef => pots + case _ => Potentials.asSeenFrom(pots, this) } } + /** The object pointed by `this` */ + case class ThisRef()(val source: Tree) extends Refinable { + def show(using Context): String = "this" + } + /** The object pointed by `C.super.this`, mainly used for override resolution */ case class SuperRef(pot: Potential, supercls: ClassSymbol)(val source: Tree) extends Potential { override def size: Int = pot.size @@ -64,30 +76,10 @@ object Potentials { * @param classSymbol The concrete class of the object * @param outer The potential for `this` of the enclosing class */ - case class Warm(classSymbol: ClassSymbol, outer: Potential)(val source: Tree) extends Potential { + case class Warm(classSymbol: ClassSymbol, outer: Potential)(val source: Tree) extends Refinable { override def level: Int = 1 + outer.level def show(using Context): String = "Warm[" + classSymbol.show + ", outer = " + outer.show + "]" - /** Effects of a method call or a lazy val access - * - * The method performs prefix substitution - */ - def effectsOf(sym: Symbol)(implicit env: Env): Effects = trace("effects of " + sym.show, init, r => Effects.show(r.asInstanceOf)) { - val cls = sym.owner.asClass - val effs = env.summaryOf(cls).effectsOf(sym) - Effects.asSeenFrom(effs, this) - } - - /** Potentials of a field, a method call or a lazy val access - * - * The method performs prefix substitution - */ - def potentialsOf(sym: Symbol)(implicit env: Env): Potentials = trace("potentials of " + sym.show, init, r => Potentials.show(r.asInstanceOf)) { - val cls = sym.owner.asClass - val pots = env.summaryOf(cls).potentialsOf(sym) - Potentials.asSeenFrom(pots, this) - } - def resolveOuter(cls: ClassSymbol)(implicit env: Env): Potentials = env.resolveOuter(this, cls) } @@ -145,7 +137,7 @@ object Potentials { } /** Reference to a global object */ - case class Global(tmref: TermRef)(val source: Tree) extends Potential { + case class Global(tmref: TermRef)(val source: Tree) extends Refinable { def show(using Context): String = tmref.show } @@ -154,7 +146,7 @@ object Potentials { * A locally hot object may potentially reference a global object which is * potentially under initialization */ - case class LocalHot(classSymbol: ClassSymbol)(val source: Tree) extends Potential { + case class LocalHot(classSymbol: ClassSymbol)(val source: Tree) extends Refinable { def show(using Context): String = "LocalHot[" + classSymbol.name.show + "]" } From 574f163490804c3d160f198d7437c2f5f48e38a9 Mon Sep 17 00:00:00 2001 From: Liu Fengyun Date: Fri, 27 Nov 2020 21:41:05 +0100 Subject: [PATCH 06/34] Propagate global potentials --- .../tools/dotc/transform/init/Checking.scala | 43 ++++++++++++++----- .../dotty/tools/dotc/transform/init/Env.scala | 2 +- .../dotc/transform/init/Potentials.scala | 5 ++- 3 files changed, 37 insertions(+), 13 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/transform/init/Checking.scala b/compiler/src/dotty/tools/dotc/transform/init/Checking.scala index d1c34544263d..f35e2b97319d 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Checking.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Checking.scala @@ -36,10 +36,10 @@ object Checking { fieldsInited: mutable.Set[Symbol], parentsInited: mutable.Set[ClassSymbol], env: Env, - superCallFinished: Boolean = false // whether super call has finished, used in singleton objects + var superCallFinished: Boolean = false // whether super call has finished, used in singleton objects ) { - def isGlobalObject: Boolean = thisClass.is(Flags.Module) && thisClass.isStatic + def isGlobalObject(using Context): Boolean = thisClass.is(Flags.Module) && thisClass.isStatic def withVisited(eff: Effect): State = { visited = visited + eff @@ -59,7 +59,8 @@ object Checking { } } - private implicit def theEnv(implicit state: State): Env = state.env + given theEnv(using State) as Env = summon[State].env + given theCtx(using State) as Context = summon[State].env.ctx /** Check that the given concrete class may be initialized safely * @@ -226,8 +227,8 @@ object Checking { if (target.is(Flags.Lazy)) check(MethodCall(pot, target)(eff.source)) else Errors.empty - case Global(tmref) => - if !state.isGlobalObject || tmref.symbol.moduleClass != state.thisClass then + case obj @ Global(tmref) => + if !state.isGlobalObject || obj.moduleClass != state.thisClass then Errors.empty else val target = resolve(state.thisClass, field) @@ -289,7 +290,7 @@ object Checking { if !state.isGlobalObject then Errors.empty else - val target = resolve(tmref.symbol.modueClass, sym) + val target = resolve(pot.moduleClass, sym) if (!target.is(Flags.Method)) check(FieldAccess(pot, target)(eff.source)) else if (target.isInternal) { @@ -298,15 +299,15 @@ object Checking { } else CallUnknown(target, eff.source, state2.path).toErrors - case _: LocalHot => + case lhot: LocalHot => if !state.isGlobalObject then Errors.empty else - val target = resolve(tmref.symbol.modueClass, sym) + val target = resolve(lhot.classSymbol, sym) if (!target.is(Flags.Method)) check(FieldAccess(pot, target)(eff.source)) else if (target.isInternal) { - val effs = pot.effectsOf(target) + val effs = lhot.effectsOf(target) effs.flatMap { check(_) } } else CallUnknown(target, eff.source, state2.path).toErrors @@ -327,7 +328,7 @@ object Checking { } case AccessGlobal(pot) => - if !state.isGlobalObject || tmref.symbol.modueClass != state.thisClass then + if !state.isGlobalObject || pot.moduleClass != state.thisClass then Errors.empty else if state.superCallFinished then Errors.empty @@ -368,6 +369,16 @@ object Checking { if (target.isInternal) (warm.potentialsOf(target), Effects.empty) else Summary.empty // warning already issued in call effect + case lhot: LocalHot => + val target = resolve(lhot.classSymbol, sym) + if (target.isInternal) (lhot.potentialsOf(target), Effects.empty) + else Summary.empty + + case obj @ Global(tmref) => + val target = resolve(obj.moduleClass, sym) + if (target.isInternal) (obj.potentialsOf(target), Effects.empty) + else Summary.empty + case _: Cold => Summary.empty // error already reported, ignore @@ -397,6 +408,16 @@ object Checking { if (target.isInternal) (warm.potentialsOf(target), Effects.empty) else (Cold()(pot.source).toPots, Effects.empty) + case lhot: LocalHot => + val target = resolve(lhot.classSymbol, sym) + if (target.isInternal) (lhot.potentialsOf(target), Effects.empty) + else (Cold()(pot.source).toPots, Effects.empty) + + case obj @ Global(tmref) => + val target = resolve(obj.moduleClass, sym) + if (target.isInternal) (obj.potentialsOf(target), Effects.empty) + else (Cold()(pot.source).toPots, Effects.empty) + case _: Cold => Summary.empty // error already reported, ignore @@ -424,7 +445,7 @@ object Checking { (pots2, effs) } - case _: ThisRef | _: Fun | _: Warm | _: Cold => + case _: ThisRef | _: Fun | _: Warm | _: Cold | _: Global | _: LocalHot => (Set(pot), Effects.empty) case SuperRef(pot1, supercls) => diff --git a/compiler/src/dotty/tools/dotc/transform/init/Env.scala b/compiler/src/dotty/tools/dotc/transform/init/Env.scala index 11d572b782af..94d458582778 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Env.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Env.scala @@ -18,7 +18,7 @@ import scala.collection.mutable import Effects._, Potentials._, Summary._ -implicit def theCtx(implicit env: Env): Context = env.ctx +given theCtx(using Env) as Context = summon[Env].ctx case class Env(ctx: Context) { private implicit def self: Env = this diff --git a/compiler/src/dotty/tools/dotc/transform/init/Potentials.scala b/compiler/src/dotty/tools/dotc/transform/init/Potentials.scala index e54a92e81462..e2f60e42f985 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Potentials.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Potentials.scala @@ -37,7 +37,7 @@ object Potentials { * * The method performs prefix substitution */ - def effectsOf(sym: Symbol)(implicit env: Env): Effects = trace("effects of " + sym.show, init, r => Effects.show(r.asInstanceOf)) { + def effectsOf(sym: Symbol)(implicit env: Env): Effects = trace("effects of " + sym.show, init, r => Effects.show(r.asInstanceOf)) { val cls = sym.owner.asClass val effs = env.summaryOf(cls).effectsOf(sym) this match @@ -139,6 +139,9 @@ object Potentials { /** Reference to a global object */ case class Global(tmref: TermRef)(val source: Tree) extends Refinable { def show(using Context): String = tmref.show + + def moduleClass(using Context): ClassSymbol = + tmref.symbol.moduleClass.asClass } /** The potential of a locally hot object From 3a4c57d3c677c8b085f70333f1d7e5bb16527c5f Mon Sep 17 00:00:00 2001 From: Liu Fengyun Date: Fri, 27 Nov 2020 21:44:58 +0100 Subject: [PATCH 07/34] Refactor: remove useless parameter --- .../src/dotty/tools/dotc/transform/init/Checking.scala | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/transform/init/Checking.scala b/compiler/src/dotty/tools/dotc/transform/init/Checking.scala index f35e2b97319d..b074130a0413 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Checking.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Checking.scala @@ -83,14 +83,14 @@ object Checking { val (pots, effs) = Summarization.analyze(vdef.rhs) theEnv.summaryOf(cls).cacheFor(vdef.symbol, (pots, effs)) if (!vdef.symbol.is(Flags.Lazy)) { - checkEffectsIn(effs, cls) + checkEffects(effs) traceIndented(vdef.symbol.show + " initialized", init) state.fieldsInited += vdef.symbol } case tree => val (_, effs) = Summarization.analyze(tree) - checkEffectsIn(effs, cls) + checkEffects(effs) } } @@ -121,7 +121,7 @@ object Checking { (stats :+ expr).foreach { stat => val (_, effs) = Summarization.analyze(stat)(theEnv.withOwner(ctor)) - checkEffectsIn(effs, cls) + checkEffects(effs) } } @@ -158,7 +158,7 @@ object Checking { tpl.body.foreach { checkClassBodyStat(_) } } - private def checkEffectsIn(effs: Effects, cls: ClassSymbol)(implicit state: State): Unit = traceOp("checking effects " + Effects.show(effs), init) { + private def checkEffects(effs: Effects)(implicit state: State): Unit = traceOp("checking effects " + Effects.show(effs), init) { for { eff <- effs error <- check(eff) From 4d747873b0b44db1e8d273fb08235d7a9b23057f Mon Sep 17 00:00:00 2001 From: Liu Fengyun Date: Fri, 27 Nov 2020 22:06:35 +0100 Subject: [PATCH 08/34] Better summary for new expressions with static path --- .../tools/dotc/transform/init/Potentials.scala | 7 +++++++ .../tools/dotc/transform/init/Summarization.scala | 13 ++++++++++--- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/transform/init/Potentials.scala b/compiler/src/dotty/tools/dotc/transform/init/Potentials.scala index e2f60e42f985..74f6e954408c 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Potentials.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Potentials.scala @@ -20,6 +20,13 @@ object Potentials { def show(pots: Potentials)(using Context): String = pots.map(_.show).mkString(", ") + /** Does the given potential represent a static path */ + def isGlobalPath(pot: Potential): Boolean = + pot match + case _: Global => true + case FieldReturn(pot, _) => isGlobalPath(pot) + case _ => false + /** A potential represents an aliasing of a value that is possibly under initialization */ sealed trait Potential { /** Length of the potential. Used for widening */ diff --git a/compiler/src/dotty/tools/dotc/transform/init/Summarization.scala b/compiler/src/dotty/tools/dotc/transform/init/Summarization.scala index e67b4b92b045..b080256c8a95 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Summarization.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Summarization.scala @@ -90,7 +90,10 @@ object Summarization { val thisRef = ThisRef()(expr) val enclosing = cls.owner.lexicallyEnclosingClass.asClass val (pots, effs) = resolveThis(enclosing, thisRef, cur, expr) - if pots.isEmpty then (Potentials.empty, effs) + val summary = Summary.empty.withEffs(effs) + if pots.isEmpty then + if env.checkGlobal then summary + LocalHot(cls)(expr) + else summary else { assert(pots.size == 1) (Warm(cls, pots.head)(expr).toPots, effs) @@ -98,13 +101,17 @@ object Summarization { } else { val (pots, effs) = analyze(tref.prefix, expr) + val summary = Summary.empty.withEffs(effs) if pots.isEmpty then - val summary = Summary.empty.withEffs(effs) if env.checkGlobal then summary + LocalHot(cls)(expr) else summary else { assert(pots.size == 1) - (Warm(cls, pots.head)(expr).toPots, effs) + val pot = pots.head + if env.checkGlobal && Potentials.isGlobalPath(pot) then + summary + LocalHot(cls)(expr) + else + summary + Warm(cls, pot)(expr) } } From 2bed5fb2984329463018f944681122184542e35a Mon Sep 17 00:00:00 2001 From: Liu Fengyun Date: Fri, 27 Nov 2020 23:08:11 +0100 Subject: [PATCH 09/34] Make summary a proper class --- .../tools/dotc/transform/init/Checking.scala | 86 +++++----- .../dotc/transform/init/Potentials.scala | 10 +- .../dotc/transform/init/Summarization.scala | 148 +++++++++--------- .../tools/dotc/transform/init/Summary.scala | 120 ++++++++------ 4 files changed, 193 insertions(+), 171 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/transform/init/Checking.scala b/compiler/src/dotty/tools/dotc/transform/init/Checking.scala index b074130a0413..15795f363a08 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Checking.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Checking.scala @@ -80,17 +80,17 @@ object Checking { def checkClassBodyStat(tree: Tree)(implicit state: State): Unit = traceOp("checking " + tree.show, init) { tree match { case vdef : ValDef => - val (pots, effs) = Summarization.analyze(vdef.rhs) - theEnv.summaryOf(cls).cacheFor(vdef.symbol, (pots, effs)) + val summary = Summarization.analyze(vdef.rhs) + theEnv.summaryOf(cls).cacheFor(vdef.symbol, summary) if (!vdef.symbol.is(Flags.Lazy)) { - checkEffects(effs) + checkEffects(summary.effs) traceIndented(vdef.symbol.show + " initialized", init) state.fieldsInited += vdef.symbol } case tree => - val (_, effs) = Summarization.analyze(tree) - checkEffects(effs) + val summary = Summarization.analyze(tree) + checkEffects(summary.effs) } } @@ -120,8 +120,8 @@ object Checking { } (stats :+ expr).foreach { stat => - val (_, effs) = Summarization.analyze(stat)(theEnv.withOwner(ctor)) - checkEffects(effs) + val summary = Summarization.analyze(stat)(theEnv.withOwner(ctor)) + checkEffects(summary.effs) } } @@ -201,7 +201,7 @@ object Checking { else ??? // check all public members case pot => - val (pots, effs) = expand(pot) + val Summary(pots, effs) = expand(pot) val effs2 = pots.map(Promote(_)(eff.source)) (effs2 ++ effs).flatMap(check(_)) } @@ -246,7 +246,7 @@ object Checking { throw new Exception("Unexpected effect " + eff.show) case pot => - val (pots, effs) = expand(pot) + val Summary(pots, effs) = expand(pot) val effs2 = pots.map(FieldAccess(_, field)(eff.source)) (effs2 ++ effs).flatMap(check(_)) @@ -322,7 +322,7 @@ object Checking { // curried, tupled, toString are harmless case pot => - val (pots, effs) = expand(pot) + val Summary(pots, effs) = expand(pot) val effs2 = pots.map(MethodCall(_, sym)(eff.source)) (effs2 ++ effs).flatMap(check(_)) } @@ -338,93 +338,95 @@ object Checking { } } - private def expand(pot: Potential)(implicit state: State): Summary = trace("expand " + pot.show, init, sum => Summary.show(sum.asInstanceOf[Summary])) { + private def expand(pot: Potential)(implicit state: State): Summary = trace("expand " + pot.show, init, _.asInstanceOf[Summary].show) { pot match { case MethodReturn(pot1, sym) => pot1 match { case thisRef: ThisRef => val target = resolve(state.thisClass, sym) - if (target.isInternal) (thisRef.potentialsOf(target), Effects.empty) + if (target.isInternal) Summary(thisRef.potentialsOf(target), Effects.empty) else Summary.empty // warning already issued in call effect case SuperRef(thisRef: ThisRef, supercls) => val target = resolveSuper(state.thisClass, supercls, sym) - if (target.isInternal) (thisRef.potentialsOf(target), Effects.empty) + if (target.isInternal) Summary(thisRef.potentialsOf(target), Effects.empty) else Summary.empty // warning already issued in call effect case Fun(pots, effs) => val name = sym.name.toString - if (name == "apply") (pots, Effects.empty) - else if (name == "tupled") (Set(pot1), Effects.empty) + if (name == "apply") Summary(pots, Effects.empty) + else if (name == "tupled") Summary(Set(pot1), Effects.empty) else if (name == "curried") { val arity = defn.functionArity(sym.info.finalResultType) - val pots = (1 until arity).foldLeft(Set(pot1)) { (acc, i) => Set(Fun(acc, Effects.empty)(pot1.source)) } - (pots, Effects.empty) + val pots = (1 until arity).foldLeft(Set(pot1)) { (acc, i) => + Set(Fun(acc, Effects.empty)(pot1.source)) + } + Summary(pots, Effects.empty) } else Summary.empty case warm : Warm => val target = resolve(warm.classSymbol, sym) - if (target.isInternal) (warm.potentialsOf(target), Effects.empty) + if (target.isInternal) Summary(warm.potentialsOf(target), Effects.empty) else Summary.empty // warning already issued in call effect case lhot: LocalHot => val target = resolve(lhot.classSymbol, sym) - if (target.isInternal) (lhot.potentialsOf(target), Effects.empty) + if (target.isInternal) Summary(lhot.potentialsOf(target), Effects.empty) else Summary.empty case obj @ Global(tmref) => val target = resolve(obj.moduleClass, sym) - if (target.isInternal) (obj.potentialsOf(target), Effects.empty) + if (target.isInternal) Summary(obj.potentialsOf(target), Effects.empty) else Summary.empty case _: Cold => Summary.empty // error already reported, ignore case _ => - val (pots, effs) = expand(pot1) - val (pots2, effs2) = pots.select(sym, pot.source) - (pots2, effs ++ effs2) + val Summary(pots, effs) = expand(pot1) + val Summary(pots2, effs2) = pots.select(sym, pot.source) + Summary(pots2, effs ++ effs2) } case FieldReturn(pot1, sym) => pot1 match { case thisRef: ThisRef => val target = resolve(state.thisClass, sym) - if (sym.isInternal) (thisRef.potentialsOf(target), Effects.empty) - else (Cold()(pot.source).toPots, Effects.empty) + if (sym.isInternal) Summary(thisRef.potentialsOf(target), Effects.empty) + else Summary(Cold()(pot.source)) case SuperRef(thisRef: ThisRef, supercls) => val target = resolveSuper(state.thisClass, supercls, sym) - if (target.isInternal) (thisRef.potentialsOf(target), Effects.empty) - else (Cold()(pot.source).toPots, Effects.empty) + if (target.isInternal) Summary(thisRef.potentialsOf(target), Effects.empty) + else Summary(Cold()(pot.source)) case _: Fun => throw new Exception("Unexpected code reached") case warm: Warm => val target = resolve(warm.classSymbol, sym) - if (target.isInternal) (warm.potentialsOf(target), Effects.empty) - else (Cold()(pot.source).toPots, Effects.empty) + if (target.isInternal) Summary(warm.potentialsOf(target), Effects.empty) + else Summary(Cold()(pot.source)) case lhot: LocalHot => val target = resolve(lhot.classSymbol, sym) - if (target.isInternal) (lhot.potentialsOf(target), Effects.empty) - else (Cold()(pot.source).toPots, Effects.empty) + if (target.isInternal) Summary(lhot.potentialsOf(target), Effects.empty) + else Summary(Cold()(pot.source)) case obj @ Global(tmref) => val target = resolve(obj.moduleClass, sym) - if (target.isInternal) (obj.potentialsOf(target), Effects.empty) - else (Cold()(pot.source).toPots, Effects.empty) + if (target.isInternal) Summary(obj.potentialsOf(target), Effects.empty) + else Summary(Cold()(pot.source)) case _: Cold => Summary.empty // error already reported, ignore case _ => - val (pots, effs) = expand(pot1) - val (pots2, effs2) = pots.select(sym, pot.source) - (pots2, effs ++ effs2) + val Summary(pots, effs) = expand(pot1) + val Summary(pots2, effs2) = pots.select(sym, pot.source) + Summary(pots2, effs ++ effs2) } case Outer(pot1, cls) => @@ -437,21 +439,21 @@ object Checking { throw new Exception("Unexpected code reached") case warm: Warm => - (warm.resolveOuter(cls), Effects.empty) + Summary(warm.resolveOuter(cls)) case _ => - val (pots, effs) = expand(pot1) + val Summary(pots, effs) = expand(pot1) val pots2 = pots.map { Outer(_, cls)(pot.source): Potential } - (pots2, effs) + Summary(pots2, effs) } case _: ThisRef | _: Fun | _: Warm | _: Cold | _: Global | _: LocalHot => - (Set(pot), Effects.empty) + Summary(pot) case SuperRef(pot1, supercls) => - val (pots, effs) = expand(pot1) + val Summary(pots, effs) = expand(pot1) val pots2 = pots.map { SuperRef(_, supercls)(pot.source): Potential } - (pots2, effs) + Summary(pots2, effs) } } } diff --git a/compiler/src/dotty/tools/dotc/transform/init/Potentials.scala b/compiler/src/dotty/tools/dotc/transform/init/Potentials.scala index 74f6e954408c..f81f26a5d976 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Potentials.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Potentials.scala @@ -179,20 +179,20 @@ object Potentials { extension (pot: Potential) def toPots: Potentials = Potentials.empty + pot extension (ps: Potentials) def select (symbol: Symbol, source: Tree)(using Context): Summary = - ps.foldLeft(Summary.empty) { case ((pots, effs), pot) => + ps.foldLeft(Summary.empty) { case (Summary(pots, effs), pot) => // max potential length // TODO: it can be specified on a project basis via compiler options if (pot.size > 2) - (pots, effs + Promote(pot)(source)) + Summary(pots, effs + Promote(pot)(source)) else if (symbol.isConstructor) - (pots + pot, effs + MethodCall(pot, symbol)(source)) + Summary(pots + pot, effs + MethodCall(pot, symbol)(source)) else if (symbol.isOneOf(Flags.Method | Flags.Lazy)) - ( + Summary( pots + MethodReturn(pot, symbol)(source), effs + MethodCall(pot, symbol)(source) ) else - (pots + FieldReturn(pot, symbol)(source), effs + FieldAccess(pot, symbol)(source)) + Summary(pots + FieldReturn(pot, symbol)(source), effs + FieldAccess(pot, symbol)(source)) } extension (ps: Potentials) def promote(source: Tree): Effects = ps.map(Promote(_)(source)) diff --git a/compiler/src/dotty/tools/dotc/transform/init/Summarization.scala b/compiler/src/dotty/tools/dotc/transform/init/Summarization.scala index b080256c8a95..3ee0f1c75a5b 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Summarization.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Summarization.scala @@ -26,7 +26,7 @@ object Summarization { * safely abandoned, as they are always fully initialized. */ def analyze(expr: Tree)(implicit env: Env): Summary = - trace("summarizing " + expr.show, init, s => Summary.show(s.asInstanceOf[Summary])) { + trace("summarizing " + expr.show, init, s => s.asInstanceOf[Summary].show) { val summary: Summary = expr match { case Ident(nme.WILDCARD) => // TODO: disallow `var x: T = _` @@ -40,14 +40,14 @@ object Summarization { analyze(supert.tpe, supert) case Select(qualifier, name) => - val (pots, effs) = analyze(qualifier) - if (env.ignoredMethods.contains(expr.symbol)) (Potentials.empty, effs) + val Summary(pots, effs) = analyze(qualifier) + if (env.ignoredMethods.contains(expr.symbol)) Summary(effs) else if (!expr.symbol.exists) { // polymorphic function apply and structural types - (Potentials.empty, pots.promote(expr) ++ effs) + Summary(pots.promote(expr) ++ effs) } else { - val (pots2, effs2) = pots.select(expr.symbol, expr) - (pots2, effs ++ effs2) + val Summary(pots2, effs2) = pots.select(expr.symbol, expr) + Summary(pots2, effs ++ effs2) } case _: This => @@ -61,13 +61,13 @@ object Summarization { case mt: MethodType => mt.paramInfos val res = args.zip(argTps).foldLeft(summary) { case (sum, (arg, argTp)) => - val (pots1, effs1) = analyze(arg) - if (ignoredCall) sum.withEffs(effs1) + val Summary(pots1, effs1) = analyze(arg) + if (ignoredCall) sum ++ effs1 else if (argTp.isInstanceOf[ExprType]) sum + Promote(Fun(pots1, effs1)(arg))(arg) - else sum.withEffs(pots1.promote(arg) ++ effs1) + else sum ++ pots1.promote(arg) ++ effs1 } - if (ignoredCall) (Potentials.empty, res._2) + if (ignoredCall) Summary(res.effs) else res case TypeApply(fun, args) => @@ -89,29 +89,29 @@ object Summarization { val cur = theCtx.owner.lexicallyEnclosingClass.asClass val thisRef = ThisRef()(expr) val enclosing = cls.owner.lexicallyEnclosingClass.asClass - val (pots, effs) = resolveThis(enclosing, thisRef, cur, expr) - val summary = Summary.empty.withEffs(effs) + val Summary(pots, effs) = resolveThis(enclosing, thisRef, cur, expr) + val summary = Summary(effs) if pots.isEmpty then if env.checkGlobal then summary + LocalHot(cls)(expr) else summary else { assert(pots.size == 1) - (Warm(cls, pots.head)(expr).toPots, effs) + summary + Warm(cls, pots.head)(expr) } } else { - val (pots, effs) = analyze(tref.prefix, expr) - val summary = Summary.empty.withEffs(effs) - if pots.isEmpty then + val summary = analyze(tref.prefix, expr) + if summary.pots.isEmpty then if env.checkGlobal then summary + LocalHot(cls)(expr) else summary else { - assert(pots.size == 1) - val pot = pots.head + assert(summary.pots.size == 1) + val pot = summary.pots.head + val res = summary.dropPotentials if env.checkGlobal && Potentials.isGlobalPath(pot) then - summary + LocalHot(cls)(expr) + res + LocalHot(cls)(expr) else - summary + Warm(cls, pot)(expr) + res + Warm(cls, pot)(expr) } } @@ -123,23 +123,23 @@ object Summarization { analyze(arg) case Assign(lhs, rhs) => - val (pots, effs) = analyze(rhs) - (Potentials.empty, pots.promote(expr) ++ effs) + val Summary(pots, effs) = analyze(rhs) + Summary(pots.promote(expr) ++ effs) case closureDef(ddef) => // must be before `Block` - val (pots, effs) = analyze(ddef.rhs) - Summary.empty + Fun(pots, effs)(expr) + val Summary(pots, effs) = analyze(ddef.rhs) + Summary(Fun(pots, effs)(expr)) case Block(stats, expr) => val effs = stats.foldLeft(Effects.empty) { (acc, stat) => acc ++ analyze(stat)._2 } - val (pots2, effs2) = analyze(expr) - (pots2, effs ++ effs2) + val Summary(pots2, effs2) = analyze(expr) + Summary(pots2, effs ++ effs2) case If(cond, thenp, elsep) => - val (pots0, effs0) = analyze(cond) - val (pots1, effs1) = analyze(thenp) - val (pots2, effs2) = analyze(elsep) - (pots0 ++ pots1 ++ pots2, effs0 ++ effs1 ++ effs2) + val Summary(_, effs0) = analyze(cond) + val Summary(pots1, effs1) = analyze(thenp) + val Summary(pots2, effs2) = analyze(elsep) + Summary(pots1 ++ pots2, effs0 ++ effs1 ++ effs2) case Annotated(arg, annot) => if (expr.tpe.hasAnnotation(defn.UncheckedAnnot)) Summary.empty @@ -147,8 +147,9 @@ object Summarization { case Match(selector, cases) => // possible for switches - val (pots, effs) = analyze(selector) - cases.foldLeft((Potentials.empty, pots.promote(selector) ++ effs)) { (acc, cas) => + val Summary(pots, effs) = analyze(selector) + val init = Summary(Potentials.empty, pots.promote(selector) ++ effs) + cases.foldLeft(init) { (acc, cas) => acc union analyze(cas.body) } @@ -156,54 +157,57 @@ object Summarization { // Summary.empty case Return(expr, from) => - val (pots, effs) = analyze(expr) - (Potentials.empty, effs ++ pots.promote(expr)) + val Summary(pots, effs) = analyze(expr) + Summary(effs ++ pots.promote(expr)) case WhileDo(cond, body) => // for lazy fields, the translation may result in `while ()` - val (_, effs1) = if (cond.isEmpty) Summary.empty else analyze(cond) - val (_, effs2) = analyze(body) - (Potentials.empty, effs1 ++ effs2) + val Summary(_, effs1) = if (cond.isEmpty) Summary.empty else analyze(cond) + val Summary(_, effs2) = analyze(body) + Summary(effs1 ++ effs2) case Labeled(_, expr) => - val (_, effs1) = analyze(expr) - (Potentials.empty, effs1) + val summary = analyze(expr) + summary.dropPotentials case Try(block, cases, finalizer) => - val (pots, effs) = cases.foldLeft(analyze(block)) { (acc, cas) => + val Summary(pots, effs) = cases.foldLeft(analyze(block)) { (acc, cas) => acc union analyze(cas.body) } - val (_, eff2) = if (finalizer.isEmpty) Summary.empty else analyze(finalizer) - (pots, effs ++ eff2) + val Summary(_, eff2) = if (finalizer.isEmpty) Summary.empty else analyze(finalizer) + Summary(pots, effs ++ eff2) case SeqLiteral(elems, elemtpt) => val effsAll: Effects = elems.foldLeft(Effects.empty) { (effs, elem) => - val (pots1, effs1) = analyze(elem) + val Summary(pots1, effs1) = analyze(elem) pots1.promote(expr) ++ effs1 ++ effs } - (Potentials.empty, effsAll) + Summary(effsAll) case Inlined(call, bindings, expansion) => - val effs = bindings.foldLeft(Effects.empty) { (acc, mdef) => acc ++ analyze(mdef)._2 } - analyze(expansion).withEffs(effs) + val effs = bindings.foldLeft(Effects.empty) { (acc, mdef) => + acc ++ analyze(mdef)._2 + } + analyze(expansion) ++ effs case vdef : ValDef => - lazy val (pots, effs) = analyze(vdef.rhs) + val Summary(pots, effs) = analyze(vdef.rhs) if (vdef.symbol.owner.isClass) - (Potentials.empty, if (vdef.symbol.is(Flags.Lazy)) Effects.empty else effs) + if (vdef.symbol.is(Flags.Lazy)) Summary.empty else Summary(effs) else - (Potentials.empty, pots.promote(vdef) ++ effs) + Summary(pots.promote(vdef) ++ effs) case Thicket(List()) => // possible in try/catch/finally, see tests/crash/i6914.scala Summary.empty case ddef : DefDef => - lazy val (pots, effs) = analyze(ddef.rhs) - if (ddef.symbol.owner.isClass) Summary.empty - else (Potentials.empty, pots.promote(ddef) ++ effs) + else { + val Summary(pots, effs) = analyze(ddef.rhs) + Summary(pots.promote(ddef) ++ effs) + } case _: TypeDef => Summary.empty @@ -215,12 +219,12 @@ object Summarization { throw new Exception("unexpected tree: " + expr.show) } - if (env.isAlwaysInitialized(expr.tpe)) (Potentials.empty, summary._2) + if (env.isAlwaysInitialized(expr.tpe)) Summary(Potentials.empty, summary.effs) else summary } def analyze(tp: Type, source: Tree)(implicit env: Env): Summary = - trace("summarizing " + tp.show, init, s => Summary.show(s.asInstanceOf[Summary])) { + trace("summarizing " + tp.show, init, s => s.asInstanceOf[Summary].show) { val summary: Summary = tp match { case _: ConstantType => Summary.empty @@ -232,14 +236,14 @@ object Summarization { if env.checkGlobal && tmref.symbol.is(Flags.Module) && tmref.symbol.isStatic => val cls = tmref.symbol.moduleClass val pot = Global(tmref)(source) - Summary.empty + pot + AccessGlobal(pot) + MethodCall(pot, cls.primaryConstructor)(source) + Summary(pot) + AccessGlobal(pot) + MethodCall(pot, cls.primaryConstructor)(source) case tmref: TermRef => - val (pots, effs) = analyze(tmref.prefix, source) - if (env.ignoredMethods.contains(tmref.symbol)) (Potentials.empty, effs) + val Summary(pots, effs) = analyze(tmref.prefix, source) + if (env.ignoredMethods.contains(tmref.symbol)) Summary(effs) else { - val (pots2, effs2) = pots.select(tmref.symbol, source) - (pots2, effs ++ effs2) + val summary = pots.select(tmref.symbol, source) + summary ++ effs } case ThisType(tref) => @@ -248,18 +252,18 @@ object Summarization { resolveThis(cls, ThisRef()(source), enclosing, source) case SuperType(thisTp, superTp) => - val (pots, effs) = analyze(thisTp, source) + val Summary(pots, effs) = analyze(thisTp, source) val pots2 = pots.map { // TODO: properly handle super of the form A & B SuperRef(_, superTp.classSymbols.head.asClass)(source): Potential } - (pots2, effs) + Summary(pots2, effs) case _ => throw new Exception("unexpected type: " + tp.show) } - if (env.isAlwaysInitialized(tp)) (Potentials.empty, summary._2) + if (env.isAlwaysInitialized(tp)) Summary(Potentials.empty, summary.effs) else summary } @@ -275,17 +279,17 @@ object Summarization { } def resolveThis(cls: ClassSymbol, pot: Potential, cur: ClassSymbol, source: Tree)(implicit env: Env): Summary = - trace("resolve " + cls.show + ", pot = " + pot.show + ", cur = " + cur.show, init, s => Summary.show(s.asInstanceOf[Summary])) { + trace("resolve " + cls.show + ", pot = " + pot.show + ", cur = " + cur.show, init, s => s.asInstanceOf[Summary].show) { if (cls.is(Flags.Package)) Summary.empty - else if (cls == cur) (pot.toPots, Effects.empty) - else if (pot.size > 2) (Potentials.empty, Promote(pot)(source).toEffs) + else if (cls == cur) Summary(pot) + else if (pot.size > 2) Summary(Promote(pot)(source)) else { val enclosing = cur.owner.lexicallyEnclosingClass.asClass // Dotty uses O$.this outside of the object O if (enclosing.is(Flags.Package) && cls.is(Flags.Module)) if env.checkGlobal then val pot = Global(cls.sourceModule.termRef)(source) - Summary.empty + pot + AccessGlobal(pot) + MethodCall(pot, cls.primaryConstructor)(source) + Summary(pot) + AccessGlobal(pot) + MethodCall(pot, cls.primaryConstructor)(source) else return Summary.empty @@ -297,7 +301,7 @@ object Summarization { /** Summarize secondary constructors or class body */ def analyzeConstructor(ctor: Symbol)(implicit env: Env): Summary = - trace("summarizing constructor " + ctor.owner.show, init, s => Summary.show(s.asInstanceOf[Summary])) { + trace("summarizing constructor " + ctor.owner.show, init, s => s.asInstanceOf[Summary].show) { if (ctor.isPrimaryConstructor) { val cls = ctor.owner.asClass val tpl = ctor.owner.defTree.asInstanceOf[TypeDef].rhs.asInstanceOf[Template] @@ -306,8 +310,8 @@ object Summarization { def parentArgEffsWithInit(stats: List[Tree], ctor: Symbol, source: Tree): Effects = val initCall = MethodCall(ThisRef()(source), ctor)(source) stats.foldLeft(Set(initCall)) { (acc, stat) => - val (_, effs) = Summarization.analyze(stat) - acc ++ effs + val summary = Summarization.analyze(stat) + acc ++ summary.effs } val effsAll = tpl.parents.foldLeft(effs) { (effs, parent) => @@ -336,7 +340,7 @@ object Summarization { }) } - (Potentials.empty, effsAll) + Summary(effsAll) } else { val ddef = ctor.defTree.asInstanceOf[DefDef] @@ -350,9 +354,9 @@ object Summarization { val parentCls = tref.classSymbol.asClass val env2: Env = env.withOwner(cls.owner.lexicallyEnclosingClass) if (tref.prefix != NoPrefix) - parentCls -> analyze(tref.prefix, source)(env2)._1 + parentCls -> analyze(tref.prefix, source)(env2).pots else - parentCls -> analyze(cls.owner.lexicallyEnclosingClass.thisType, source)(env2)._1 + parentCls -> analyze(cls.owner.lexicallyEnclosingClass.thisType, source)(env2).pots } if (cls.defTree.isEmpty) diff --git a/compiler/src/dotty/tools/dotc/transform/init/Summary.scala b/compiler/src/dotty/tools/dotc/transform/init/Summary.scala index 0338f1cb0a5d..d4b72d4c0214 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Summary.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Summary.scala @@ -3,6 +3,7 @@ package transform package init import scala.collection.mutable +import scala.annotation.targetName import core._ import Contexts._ @@ -12,65 +13,80 @@ import config.Printers.init import Potentials._, Effects._, Util._ -object Summary { - type Summary = (Potentials, Effects) - val empty: Summary = (Potentials.empty, Effects.empty) - - /** Summary of class. - * - * It makes ObjectPart construction easier with already established raw outer for parents. - */ - case class ClassSummary(currentClass: ClassSymbol, parentOuter: Map[ClassSymbol, Potentials]) { - private val summaryCache: mutable.Map[Symbol, Summary] = mutable.Map.empty - - def cacheFor(member: Symbol, summary: Summary)(using Context): Unit = { - traceIndented("cache for " + member.show + ", summary = " + Summary.show(summary), init) - assert(member.owner == currentClass, "owner = " + member.owner.show + ", current = " + currentClass.show) - summaryCache(member) = summary - } +case class Summary(pots: Potentials, effs: Effects) { + def union(summary2: Summary): Summary = + Summary(pots ++ summary2.pots, this.effs ++ summary2.effs) - def summaryOf(member: Symbol)(implicit env: Env): Summary = - if (summaryCache.contains(member)) summaryCache(member) - else trace("summary for " + member.show, init, s => Summary.show(s.asInstanceOf[Summary])) { - implicit val env2 = env.withOwner(member) - val summary = - if (member.isConstructor) - Summarization.analyzeConstructor(member) - else if (member.is(Flags.Method)) - Summarization.analyzeMethod(member) - else // field - Summarization.analyzeField(member) - - summaryCache(member) = summary - summary - } - - def effectsOf(member: Symbol)(implicit env: Env): Effects = summaryOf(member)._2 - def potentialsOf(member: Symbol)(implicit env: Env): Potentials = summaryOf(member)._1 - - def show(using Context): String = - "ClassSummary(" + currentClass.name.show + - ", parents = " + parentOuter.map { case (k, v) => k.show + "->" + "[" + Potentials.show(v) + "]" } - } + def +(pot: Potential): Summary = + Summary(pots + pot, effs) + + def +(eff: Effect): Summary = + Summary(pots, effs + eff) - def show(summary: Summary)(using Context): String = { - val pots = Potentials.show(summary._1) - val effs = Effects.show(summary._2) + def dropPotentials: Summary = + Summary(Potentials.empty, effs) + + @targetName("withPotentials") + def ++(pots: Potentials): Summary = + Summary(this.pots ++ pots, effs) + + @targetName("withEffects") + def ++(effs: Effects): Summary = + Summary(pots, this.effs ++ effs) + + def show(using Context): String = { + val pots = Potentials.show(this.pots) + val effs = Effects.show(this.effs) s"([$pots], [$effs])" } +} - extension (summary1: Summary) def union (summary2: Summary): Summary = - (summary1._1 ++ summary2._1, summary1._2 ++ summary2._2) +object Summary { + val empty: Summary = Summary(Potentials.empty, Effects.empty) - extension (summary: Summary) def + (pot: Potential): Summary = - (summary._1 + pot, summary._2) + def apply(pots: Potentials): Summary = new Summary(pots, Effects.empty) - extension (summary: Summary) def + (eff: Effect): Summary = - (summary._1, summary._2 + eff) + @targetName("withEffects") + def apply(effs: Effects): Summary = new Summary(Potentials.empty, effs) + + def apply(pot: Potential): Summary = new Summary(Potentials.empty + pot, Effects.empty) + + def apply(eff: Effect): Summary = new Summary(Potentials.empty, Effects.empty + eff) +} + +/** Summary of class. + * + * It makes ObjectPart construction easier with already established raw outer for parents. + */ +case class ClassSummary(currentClass: ClassSymbol, parentOuter: Map[ClassSymbol, Potentials]) { + private val summaryCache: mutable.Map[Symbol, Summary] = mutable.Map.empty + + def cacheFor(member: Symbol, summary: Summary)(using Context): Unit = { + traceIndented("cache for " + member.show + ", summary = " + summary.show, init) + assert(member.owner == currentClass, "owner = " + member.owner.show + ", current = " + currentClass.show) + summaryCache(member) = summary + } + + def summaryOf(member: Symbol)(implicit env: Env): Summary = + if (summaryCache.contains(member)) summaryCache(member) + else trace("summary for " + member.show, init, s => s.asInstanceOf[Summary].show) { + implicit val env2 = env.withOwner(member) + val summary = + if (member.isConstructor) + Summarization.analyzeConstructor(member) + else if (member.is(Flags.Method)) + Summarization.analyzeMethod(member) + else // field + Summarization.analyzeField(member) + + summaryCache(member) = summary + summary + } - extension (summary: Summary) def withPots (pots: Potentials): Summary = - (summary._1 ++ pots, summary._2) + def effectsOf(member: Symbol)(implicit env: Env): Effects = summaryOf(member).effs + def potentialsOf(member: Symbol)(implicit env: Env): Potentials = summaryOf(member).pots - extension (summary: Summary) def withEffs (effs: Effects): Summary = - (summary._1, summary._2 ++ effs) + def show(using Context): String = + "ClassSummary(" + currentClass.name.show + + ", parents = " + parentOuter.map { case (k, v) => k.show + "->" + "[" + Potentials.show(v) + "]" } } From 7068ea7e9ecc7c27bd22db368680a9f76e63866a Mon Sep 17 00:00:00 2001 From: Liu Fengyun Date: Sat, 28 Nov 2020 16:00:46 +0100 Subject: [PATCH 10/34] Rename isInternal to hasSource --- .../tools/dotc/transform/init/Checking.scala | 30 +++++++++---------- .../tools/dotc/transform/init/Util.scala | 2 +- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/transform/init/Checking.scala b/compiler/src/dotty/tools/dotc/transform/init/Checking.scala index 15795f363a08..833c3913a9b5 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Checking.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Checking.scala @@ -258,7 +258,7 @@ object Checking { val target = resolve(state.thisClass, sym) if (!target.isOneOf(Flags.Method | Flags.Lazy)) check(FieldAccess(pot, target)(eff.source)) - else if (target.isInternal) { + else if (target.hasSource) { val effs = thisRef.effectsOf(target) effs.flatMap { check(_) } } @@ -268,7 +268,7 @@ object Checking { val target = resolveSuper(state.thisClass, supercls, sym) if (!target.is(Flags.Method)) check(FieldAccess(pot, target)(eff.source)) - else if (target.isInternal) { + else if (target.hasSource) { val effs = thisRef.effectsOf(target) effs.flatMap { check(_) } } @@ -277,7 +277,7 @@ object Checking { case warm @ Warm(cls, outer) => val target = resolve(cls, sym) - if (target.isInternal) { + if (target.hasSource) { val effs = warm.effectsOf(target) effs.flatMap { check(_) } } @@ -293,7 +293,7 @@ object Checking { val target = resolve(pot.moduleClass, sym) if (!target.is(Flags.Method)) check(FieldAccess(pot, target)(eff.source)) - else if (target.isInternal) { + else if (target.hasSource) { val effs = pot.effectsOf(target) effs.flatMap { check(_) } } @@ -306,7 +306,7 @@ object Checking { val target = resolve(lhot.classSymbol, sym) if (!target.is(Flags.Method)) check(FieldAccess(pot, target)(eff.source)) - else if (target.isInternal) { + else if (target.hasSource) { val effs = lhot.effectsOf(target) effs.flatMap { check(_) } } @@ -344,12 +344,12 @@ object Checking { pot1 match { case thisRef: ThisRef => val target = resolve(state.thisClass, sym) - if (target.isInternal) Summary(thisRef.potentialsOf(target), Effects.empty) + if (target.hasSource) Summary(thisRef.potentialsOf(target), Effects.empty) else Summary.empty // warning already issued in call effect case SuperRef(thisRef: ThisRef, supercls) => val target = resolveSuper(state.thisClass, supercls, sym) - if (target.isInternal) Summary(thisRef.potentialsOf(target), Effects.empty) + if (target.hasSource) Summary(thisRef.potentialsOf(target), Effects.empty) else Summary.empty // warning already issued in call effect @@ -368,17 +368,17 @@ object Checking { case warm : Warm => val target = resolve(warm.classSymbol, sym) - if (target.isInternal) Summary(warm.potentialsOf(target), Effects.empty) + if (target.hasSource) Summary(warm.potentialsOf(target), Effects.empty) else Summary.empty // warning already issued in call effect case lhot: LocalHot => val target = resolve(lhot.classSymbol, sym) - if (target.isInternal) Summary(lhot.potentialsOf(target), Effects.empty) + if (target.hasSource) Summary(lhot.potentialsOf(target), Effects.empty) else Summary.empty case obj @ Global(tmref) => val target = resolve(obj.moduleClass, sym) - if (target.isInternal) Summary(obj.potentialsOf(target), Effects.empty) + if (target.hasSource) Summary(obj.potentialsOf(target), Effects.empty) else Summary.empty case _: Cold => @@ -394,12 +394,12 @@ object Checking { pot1 match { case thisRef: ThisRef => val target = resolve(state.thisClass, sym) - if (sym.isInternal) Summary(thisRef.potentialsOf(target), Effects.empty) + if (sym.hasSource) Summary(thisRef.potentialsOf(target), Effects.empty) else Summary(Cold()(pot.source)) case SuperRef(thisRef: ThisRef, supercls) => val target = resolveSuper(state.thisClass, supercls, sym) - if (target.isInternal) Summary(thisRef.potentialsOf(target), Effects.empty) + if (target.hasSource) Summary(thisRef.potentialsOf(target), Effects.empty) else Summary(Cold()(pot.source)) case _: Fun => @@ -407,17 +407,17 @@ object Checking { case warm: Warm => val target = resolve(warm.classSymbol, sym) - if (target.isInternal) Summary(warm.potentialsOf(target), Effects.empty) + if (target.hasSource) Summary(warm.potentialsOf(target), Effects.empty) else Summary(Cold()(pot.source)) case lhot: LocalHot => val target = resolve(lhot.classSymbol, sym) - if (target.isInternal) Summary(lhot.potentialsOf(target), Effects.empty) + if (target.hasSource) Summary(lhot.potentialsOf(target), Effects.empty) else Summary(Cold()(pot.source)) case obj @ Global(tmref) => val target = resolve(obj.moduleClass, sym) - if (target.isInternal) Summary(obj.potentialsOf(target), Effects.empty) + if (target.hasSource) Summary(obj.potentialsOf(target), Effects.empty) else Summary(Cold()(pot.source)) case _: Cold => diff --git a/compiler/src/dotty/tools/dotc/transform/init/Util.scala b/compiler/src/dotty/tools/dotc/transform/init/Util.scala index ac3d12364d26..41d77a8fe608 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Util.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Util.scala @@ -19,7 +19,7 @@ object Util { traceIndented(s"<== ${msg}", printer) } - extension (symbol: Symbol) def isInternal(using Context): Boolean = + extension (symbol: Symbol) def hasSource(using Context): Boolean = !symbol.defTree.isEmpty def resolve(cls: ClassSymbol, sym: Symbol)(using Context): Symbol = From 1718b308f64384305de6b593b925b907aab9dec1 Mon Sep 17 00:00:00 2001 From: Liu Fengyun Date: Sat, 28 Nov 2020 17:11:28 +0100 Subject: [PATCH 11/34] Check promotion of potentials --- .../tools/dotc/transform/init/Checking.scala | 85 +++++++++++++++---- .../tools/dotc/transform/init/Errors.scala | 8 +- 2 files changed, 73 insertions(+), 20 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/transform/init/Checking.scala b/compiler/src/dotty/tools/dotc/transform/init/Checking.scala index 833c3913a9b5..091c5ae862c5 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Checking.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Checking.scala @@ -165,6 +165,42 @@ object Checking { } error.issue } + private def checkPromotion(pot: Refinable, source: Tree)(implicit state: State): Errors = + val buffer = new mutable.ArrayBuffer[Effect] + val classRef = + pot match + case lhot: LocalHot => lhot.classSymbol.typeRef + case obj: Global => obj.tmref + case warm: Warm => warm.classSymbol.typeRef + case _: ThisRef => ??? // impossible + + val accessibleFlags = Flags.Deferred | Flags.Private | Flags.Protected + + classRef.fields.foreach { denot => + val f = denot.symbol + if f.isOneOf(accessibleFlags) then + buffer += Promote(FieldReturn(pot, f)(source))(source) + buffer += FieldAccess(pot, f)(source) + } + + classRef.membersBasedOnFlags(Flags.Method, accessibleFlags).foreach { denot => + val m = denot.symbol + buffer += MethodCall(pot, m)(source) + buffer += Promote(MethodReturn(pot, m)(source))(source) + } + + classRef.memberClasses.foreach { denot => + val cls = denot.symbol.asClass + val potInner = + pot match + case warm: Warm => Warm(cls, pot)(source) + case _ => LocalHot(cls)(source) + buffer += MethodCall(potInner, cls.primaryConstructor)(source) + buffer += Promote(potInner)(source) + } + + buffer.toList.flatMap(eff => check(eff)) + private def check(eff: Effect)(implicit state: State): Errors = if (state.hasVisited(eff)) Errors.empty else trace("checking effect " + eff.show, init, errs => Errors.show(errs.asInstanceOf[Errors])) { @@ -182,28 +218,45 @@ object Checking { case pot @ Warm(cls, outer) => val errors = state.test { check(Promote(outer)(eff.source)) } if (errors.isEmpty) Errors.empty - else PromoteWarm(pot, eff.source, state2.path).toErrors + else + val errs = state.test { checkPromotion(pot, eff.source) } + if errs.nonEmpty then UnsafePromotion(pot, eff.source, state2.path, errs).toErrors + else Errors.empty case Fun(pots, effs) => - val errs1 = state.test { effs.flatMap { check(_) } } - val errs2 = state.test { pots.flatMap { pot => check(Promote(pot)(eff.source))(state.copy(path = Vector.empty)) } } + val errs1 = state.test { + effs.toList.flatMap { check(_) } + } + + val errs2 = state.test { + pots.toList.flatMap { pot => + check(Promote(pot)(eff.source))(state.copy(path = Vector.empty)) + } + } + if (errs1.nonEmpty || errs2.nonEmpty) UnsafePromotion(pot, eff.source, state2.path, errs1 ++ errs2).toErrors else Errors.empty - case Global(tmref) => + case obj @ Global(tmref) => if !state.isGlobalObject then Errors.empty - else ??? // check all public members + else + val errs = state.test { checkPromotion(obj, eff.source) } + if errs.nonEmpty then UnsafePromotion(obj, eff.source, state2.path, errs).toErrors + else Errors.empty - case LocalHot(cls) => + case lhot @ LocalHot(cls) => if !state.isGlobalObject then Errors.empty - else ??? // check all public members + else + val errs = state.test { checkPromotion(lhot, eff.source) } + if errs.nonEmpty then UnsafePromotion(lhot, eff.source, state2.path, errs).toErrors + else Errors.empty case pot => val Summary(pots, effs) = expand(pot) val effs2 = pots.map(Promote(_)(eff.source)) - (effs2 ++ effs).flatMap(check(_)) + (effs2 ++ effs).toList.flatMap(check(_)) } case FieldAccess(pot, field) => @@ -248,7 +301,7 @@ object Checking { case pot => val Summary(pots, effs) = expand(pot) val effs2 = pots.map(FieldAccess(_, field)(eff.source)) - (effs2 ++ effs).flatMap(check(_)) + (effs2 ++ effs).toList.flatMap(check(_)) } @@ -259,7 +312,7 @@ object Checking { if (!target.isOneOf(Flags.Method | Flags.Lazy)) check(FieldAccess(pot, target)(eff.source)) else if (target.hasSource) { - val effs = thisRef.effectsOf(target) + val effs = thisRef.effectsOf(target).toList effs.flatMap { check(_) } } else CallUnknown(target, eff.source, state2.path).toErrors @@ -269,7 +322,7 @@ object Checking { if (!target.is(Flags.Method)) check(FieldAccess(pot, target)(eff.source)) else if (target.hasSource) { - val effs = thisRef.effectsOf(target) + val effs = thisRef.effectsOf(target).toList effs.flatMap { check(_) } } else CallUnknown(target, eff.source, state2.path).toErrors @@ -278,7 +331,7 @@ object Checking { val target = resolve(cls, sym) if (target.hasSource) { - val effs = warm.effectsOf(target) + val effs = warm.effectsOf(target).toList effs.flatMap { check(_) } } else if (!sym.isConstructor) @@ -294,7 +347,7 @@ object Checking { if (!target.is(Flags.Method)) check(FieldAccess(pot, target)(eff.source)) else if (target.hasSource) { - val effs = pot.effectsOf(target) + val effs = pot.effectsOf(target).toList effs.flatMap { check(_) } } else CallUnknown(target, eff.source, state2.path).toErrors @@ -307,7 +360,7 @@ object Checking { if (!target.is(Flags.Method)) check(FieldAccess(pot, target)(eff.source)) else if (target.hasSource) { - val effs = lhot.effectsOf(target) + val effs = lhot.effectsOf(target).toList effs.flatMap { check(_) } } else CallUnknown(target, eff.source, state2.path).toErrors @@ -317,14 +370,14 @@ object Checking { case Fun(pots, effs) => // TODO: assertion might be false, due to SAM - if (sym.name.toString == "apply") effs.flatMap { check(_) } + if (sym.name.toString == "apply") effs.toList.flatMap { check(_) } else Errors.empty // curried, tupled, toString are harmless case pot => val Summary(pots, effs) = expand(pot) val effs2 = pots.map(MethodCall(_, sym)(eff.source)) - (effs2 ++ effs).flatMap(check(_)) + (effs2 ++ effs).toList.flatMap(check(_)) } case AccessGlobal(pot) => diff --git a/compiler/src/dotty/tools/dotc/transform/init/Errors.scala b/compiler/src/dotty/tools/dotc/transform/init/Errors.scala index b0c9b4c6e29c..4c9a220b8c00 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Errors.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Errors.scala @@ -12,8 +12,8 @@ import Types._, Symbols._, Contexts._ import Effects._, Potentials._ object Errors { - type Errors = Set[Error] - val empty: Errors = Set.empty + type Errors = List[Error] + val empty: Errors = Nil def show(errs: Errors)(using Context): String = errs.map(_.show).mkString(", ") @@ -26,7 +26,7 @@ object Errors { def issue(using Context): Unit = report.warning(show + stacktrace, source.srcPos) - def toErrors: Errors = Set(this) + def toErrors: Errors = this :: Nil def stacktrace(using Context): String = if (trace.isEmpty) "" else " Calling trace:\n" + { var indentCount = 0 @@ -55,7 +55,7 @@ object Errors { */ def flatten: Errors = this match { case unsafe: UnsafePromotion => unsafe.errors.flatMap(_.flatten) - case _ => Set(this) + case _ => this :: Nil } } From a2c30c91c231f911a7dd0793a56e42d52b90c547 Mon Sep 17 00:00:00 2001 From: Liu Fengyun Date: Sat, 28 Nov 2020 20:57:01 +0100 Subject: [PATCH 12/34] Cache safely promoted potential --- .../tools/dotc/transform/init/Checking.scala | 32 +++++++++++++++---- 1 file changed, 25 insertions(+), 7 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/transform/init/Checking.scala b/compiler/src/dotty/tools/dotc/transform/init/Checking.scala index 091c5ae862c5..eb7ed015bdb8 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Checking.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Checking.scala @@ -36,7 +36,8 @@ object Checking { fieldsInited: mutable.Set[Symbol], parentsInited: mutable.Set[ClassSymbol], env: Env, - var superCallFinished: Boolean = false // whether super call has finished, used in singleton objects + var superCallFinished: Boolean = false, // whether super call has finished, used in singleton objects + safePromoted: mutable.Set[Potential] = mutable.Set.empty // potentials that can be safely promoted ) { def isGlobalObject(using Context): Boolean = thisClass.is(Flags.Module) && thisClass.isStatic @@ -210,7 +211,16 @@ object Checking { case Promote(pot) => pot match { case pot: ThisRef => - PromoteThis(pot, eff.source, state2.path).toErrors + val classRef = state.thisClass.typeRef + val sum = classRef.fields.foldLeft(0) { (sum, denot) => + if denot.symbol.is(Flags.Lazy) then sum + else sum + 1 + } + if sum < state.fieldsInited.size then + PromoteThis(pot, eff.source, state2.path).toErrors + else + state.safePromoted += pot + Errors.empty case _: Cold => PromoteCold(eff.source, state2.path).toErrors @@ -221,7 +231,9 @@ object Checking { else val errs = state.test { checkPromotion(pot, eff.source) } if errs.nonEmpty then UnsafePromotion(pot, eff.source, state2.path, errs).toErrors - else Errors.empty + else + state.safePromoted += pot + Errors.empty case Fun(pots, effs) => val errs1 = state.test { @@ -243,15 +255,21 @@ object Checking { if !state.isGlobalObject then Errors.empty else val errs = state.test { checkPromotion(obj, eff.source) } - if errs.nonEmpty then UnsafePromotion(obj, eff.source, state2.path, errs).toErrors - else Errors.empty + if errs.nonEmpty then + UnsafePromotion(obj, eff.source, state2.path, errs).toErrors + else + state.safePromoted += pot + Errors.empty case lhot @ LocalHot(cls) => if !state.isGlobalObject then Errors.empty else val errs = state.test { checkPromotion(lhot, eff.source) } - if errs.nonEmpty then UnsafePromotion(lhot, eff.source, state2.path, errs).toErrors - else Errors.empty + if errs.nonEmpty then + UnsafePromotion(lhot, eff.source, state2.path, errs).toErrors + else + state.safePromoted += pot + Errors.empty case pot => val Summary(pots, effs) = expand(pot) From 0bd9fae9885b57bac44cef95e9be12fcd9398b3b Mon Sep 17 00:00:00 2001 From: Liu Fengyun Date: Sat, 28 Nov 2020 21:13:46 +0100 Subject: [PATCH 13/34] Refactor ignored methods handling --- .../tools/dotc/transform/init/Checking.scala | 5 +++-- .../dotty/tools/dotc/transform/init/Env.scala | 16 +++++++--------- .../dotc/transform/init/Summarization.scala | 6 +++--- 3 files changed, 13 insertions(+), 14 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/transform/init/Checking.scala b/compiler/src/dotty/tools/dotc/transform/init/Checking.scala index eb7ed015bdb8..c0d8b160ae1d 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Checking.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Checking.scala @@ -186,8 +186,9 @@ object Checking { classRef.membersBasedOnFlags(Flags.Method, accessibleFlags).foreach { denot => val m = denot.symbol - buffer += MethodCall(pot, m)(source) - buffer += Promote(MethodReturn(pot, m)(source))(source) + if !theEnv.canIgnoreMethod(m) then + buffer += MethodCall(pot, m)(source) + buffer += Promote(MethodReturn(pot, m)(source))(source) } classRef.memberClasses.foreach { denot => diff --git a/compiler/src/dotty/tools/dotc/transform/init/Env.scala b/compiler/src/dotty/tools/dotc/transform/init/Env.scala index 94d458582778..ca200c2564a1 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Env.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Env.scala @@ -23,20 +23,18 @@ given theCtx(using Env) as Context = summon[Env].ctx case class Env(ctx: Context) { private implicit def self: Env = this - // Methods that should be ignored in the checking - lazy val ignoredMethods: Set[Symbol] = Set( - defn.Any_getClass, - defn.Any_isInstanceOf, - defn.Object_eq, - defn.Object_ne, - defn.Object_synchronized - ) + /** Can the method call be ignored? */ + def canIgnoreMethod(symbol: Symbol): Boolean = + val owner = symbol.owner + owner == defn.AnyClass || + owner == defn.ObjectClass || + owner == defn.TupleClass def withCtx(newCtx: Context): Env = this.copy(ctx = newCtx) def withOwner(owner: Symbol) = this.copy(ctx = this.ctx.withOwner(owner)) - def checkGlobal: Boolean = true + def checkGlobal: Boolean = false /** Whether values of a given type is always fully initialized? * diff --git a/compiler/src/dotty/tools/dotc/transform/init/Summarization.scala b/compiler/src/dotty/tools/dotc/transform/init/Summarization.scala index 3ee0f1c75a5b..60ad6c2339df 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Summarization.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Summarization.scala @@ -41,7 +41,7 @@ object Summarization { case Select(qualifier, name) => val Summary(pots, effs) = analyze(qualifier) - if (env.ignoredMethods.contains(expr.symbol)) Summary(effs) + if (env.canIgnoreMethod(expr.symbol)) Summary(effs) else if (!expr.symbol.exists) { // polymorphic function apply and structural types Summary(pots.promote(expr) ++ effs) } @@ -55,7 +55,7 @@ object Summarization { case Apply(fun, args) => val summary = analyze(fun) - val ignoredCall = env.ignoredMethods.contains(expr.symbol) + val ignoredCall = env.canIgnoreMethod(expr.symbol) val argTps = fun.tpe.widen match case mt: MethodType => mt.paramInfos @@ -240,7 +240,7 @@ object Summarization { case tmref: TermRef => val Summary(pots, effs) = analyze(tmref.prefix, source) - if (env.ignoredMethods.contains(tmref.symbol)) Summary(effs) + if (env.canIgnoreMethod(tmref.symbol)) Summary(effs) else { val summary = pots.select(tmref.symbol, source) summary ++ effs From 28652bc3b7ecaf3e4e180e37a971af26f88508fb Mon Sep 17 00:00:00 2001 From: Liu Fengyun Date: Sat, 28 Nov 2020 22:06:10 +0100 Subject: [PATCH 14/34] Fix debugging code --- compiler/src/dotty/tools/dotc/transform/init/Effects.scala | 2 +- compiler/src/dotty/tools/dotc/transform/init/Potentials.scala | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/transform/init/Effects.scala b/compiler/src/dotty/tools/dotc/transform/init/Effects.scala index 5173fe489a95..d3eca01b8fc2 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Effects.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Effects.scala @@ -68,7 +68,7 @@ object Effects { extension (eff: Effect) def toEffs: Effects = Effects.empty + eff def asSeenFrom(eff: Effect, thisValue: Potential)(implicit env: Env): Effect = - trace(eff.show + " asSeenFrom " + thisValue.show + ", current = " + currentClass.show, init, effs => show(effs.asInstanceOf[Effects])) { eff match { + trace(eff.show + " asSeenFrom " + thisValue.show + ", current = " + currentClass.show, init, _.asInstanceOf[Effect].show) { eff match { case Promote(pot) => val pot1 = Potentials.asSeenFrom(pot, thisValue) Promote(pot1)(eff.source) diff --git a/compiler/src/dotty/tools/dotc/transform/init/Potentials.scala b/compiler/src/dotty/tools/dotc/transform/init/Potentials.scala index f81f26a5d976..317c5cea57f4 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Potentials.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Potentials.scala @@ -197,7 +197,7 @@ object Potentials { extension (ps: Potentials) def promote(source: Tree): Effects = ps.map(Promote(_)(source)) - def asSeenFrom(pot: Potential, thisValue: Potential)(implicit env: Env): Potential = trace(pot.show + " asSeenFrom " + thisValue.show, init, pot => pot.asInstanceOf[Potential].show) { + def asSeenFrom(pot: Potential, thisValue: Potential)(implicit env: Env): Potential = trace(pot.show + " asSeenFrom " + thisValue.show, init, _.asInstanceOf[Potential].show) { pot match { case MethodReturn(pot1, sym) => val pot = asSeenFrom(pot1, thisValue) From f61ddd86066f8e2b75b7d0c04e3d46c119097fb8 Mon Sep 17 00:00:00 2001 From: Liu Fengyun Date: Sat, 28 Nov 2020 22:21:09 +0100 Subject: [PATCH 15/34] Don't produce duplicate select effect in potential expansion --- .../tools/dotc/transform/init/Checking.scala | 4 ++-- .../dotc/transform/init/Potentials.scala | 21 +++++++++++-------- 2 files changed, 14 insertions(+), 11 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/transform/init/Checking.scala b/compiler/src/dotty/tools/dotc/transform/init/Checking.scala index c0d8b160ae1d..4d4fa721c15a 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Checking.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Checking.scala @@ -458,7 +458,7 @@ object Checking { case _ => val Summary(pots, effs) = expand(pot1) - val Summary(pots2, effs2) = pots.select(sym, pot.source) + val Summary(pots2, effs2) = pots.select(sym, pot.source, selectEffect = false) Summary(pots2, effs ++ effs2) } @@ -497,7 +497,7 @@ object Checking { case _ => val Summary(pots, effs) = expand(pot1) - val Summary(pots2, effs2) = pots.select(sym, pot.source) + val Summary(pots2, effs2) = pots.select(sym, pot.source, selectEffect = false) Summary(pots2, effs ++ effs2) } diff --git a/compiler/src/dotty/tools/dotc/transform/init/Potentials.scala b/compiler/src/dotty/tools/dotc/transform/init/Potentials.scala index 317c5cea57f4..188ea904d283 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Potentials.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Potentials.scala @@ -178,21 +178,24 @@ object Potentials { extension (pot: Potential) def toPots: Potentials = Potentials.empty + pot - extension (ps: Potentials) def select (symbol: Symbol, source: Tree)(using Context): Summary = - ps.foldLeft(Summary.empty) { case (Summary(pots, effs), pot) => + extension (ps: Potentials) def select (symbol: Symbol, source: Tree, selectEffect: Boolean = true)(using Context): Summary = + ps.foldLeft(Summary.empty) { case (summary, pot) => // max potential length // TODO: it can be specified on a project basis via compiler options if (pot.size > 2) - Summary(pots, effs + Promote(pot)(source)) + summary + Promote(pot)(source) else if (symbol.isConstructor) - Summary(pots + pot, effs + MethodCall(pot, symbol)(source)) + val res = summary + pot + if selectEffect then res + MethodCall(pot, symbol)(source) + else res else if (symbol.isOneOf(Flags.Method | Flags.Lazy)) - Summary( - pots + MethodReturn(pot, symbol)(source), - effs + MethodCall(pot, symbol)(source) - ) + val res = summary + MethodReturn(pot, symbol)(source) + if selectEffect then res + MethodCall(pot, symbol)(source) + else res else - Summary(pots + FieldReturn(pot, symbol)(source), effs + FieldAccess(pot, symbol)(source)) + val res = summary + FieldReturn(pot, symbol)(source) + if selectEffect then res + FieldAccess(pot, symbol)(source) + else res } extension (ps: Potentials) def promote(source: Tree): Effects = ps.map(Promote(_)(source)) From 365e0f950c0a43109b2144b5d28aabe3dc306bef Mon Sep 17 00:00:00 2001 From: Liu Fengyun Date: Sat, 28 Nov 2020 23:52:50 +0100 Subject: [PATCH 16/34] Enable hijacking check --- .../tools/dotc/transform/init/Checking.scala | 392 +++++++++--------- 1 file changed, 204 insertions(+), 188 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/transform/init/Checking.scala b/compiler/src/dotty/tools/dotc/transform/init/Checking.scala index 4d4fa721c15a..0bc9b78acdff 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Checking.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Checking.scala @@ -29,6 +29,7 @@ object Checking { * } * */ + case class State( private var visited: Set[Effect], // effects that have been checked path: Vector[Tree], // the path that leads to the current effect @@ -36,9 +37,19 @@ object Checking { fieldsInited: mutable.Set[Symbol], parentsInited: mutable.Set[ClassSymbol], env: Env, - var superCallFinished: Boolean = false, // whether super call has finished, used in singleton objects - safePromoted: mutable.Set[Potential] = mutable.Set.empty // potentials that can be safely promoted + var superCallFinished: Boolean = false, // whether super call has finished, used in singleton objects + safePromoted: mutable.Set[Potential] = mutable.Set.empty, // potentials that can be safely promoted ) { + given State = this + + /** This method allows hijacking the checking logic */ + def check(eff: Effect): Errors = + trace("checking effect " + eff.show, init, errs => Errors.show(errs.asInstanceOf[Errors])) { + val state: State = this.withVisited(eff) + + if (this.hasVisited(eff)) Errors.empty + else Checking.doCheck(eff) + } def isGlobalObject(using Context): Boolean = thisClass.is(Flags.Module) && thisClass.isStatic @@ -69,7 +80,7 @@ object Checking { * However, summarization can be done lazily on-demand to improve * performance. */ - def checkClassBody(cdef: TypeDef)(implicit state: State): Unit = { + def checkClassBody(cdef: TypeDef)(using state: State): Unit = { traceIndented("\n\n>>>> checking " + cdef.symbol.show, init) val cls = cdef.symbol.asClass @@ -78,7 +89,7 @@ object Checking { // mark current class as initialized, required for linearization state.parentsInited += cls - def checkClassBodyStat(tree: Tree)(implicit state: State): Unit = traceOp("checking " + tree.show, init) { + def checkClassBodyStat(tree: Tree)(using state: State): Unit = traceOp("checking " + tree.show, init) { tree match { case vdef : ValDef => val summary = Summarization.analyze(vdef.rhs) @@ -99,16 +110,17 @@ object Checking { // see spec 5.1 about "Template Evaluation". // https://www.scala-lang.org/files/archive/spec/2.13/05-classes-and-objects.html - def checkConstructor(ctor: Symbol, tp: Type, source: Tree)(implicit state: State): Unit = traceOp("checking " + ctor.show, init) { + def checkConstructor(ctor: Symbol, tp: Type, source: Tree)(using state: State): Unit = traceOp("checking " + ctor.show, init) { val cls = ctor.owner val classDef = cls.defTree if (!classDef.isEmpty) { - if (ctor.isPrimaryConstructor) checkClassBody(classDef.asInstanceOf[TypeDef])(state.withOwner(cls)) - else checkSecondaryConstructor(ctor)(state.withOwner(cls)) + given State = state.withOwner(cls) + if (ctor.isPrimaryConstructor) checkClassBody(classDef.asInstanceOf[TypeDef]) + else checkSecondaryConstructor(ctor) } } - def checkSecondaryConstructor(ctor: Symbol)(implicit state: State): Unit = traceOp("checking " + ctor.show, init) { + def checkSecondaryConstructor(ctor: Symbol)(using state: State): Unit = traceOp("checking " + ctor.show, init) { val Block(ctorCall :: stats, expr) = ctor.defTree.asInstanceOf[DefDef].rhs val cls = ctor.owner.asClass @@ -159,14 +171,14 @@ object Checking { tpl.body.foreach { checkClassBodyStat(_) } } - private def checkEffects(effs: Effects)(implicit state: State): Unit = traceOp("checking effects " + Effects.show(effs), init) { + private def checkEffects(effs: Effects)(using state: State): Unit = traceOp("checking effects " + Effects.show(effs), init) { for { eff <- effs - error <- check(eff) + error <- state.check(eff) } error.issue } - private def checkPromotion(pot: Refinable, source: Tree)(implicit state: State): Errors = + private def checkPromotion(pot: Refinable, source: Tree)(using state: State): Errors = val buffer = new mutable.ArrayBuffer[Effect] val classRef = pot match @@ -175,18 +187,21 @@ object Checking { case warm: Warm => warm.classSymbol.typeRef case _: ThisRef => ??? // impossible - val accessibleFlags = Flags.Deferred | Flags.Private | Flags.Protected + val excludedFlags = Flags.Deferred | Flags.Private | Flags.Protected classRef.fields.foreach { denot => val f = denot.symbol - if f.isOneOf(accessibleFlags) then + if !f.isOneOf(excludedFlags) then buffer += Promote(FieldReturn(pot, f)(source))(source) buffer += FieldAccess(pot, f)(source) } - classRef.membersBasedOnFlags(Flags.Method, accessibleFlags).foreach { denot => + val methods = classRef.membersBasedOnFlags(Flags.Method, Flags.Deferred) + println("methods in " + classRef.show + ": " + methods.map(_.show).mkString(", ")) + + classRef.membersBasedOnFlags(Flags.Method, Flags.Deferred).foreach { denot => val m = denot.symbol - if !theEnv.canIgnoreMethod(m) then + if !m.isConstructor && !theEnv.canIgnoreMethod(m) then buffer += MethodCall(pot, m)(source) buffer += Promote(MethodReturn(pot, m)(source))(source) } @@ -201,216 +216,217 @@ object Checking { buffer += Promote(potInner)(source) } - buffer.toList.flatMap(eff => check(eff)) - - private def check(eff: Effect)(implicit state: State): Errors = - if (state.hasVisited(eff)) Errors.empty - else trace("checking effect " + eff.show, init, errs => Errors.show(errs.asInstanceOf[Errors])) { - implicit val state2: State = state.withVisited(eff) - - eff match { - case Promote(pot) => - pot match { - case pot: ThisRef => - val classRef = state.thisClass.typeRef - val sum = classRef.fields.foldLeft(0) { (sum, denot) => - if denot.symbol.is(Flags.Lazy) then sum - else sum + 1 - } - if sum < state.fieldsInited.size then - PromoteThis(pot, eff.source, state2.path).toErrors + buffer.toList.flatMap(eff => state.check(eff)) + + private def doCheck(eff: Effect)(using state: State): Errors = + eff match { + case Promote(pot) => + pot match { + case pot: ThisRef => + val classRef = state.thisClass.typeRef + val sum = classRef.fields.foldLeft(0) { (sum, denot) => + if denot.symbol.isOneOf(Flags.Lazy | Flags.Deferred) then sum + else sum + 1 + } + if sum < state.fieldsInited.size then + throw new Exception(s"$sum fields in " + state.thisClass + ", but " + state.fieldsInited.size + " initialized") + else if sum > state.fieldsInited.size then + PromoteThis(pot, eff.source, state.path).toErrors + else + state.safePromoted += pot + Errors.empty + + case _: Cold => + PromoteCold(eff.source, state.path).toErrors + + case pot @ Warm(cls, outer) => + // Use the assumption that `Promote(pot)` eagerly in the visited set is unsound. + // It can only be safely used for checking the expanded effects. + // tests/init/neg/hybrid2.scala + val errors = state.test { state.check(Promote(outer)(eff.source)) } + if (errors.isEmpty) Errors.empty + else + val errs = state.test { checkPromotion(pot, eff.source) } + if errs.nonEmpty then UnsafePromotion(pot, eff.source, state.path, errs).toErrors else state.safePromoted += pot Errors.empty - case _: Cold => - PromoteCold(eff.source, state2.path).toErrors + case Fun(pots, effs) => + val errs1 = state.test { + effs.toList.flatMap { state.check(_) } + } - case pot @ Warm(cls, outer) => - val errors = state.test { check(Promote(outer)(eff.source)) } - if (errors.isEmpty) Errors.empty - else - val errs = state.test { checkPromotion(pot, eff.source) } - if errs.nonEmpty then UnsafePromotion(pot, eff.source, state2.path, errs).toErrors - else - state.safePromoted += pot - Errors.empty - - case Fun(pots, effs) => - val errs1 = state.test { - effs.toList.flatMap { check(_) } + val errs2 = state.test { + pots.toList.flatMap { pot => + state.copy(path = Vector.empty).check(Promote(pot)(eff.source)) } + } - val errs2 = state.test { - pots.toList.flatMap { pot => - check(Promote(pot)(eff.source))(state.copy(path = Vector.empty)) - } - } + if (errs1.nonEmpty || errs2.nonEmpty) + UnsafePromotion(pot, eff.source, state.path, errs1 ++ errs2).toErrors + else + Errors.empty - if (errs1.nonEmpty || errs2.nonEmpty) - UnsafePromotion(pot, eff.source, state2.path, errs1 ++ errs2).toErrors + case obj @ Global(tmref) => + if !state.isGlobalObject then Errors.empty + else + val errs = state.test { checkPromotion(obj, eff.source) } + if errs.nonEmpty then + UnsafePromotion(obj, eff.source, state.path, errs).toErrors else + state.safePromoted += pot Errors.empty - case obj @ Global(tmref) => - if !state.isGlobalObject then Errors.empty + case lhot @ LocalHot(cls) => + if !state.isGlobalObject then Errors.empty + else + val errs = state.test { checkPromotion(lhot, eff.source) } + if errs.nonEmpty then + UnsafePromotion(lhot, eff.source, state.path, errs).toErrors else - val errs = state.test { checkPromotion(obj, eff.source) } - if errs.nonEmpty then - UnsafePromotion(obj, eff.source, state2.path, errs).toErrors - else - state.safePromoted += pot - Errors.empty - - case lhot @ LocalHot(cls) => - if !state.isGlobalObject then Errors.empty - else - val errs = state.test { checkPromotion(lhot, eff.source) } - if errs.nonEmpty then - UnsafePromotion(lhot, eff.source, state2.path, errs).toErrors - else - state.safePromoted += pot - Errors.empty - - case pot => - val Summary(pots, effs) = expand(pot) - val effs2 = pots.map(Promote(_)(eff.source)) - (effs2 ++ effs).toList.flatMap(check(_)) - } + state.safePromoted += pot + Errors.empty - case FieldAccess(pot, field) => + case pot => + val Summary(pots, effs) = expand(pot) + val effs2 = pots.map(Promote(_)(eff.source)) + (effs2 ++ effs).toList.flatMap(state.check(_)) + } - pot match { - case _: ThisRef => + case FieldAccess(pot, field) => + + pot match { + case _: ThisRef => + val target = resolve(state.thisClass, field) + if (target.is(Flags.Lazy)) state.check(MethodCall(pot, target)(eff.source)) + else if (!state.fieldsInited.contains(target)) AccessNonInit(target, state.path).toErrors + else Errors.empty + + case SuperRef(_: ThisRef, supercls) => + val target = resolveSuper(state.thisClass, supercls, field) + if (target.is(Flags.Lazy)) state.check(MethodCall(pot, target)(eff.source)) + else if (!state.fieldsInited.contains(target)) AccessNonInit(target, state.path).toErrors + else Errors.empty + + case Warm(cls, outer) => + // all fields of warm values are initialized + val target = resolve(cls, field) + if (target.is(Flags.Lazy)) state.check(MethodCall(pot, target)(eff.source)) + else Errors.empty + + case obj @ Global(tmref) => + if !state.isGlobalObject || obj.moduleClass != state.thisClass then + Errors.empty + else val target = resolve(state.thisClass, field) - if (target.is(Flags.Lazy)) check(MethodCall(pot, target)(eff.source)) - else if (!state.fieldsInited.contains(target)) AccessNonInit(target, state2.path).toErrors + if (target.is(Flags.Lazy)) state.check(MethodCall(pot, target)(eff.source)) + else if (!state.fieldsInited.contains(target)) AccessNonInit(target, state.path).toErrors else Errors.empty - case SuperRef(_: ThisRef, supercls) => - val target = resolveSuper(state.thisClass, supercls, field) - if (target.is(Flags.Lazy)) check(MethodCall(pot, target)(eff.source)) - else if (!state.fieldsInited.contains(target)) AccessNonInit(target, state2.path).toErrors - else Errors.empty + case _: LocalHot => + Errors.empty - case Warm(cls, outer) => - // all fields of warm values are initialized - val target = resolve(cls, field) - if (target.is(Flags.Lazy)) check(MethodCall(pot, target)(eff.source)) - else Errors.empty + case _: Cold => + AccessCold(field, eff.source, state.path).toErrors - case obj @ Global(tmref) => - if !state.isGlobalObject || obj.moduleClass != state.thisClass then - Errors.empty - else - val target = resolve(state.thisClass, field) - if (target.is(Flags.Lazy)) check(MethodCall(pot, target)(eff.source)) - else if (!state.fieldsInited.contains(target)) AccessNonInit(target, state2.path).toErrors - else Errors.empty + case Fun(pots, effs) => + throw new Exception("Unexpected effect " + eff.show) - case _: LocalHot => - Errors.empty + case pot => + val Summary(pots, effs) = expand(pot) + val effs2 = pots.map(FieldAccess(_, field)(eff.source)) + (effs2 ++ effs).toList.flatMap(state.check(_)) - case _: Cold => - AccessCold(field, eff.source, state2.path).toErrors + } - case Fun(pots, effs) => - throw new Exception("Unexpected effect " + eff.show) + case MethodCall(pot, sym) => + pot match { + case thisRef: ThisRef => + val target = resolve(state.thisClass, sym) + if (!target.isOneOf(Flags.Method | Flags.Lazy)) + state.check(FieldAccess(pot, target)(eff.source)) + else if (target.hasSource) { + val effs = thisRef.effectsOf(target).toList + effs.flatMap { state.check(_) } + } + else CallUnknown(target, eff.source, state.path).toErrors - case pot => - val Summary(pots, effs) = expand(pot) - val effs2 = pots.map(FieldAccess(_, field)(eff.source)) - (effs2 ++ effs).toList.flatMap(check(_)) + case SuperRef(thisRef: ThisRef, supercls) => + val target = resolveSuper(state.thisClass, supercls, sym) + if (!target.is(Flags.Method)) + state.check(FieldAccess(pot, target)(eff.source)) + else if (target.hasSource) { + val effs = thisRef.effectsOf(target).toList + effs.flatMap { state.check(_) } + } + else CallUnknown(target, eff.source, state.path).toErrors - } + case warm @ Warm(cls, outer) => + val target = resolve(cls, sym) - case MethodCall(pot, sym) => - pot match { - case thisRef: ThisRef => - val target = resolve(state.thisClass, sym) - if (!target.isOneOf(Flags.Method | Flags.Lazy)) - check(FieldAccess(pot, target)(eff.source)) - else if (target.hasSource) { - val effs = thisRef.effectsOf(target).toList - effs.flatMap { check(_) } - } - else CallUnknown(target, eff.source, state2.path).toErrors + if (target.hasSource) { + val effs = warm.effectsOf(target).toList + effs.flatMap { state.check(_) } + } + else if (!sym.isConstructor) + CallUnknown(target, eff.source, state.path).toErrors + else + Errors.empty - case SuperRef(thisRef: ThisRef, supercls) => - val target = resolveSuper(state.thisClass, supercls, sym) + case pot @ Global(tmref) => + if !state.isGlobalObject then + Errors.empty + else + val target = resolve(pot.moduleClass, sym) if (!target.is(Flags.Method)) - check(FieldAccess(pot, target)(eff.source)) + state.check(FieldAccess(pot, target)(eff.source)) else if (target.hasSource) { - val effs = thisRef.effectsOf(target).toList - effs.flatMap { check(_) } + val effs = pot.effectsOf(target).toList + effs.flatMap { state.check(_) } } - else CallUnknown(target, eff.source, state2.path).toErrors + else CallUnknown(target, eff.source, state.path).toErrors - case warm @ Warm(cls, outer) => - val target = resolve(cls, sym) - - if (target.hasSource) { - val effs = warm.effectsOf(target).toList - effs.flatMap { check(_) } + case lhot: LocalHot => + if !state.isGlobalObject then + Errors.empty + else + val target = resolve(lhot.classSymbol, sym) + if (!target.is(Flags.Method)) + state.check(FieldAccess(pot, target)(eff.source)) + else if (target.hasSource) { + val effs = lhot.effectsOf(target).toList + effs.flatMap { state.check(_) } } - else if (!sym.isConstructor) - CallUnknown(target, eff.source, state2.path).toErrors - else - Errors.empty + else CallUnknown(target, eff.source, state.path).toErrors - case pot @ Global(tmref) => - if !state.isGlobalObject then - Errors.empty - else - val target = resolve(pot.moduleClass, sym) - if (!target.is(Flags.Method)) - check(FieldAccess(pot, target)(eff.source)) - else if (target.hasSource) { - val effs = pot.effectsOf(target).toList - effs.flatMap { check(_) } - } - else CallUnknown(target, eff.source, state2.path).toErrors - - case lhot: LocalHot => - if !state.isGlobalObject then - Errors.empty - else - val target = resolve(lhot.classSymbol, sym) - if (!target.is(Flags.Method)) - check(FieldAccess(pot, target)(eff.source)) - else if (target.hasSource) { - val effs = lhot.effectsOf(target).toList - effs.flatMap { check(_) } - } - else CallUnknown(target, eff.source, state2.path).toErrors - - case _: Cold => - CallCold(sym, eff.source, state2.path).toErrors - - case Fun(pots, effs) => - // TODO: assertion might be false, due to SAM - if (sym.name.toString == "apply") effs.toList.flatMap { check(_) } - else Errors.empty - // curried, tupled, toString are harmless + case _: Cold => + CallCold(sym, eff.source, state.path).toErrors - case pot => - val Summary(pots, effs) = expand(pot) - val effs2 = pots.map(MethodCall(_, sym)(eff.source)) - (effs2 ++ effs).toList.flatMap(check(_)) - } + case Fun(pots, effs) => + // TODO: assertion might be false, due to SAM + if (sym.name.toString == "apply") effs.toList.flatMap { state.check(_) } + else Errors.empty + // curried, tupled, toString are harmless + + case pot => + val Summary(pots, effs) = expand(pot) + val effs2 = pots.map(MethodCall(_, sym)(eff.source)) + (effs2 ++ effs).toList.flatMap(state.check(_)) + } - case AccessGlobal(pot) => - if !state.isGlobalObject || pot.moduleClass != state.thisClass then - Errors.empty - else if state.superCallFinished then - Errors.empty - else - AccessNonInit(pot.tmref.symbol, state2.path).toErrors + case AccessGlobal(pot) => + if !state.isGlobalObject || pot.moduleClass != state.thisClass then + Errors.empty + else if state.superCallFinished then + Errors.empty + else + AccessNonInit(pot.tmref.symbol, state.path).toErrors - } } - private def expand(pot: Potential)(implicit state: State): Summary = trace("expand " + pot.show, init, _.asInstanceOf[Summary].show) { + + private def expand(pot: Potential)(using state: State): Summary = trace("expand " + pot.show, init, _.asInstanceOf[Summary].show) { pot match { case MethodReturn(pot1, sym) => pot1 match { From c7864b9661c63985285251b4cd8849ba9aa8ea14 Mon Sep 17 00:00:00 2001 From: Liu Fengyun Date: Sun, 29 Nov 2020 01:51:11 +0100 Subject: [PATCH 17/34] Fix test case tests/init/neg/hybrid2.scala --- .../tools/dotc/transform/init/Checker.scala | 3 +- .../tools/dotc/transform/init/Checking.scala | 78 +++++++++++++------ 2 files changed, 57 insertions(+), 24 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/transform/init/Checker.scala b/compiler/src/dotty/tools/dotc/transform/init/Checker.scala index 7172db190ce0..6cf71ff4ee90 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Checker.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Checker.scala @@ -47,7 +47,8 @@ class Checker extends MiniPhase { // A concrete class may not be instantiated if the self type is not satisfied if (instantiable) { implicit val state: Checking.State = Checking.State( - visited = Set.empty, + checking = Set.empty, + checked = Set.empty, path = Vector.empty, thisClass = cls, fieldsInited = mutable.Set.empty, diff --git a/compiler/src/dotty/tools/dotc/transform/init/Checking.scala b/compiler/src/dotty/tools/dotc/transform/init/Checking.scala index 0bc9b78acdff..ae4be0c0c5b6 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Checking.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Checking.scala @@ -31,7 +31,8 @@ object Checking { */ case class State( - private var visited: Set[Effect], // effects that have been checked + var checking: Set[Effect], // effects that are being checked + var checked: Set[Effect], // effects that have been checked path: Vector[Tree], // the path that leads to the current effect thisClass: ClassSymbol, // the concrete class of `this` fieldsInited: mutable.Set[Symbol], @@ -39,38 +40,47 @@ object Checking { env: Env, var superCallFinished: Boolean = false, // whether super call has finished, used in singleton objects safePromoted: mutable.Set[Potential] = mutable.Set.empty, // potentials that can be safely promoted + checkFun: (Effect, State) => Errors = defaultCheck // check function to be used ) { - given State = this - /** This method allows hijacking the checking logic */ - def check(eff: Effect): Errors = - trace("checking effect " + eff.show, init, errs => Errors.show(errs.asInstanceOf[Errors])) { - val state: State = this.withVisited(eff) - - if (this.hasVisited(eff)) Errors.empty - else Checking.doCheck(eff) - } + def check(eff: Effect)(using state: State): Errors = checkFun(eff, state) def isGlobalObject(using Context): Boolean = thisClass.is(Flags.Module) && thisClass.isStatic - def withVisited(eff: Effect): State = { - visited = visited + eff - copy(path = this.path :+ eff.source) - } - - def hasVisited(eff: Effect): Boolean = - visited.contains(eff) - def withOwner(sym: Symbol): State = copy(env = env.withOwner(sym)) def test(op: State ?=> Errors): Errors = { - val saved = visited + val savedChecked = checked + val savedChecking = checking val errors = op(using this) - visited = saved + checked = savedChecked + checking = savedChecking errors } } + /** Enable hijacking checking logic + * + * This function should not be called directly + */ + private val defaultCheck: (Effect, State) => Errors = { (eff: Effect, state: State) => + given State = state + trace("checking effect " + eff.show, init, errs => Errors.show(errs.asInstanceOf[Errors])) { + if (state.checked.contains(eff) || state.checking.contains(eff)) { + traceIndented("Already checked " + eff.show, init) + Errors.empty + } + else { + state.checking = state.checking + eff + val state2: State = state.copy(path = state.path :+ eff.source) + val errors = Checking.doCheck(eff)(using state2) + state.checking -= eff + state.checked += eff + errors + } + } + } + given theEnv(using State) as Env = summon[State].env given theCtx(using State) as Context = summon[State].env.ctx @@ -197,7 +207,7 @@ object Checking { } val methods = classRef.membersBasedOnFlags(Flags.Method, Flags.Deferred) - println("methods in " + classRef.show + ": " + methods.map(_.show).mkString(", ")) + // println("methods in " + classRef.show + ": " + methods.map(_.show).mkString(", ")) classRef.membersBasedOnFlags(Flags.Method, Flags.Deferred).foreach { denot => val m = denot.symbol @@ -240,11 +250,33 @@ object Checking { PromoteCold(eff.source, state.path).toErrors case pot @ Warm(cls, outer) => + import scala.util.control.NonLocalReturns._ + // Use the assumption that `Promote(pot)` eagerly in the visited set is unsound. // It can only be safely used for checking the expanded effects. // tests/init/neg/hybrid2.scala - val errors = state.test { state.check(Promote(outer)(eff.source)) } - if (errors.isEmpty) Errors.empty + var eagerlyUsed: Boolean = false + var capException: ReturnThrowable[Errors] = null + val checkFun = { (eff2: Effect, state2: State) => + if eff == eff2 then + traceIndented("Eagerly using " + eff.show + ", will check thoroughly", init) + eagerlyUsed = true + throwReturn(Errors.empty)(using capException) + else + state.check(eff2)(using state2) + } + + val state2 = state.copy( + checking = Set(eff), // important to be empty as eager usage might be hidden under a visited node + checkFun = checkFun + ) + + val errors = returning { + capException = summon[ReturnThrowable[Errors]] + state2.check(Promote(outer)(eff.source))(using state2) + } + + if (errors.isEmpty && !eagerlyUsed) Errors.empty else val errs = state.test { checkPromotion(pot, eff.source) } if errs.nonEmpty then UnsafePromotion(pot, eff.source, state.path, errs).toErrors From 0e35f9e0716dc8f547ff40552695ff2c20e21ab2 Mon Sep 17 00:00:00 2001 From: Liu Fengyun Date: Sun, 29 Nov 2020 11:26:38 +0100 Subject: [PATCH 18/34] Split effect check into multiple methods --- .../tools/dotc/transform/init/Checking.scala | 409 +++++++++--------- 1 file changed, 207 insertions(+), 202 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/transform/init/Checking.scala b/compiler/src/dotty/tools/dotc/transform/init/Checking.scala index ae4be0c0c5b6..971cb814fb59 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Checking.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Checking.scala @@ -73,7 +73,14 @@ object Checking { else { state.checking = state.checking + eff val state2: State = state.copy(path = state.path :+ eff.source) - val errors = Checking.doCheck(eff)(using state2) + val errors = + eff match { + case eff: Promote => Checking.checkPromote(eff)(using state2) + case eff: FieldAccess => Checking.checkFieldAccess(eff)(using state2) + case eff: MethodCall => Checking.checkMethodCall(eff)(using state2) + case eff: AccessGlobal => Checking.checkAccessGlobal(eff)(using state2) + } + state.checking -= eff state.checked += eff errors @@ -188,7 +195,7 @@ object Checking { } error.issue } - private def checkPromotion(pot: Refinable, source: Tree)(using state: State): Errors = + private def promote(pot: Refinable, source: Tree)(using state: State): Errors = val buffer = new mutable.ArrayBuffer[Effect] val classRef = pot match @@ -228,235 +235,233 @@ object Checking { buffer.toList.flatMap(eff => state.check(eff)) - private def doCheck(eff: Effect)(using state: State): Errors = - eff match { - case Promote(pot) => - pot match { - case pot: ThisRef => - val classRef = state.thisClass.typeRef - val sum = classRef.fields.foldLeft(0) { (sum, denot) => - if denot.symbol.isOneOf(Flags.Lazy | Flags.Deferred) then sum - else sum + 1 - } - if sum < state.fieldsInited.size then - throw new Exception(s"$sum fields in " + state.thisClass + ", but " + state.fieldsInited.size + " initialized") - else if sum > state.fieldsInited.size then - PromoteThis(pot, eff.source, state.path).toErrors - else - state.safePromoted += pot - Errors.empty - - case _: Cold => - PromoteCold(eff.source, state.path).toErrors - - case pot @ Warm(cls, outer) => - import scala.util.control.NonLocalReturns._ - - // Use the assumption that `Promote(pot)` eagerly in the visited set is unsound. - // It can only be safely used for checking the expanded effects. - // tests/init/neg/hybrid2.scala - var eagerlyUsed: Boolean = false - var capException: ReturnThrowable[Errors] = null - val checkFun = { (eff2: Effect, state2: State) => - if eff == eff2 then - traceIndented("Eagerly using " + eff.show + ", will check thoroughly", init) - eagerlyUsed = true - throwReturn(Errors.empty)(using capException) - else - state.check(eff2)(using state2) - } - - val state2 = state.copy( - checking = Set(eff), // important to be empty as eager usage might be hidden under a visited node - checkFun = checkFun - ) - - val errors = returning { - capException = summon[ReturnThrowable[Errors]] - state2.check(Promote(outer)(eff.source))(using state2) - } + private def checkPromote(eff: Promote)(using state: State): Errors = + val pot = eff.potential + pot match { + case pot: ThisRef => + val classRef = state.thisClass.typeRef + val sum = classRef.fields.foldLeft(0) { (sum, denot) => + if denot.symbol.isOneOf(Flags.Lazy | Flags.Deferred) then sum + else sum + 1 + } + if sum < state.fieldsInited.size then + throw new Exception(s"$sum fields in " + state.thisClass + ", but " + state.fieldsInited.size + " initialized") + else if sum > state.fieldsInited.size then + PromoteThis(pot, eff.source, state.path).toErrors + else + state.safePromoted += pot + Errors.empty - if (errors.isEmpty && !eagerlyUsed) Errors.empty - else - val errs = state.test { checkPromotion(pot, eff.source) } - if errs.nonEmpty then UnsafePromotion(pot, eff.source, state.path, errs).toErrors - else - state.safePromoted += pot - Errors.empty + case _: Cold => + PromoteCold(eff.source, state.path).toErrors + + case pot @ Warm(cls, outer) => + import scala.util.control.NonLocalReturns._ + + // Use the assumption that `Promote(pot)` eagerly in the visited set is unsound. + // It can only be safely used for checking the expanded effects. + // tests/init/neg/hybrid2.scala + var eagerlyUsed: Boolean = false + var capException: ReturnThrowable[Errors] = null + val checkFun = { (eff2: Effect, state2: State) => + if eff == eff2 then + traceIndented("Eagerly using " + eff.show + ", will check thoroughly", init) + eagerlyUsed = true + throwReturn(Errors.empty)(using capException) + else + state.check(eff2)(using state2) + } - case Fun(pots, effs) => - val errs1 = state.test { - effs.toList.flatMap { state.check(_) } - } + val state2 = state.copy( + checking = Set(eff), // important to be empty as eager usage might be hidden under a visited node + checkFun = checkFun + ) - val errs2 = state.test { - pots.toList.flatMap { pot => - state.copy(path = Vector.empty).check(Promote(pot)(eff.source)) - } - } + val errors = returning { + capException = summon[ReturnThrowable[Errors]] + state2.check(Promote(outer)(eff.source))(using state2) + } - if (errs1.nonEmpty || errs2.nonEmpty) - UnsafePromotion(pot, eff.source, state.path, errs1 ++ errs2).toErrors - else - Errors.empty + if (errors.isEmpty && !eagerlyUsed) Errors.empty + else + val errs = state.test { promote(pot, eff.source) } + if errs.nonEmpty then UnsafePromotion(pot, eff.source, state.path, errs).toErrors + else + state.safePromoted += pot + Errors.empty - case obj @ Global(tmref) => - if !state.isGlobalObject then Errors.empty - else - val errs = state.test { checkPromotion(obj, eff.source) } - if errs.nonEmpty then - UnsafePromotion(obj, eff.source, state.path, errs).toErrors - else - state.safePromoted += pot - Errors.empty - - case lhot @ LocalHot(cls) => - if !state.isGlobalObject then Errors.empty - else - val errs = state.test { checkPromotion(lhot, eff.source) } - if errs.nonEmpty then - UnsafePromotion(lhot, eff.source, state.path, errs).toErrors - else - state.safePromoted += pot - Errors.empty - - case pot => - val Summary(pots, effs) = expand(pot) - val effs2 = pots.map(Promote(_)(eff.source)) - (effs2 ++ effs).toList.flatMap(state.check(_)) + case Fun(pots, effs) => + val errs1 = state.test { + effs.toList.flatMap { state.check(_) } } - case FieldAccess(pot, field) => + val errs2 = state.test { + pots.toList.flatMap { pot => + state.copy(path = Vector.empty).check(Promote(pot)(eff.source)) + } + } - pot match { - case _: ThisRef => - val target = resolve(state.thisClass, field) - if (target.is(Flags.Lazy)) state.check(MethodCall(pot, target)(eff.source)) - else if (!state.fieldsInited.contains(target)) AccessNonInit(target, state.path).toErrors - else Errors.empty - - case SuperRef(_: ThisRef, supercls) => - val target = resolveSuper(state.thisClass, supercls, field) - if (target.is(Flags.Lazy)) state.check(MethodCall(pot, target)(eff.source)) - else if (!state.fieldsInited.contains(target)) AccessNonInit(target, state.path).toErrors - else Errors.empty - - case Warm(cls, outer) => - // all fields of warm values are initialized - val target = resolve(cls, field) - if (target.is(Flags.Lazy)) state.check(MethodCall(pot, target)(eff.source)) - else Errors.empty + if (errs1.nonEmpty || errs2.nonEmpty) + UnsafePromotion(pot, eff.source, state.path, errs1 ++ errs2).toErrors + else + Errors.empty - case obj @ Global(tmref) => - if !state.isGlobalObject || obj.moduleClass != state.thisClass then - Errors.empty - else - val target = resolve(state.thisClass, field) - if (target.is(Flags.Lazy)) state.check(MethodCall(pot, target)(eff.source)) - else if (!state.fieldsInited.contains(target)) AccessNonInit(target, state.path).toErrors - else Errors.empty - - case _: LocalHot => + case obj @ Global(tmref) => + if !state.isGlobalObject then Errors.empty + else + val errs = state.test { promote(obj, eff.source) } + if errs.nonEmpty then + UnsafePromotion(obj, eff.source, state.path, errs).toErrors + else + state.safePromoted += pot Errors.empty - case _: Cold => - AccessCold(field, eff.source, state.path).toErrors + case lhot @ LocalHot(cls) => + if !state.isGlobalObject then Errors.empty + else + val errs = state.test { promote(lhot, eff.source) } + if errs.nonEmpty then + UnsafePromotion(lhot, eff.source, state.path, errs).toErrors + else + state.safePromoted += pot + Errors.empty - case Fun(pots, effs) => - throw new Exception("Unexpected effect " + eff.show) + case pot => + val Summary(pots, effs) = expand(pot) + val effs2 = pots.map(Promote(_)(eff.source)) + (effs2 ++ effs).toList.flatMap(state.check(_)) + } - case pot => - val Summary(pots, effs) = expand(pot) - val effs2 = pots.map(FieldAccess(_, field)(eff.source)) - (effs2 ++ effs).toList.flatMap(state.check(_)) + private def checkFieldAccess(eff: FieldAccess)(using state: State): Errors = + val FieldAccess(pot, field) = eff + pot match { + case _: ThisRef => + val target = resolve(state.thisClass, field) + if (target.is(Flags.Lazy)) state.check(MethodCall(pot, target)(eff.source)) + else if (!state.fieldsInited.contains(target)) AccessNonInit(target, state.path).toErrors + else Errors.empty + + case SuperRef(_: ThisRef, supercls) => + val target = resolveSuper(state.thisClass, supercls, field) + if (target.is(Flags.Lazy)) state.check(MethodCall(pot, target)(eff.source)) + else if (!state.fieldsInited.contains(target)) AccessNonInit(target, state.path).toErrors + else Errors.empty + + case Warm(cls, outer) => + // all fields of warm values are initialized + val target = resolve(cls, field) + if (target.is(Flags.Lazy)) state.check(MethodCall(pot, target)(eff.source)) + else Errors.empty + + case obj @ Global(tmref) => + if !state.isGlobalObject || obj.moduleClass != state.thisClass then + Errors.empty + else + val target = resolve(state.thisClass, field) + if (target.is(Flags.Lazy)) state.check(MethodCall(pot, target)(eff.source)) + else if (!state.fieldsInited.contains(target)) AccessNonInit(target, state.path).toErrors + else Errors.empty - } + case _: LocalHot => + Errors.empty - case MethodCall(pot, sym) => - pot match { - case thisRef: ThisRef => - val target = resolve(state.thisClass, sym) - if (!target.isOneOf(Flags.Method | Flags.Lazy)) - state.check(FieldAccess(pot, target)(eff.source)) - else if (target.hasSource) { - val effs = thisRef.effectsOf(target).toList - effs.flatMap { state.check(_) } - } - else CallUnknown(target, eff.source, state.path).toErrors + case _: Cold => + AccessCold(field, eff.source, state.path).toErrors - case SuperRef(thisRef: ThisRef, supercls) => - val target = resolveSuper(state.thisClass, supercls, sym) - if (!target.is(Flags.Method)) - state.check(FieldAccess(pot, target)(eff.source)) - else if (target.hasSource) { - val effs = thisRef.effectsOf(target).toList - effs.flatMap { state.check(_) } - } - else CallUnknown(target, eff.source, state.path).toErrors + case Fun(pots, effs) => + throw new Exception("Unexpected effect " + eff.show) - case warm @ Warm(cls, outer) => - val target = resolve(cls, sym) + case pot => + val Summary(pots, effs) = expand(pot) + val effs2 = pots.map(FieldAccess(_, field)(eff.source)) + (effs2 ++ effs).toList.flatMap(state.check(_)) - if (target.hasSource) { - val effs = warm.effectsOf(target).toList - effs.flatMap { state.check(_) } - } - else if (!sym.isConstructor) - CallUnknown(target, eff.source, state.path).toErrors - else - Errors.empty - - case pot @ Global(tmref) => - if !state.isGlobalObject then - Errors.empty - else - val target = resolve(pot.moduleClass, sym) - if (!target.is(Flags.Method)) - state.check(FieldAccess(pot, target)(eff.source)) - else if (target.hasSource) { - val effs = pot.effectsOf(target).toList - effs.flatMap { state.check(_) } - } - else CallUnknown(target, eff.source, state.path).toErrors + } - case lhot: LocalHot => - if !state.isGlobalObject then - Errors.empty - else - val target = resolve(lhot.classSymbol, sym) - if (!target.is(Flags.Method)) - state.check(FieldAccess(pot, target)(eff.source)) - else if (target.hasSource) { - val effs = lhot.effectsOf(target).toList - effs.flatMap { state.check(_) } - } - else CallUnknown(target, eff.source, state.path).toErrors + private def checkMethodCall(eff: MethodCall)(using state: State): Errors = + val MethodCall(pot, sym) = eff + pot match { + case thisRef: ThisRef => + val target = resolve(state.thisClass, sym) + if (!target.isOneOf(Flags.Method | Flags.Lazy)) + state.check(FieldAccess(pot, target)(eff.source)) + else if (target.hasSource) { + val effs = thisRef.effectsOf(target).toList + effs.flatMap { state.check(_) } + } + else CallUnknown(target, eff.source, state.path).toErrors + + case SuperRef(thisRef: ThisRef, supercls) => + val target = resolveSuper(state.thisClass, supercls, sym) + if (!target.is(Flags.Method)) + state.check(FieldAccess(pot, target)(eff.source)) + else if (target.hasSource) { + val effs = thisRef.effectsOf(target).toList + effs.flatMap { state.check(_) } + } + else CallUnknown(target, eff.source, state.path).toErrors - case _: Cold => - CallCold(sym, eff.source, state.path).toErrors + case warm @ Warm(cls, outer) => + val target = resolve(cls, sym) - case Fun(pots, effs) => - // TODO: assertion might be false, due to SAM - if (sym.name.toString == "apply") effs.toList.flatMap { state.check(_) } - else Errors.empty - // curried, tupled, toString are harmless - - case pot => - val Summary(pots, effs) = expand(pot) - val effs2 = pots.map(MethodCall(_, sym)(eff.source)) - (effs2 ++ effs).toList.flatMap(state.check(_)) + if (target.hasSource) { + val effs = warm.effectsOf(target).toList + effs.flatMap { state.check(_) } } + else if (!sym.isConstructor) + CallUnknown(target, eff.source, state.path).toErrors + else + Errors.empty - case AccessGlobal(pot) => - if !state.isGlobalObject || pot.moduleClass != state.thisClass then + case pot @ Global(tmref) => + if !state.isGlobalObject then Errors.empty - else if state.superCallFinished then + else + val target = resolve(pot.moduleClass, sym) + if (!target.is(Flags.Method)) + state.check(FieldAccess(pot, target)(eff.source)) + else if (target.hasSource) { + val effs = pot.effectsOf(target).toList + effs.flatMap { state.check(_) } + } + else CallUnknown(target, eff.source, state.path).toErrors + + case lhot: LocalHot => + if !state.isGlobalObject then Errors.empty else - AccessNonInit(pot.tmref.symbol, state.path).toErrors + val target = resolve(lhot.classSymbol, sym) + if (!target.is(Flags.Method)) + state.check(FieldAccess(pot, target)(eff.source)) + else if (target.hasSource) { + val effs = lhot.effectsOf(target).toList + effs.flatMap { state.check(_) } + } + else CallUnknown(target, eff.source, state.path).toErrors + + case _: Cold => + CallCold(sym, eff.source, state.path).toErrors + + case Fun(pots, effs) => + // TODO: assertion might be false, due to SAM + if (sym.name.toString == "apply") effs.toList.flatMap { state.check(_) } + else Errors.empty + // curried, tupled, toString are harmless + case pot => + val Summary(pots, effs) = expand(pot) + val effs2 = pots.map(MethodCall(_, sym)(eff.source)) + (effs2 ++ effs).toList.flatMap(state.check(_)) } + private def checkAccessGlobal(eff: AccessGlobal)(using state: State): Errors = + val pot = eff.potential + if !state.isGlobalObject || pot.moduleClass != state.thisClass then + Errors.empty + else if state.superCallFinished then + Errors.empty + else + AccessNonInit(pot.tmref.symbol, state.path).toErrors private def expand(pot: Potential)(using state: State): Summary = trace("expand " + pot.show, init, _.asInstanceOf[Summary].show) { pot match { From 232a4fb77a45c73ae5e91d7a137e9810ee3bf6c9 Mon Sep 17 00:00:00 2001 From: Liu Fengyun Date: Sun, 29 Nov 2020 11:28:55 +0100 Subject: [PATCH 19/34] Fix range for promotion effects --- compiler/src/dotty/tools/dotc/transform/init/Potentials.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/compiler/src/dotty/tools/dotc/transform/init/Potentials.scala b/compiler/src/dotty/tools/dotc/transform/init/Potentials.scala index 188ea904d283..3113fe0ddc9d 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Potentials.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Potentials.scala @@ -183,7 +183,7 @@ object Potentials { // max potential length // TODO: it can be specified on a project basis via compiler options if (pot.size > 2) - summary + Promote(pot)(source) + summary + Promote(pot)(pot.source) else if (symbol.isConstructor) val res = summary + pot if selectEffect then res + MethodCall(pot, symbol)(source) From 0677fe546485973ec3b907396a0da2b3fad09e08 Mon Sep 17 00:00:00 2001 From: Liu Fengyun Date: Sun, 29 Nov 2020 13:42:05 +0100 Subject: [PATCH 20/34] Simplify promotion logic for warm --- .../tools/dotc/transform/init/Checking.scala | 29 ++++++++----------- 1 file changed, 12 insertions(+), 17 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/transform/init/Checking.scala b/compiler/src/dotty/tools/dotc/transform/init/Checking.scala index 971cb814fb59..067b7c089933 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Checking.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Checking.scala @@ -195,7 +195,7 @@ object Checking { } error.issue } - private def promote(pot: Refinable, source: Tree)(using state: State): Errors = + private def promote(pot: Refinable, source: Tree)(using state: State): Errors = trace("promoting " + pot.show, init, errs => Errors.show(errs.asInstanceOf[Errors])) { val buffer = new mutable.ArrayBuffer[Effect] val classRef = pot match @@ -234,6 +234,7 @@ object Checking { } buffer.toList.flatMap(eff => state.check(eff)) + } private def checkPromote(eff: Promote)(using state: State): Errors = val pot = eff.potential @@ -261,30 +262,24 @@ object Checking { // Use the assumption that `Promote(pot)` eagerly in the visited set is unsound. // It can only be safely used for checking the expanded effects. // tests/init/neg/hybrid2.scala - var eagerlyUsed: Boolean = false - var capException: ReturnThrowable[Errors] = null + var promoted: Boolean = false val checkFun = { (eff2: Effect, state2: State) => if eff == eff2 then - traceIndented("Eagerly using " + eff.show + ", will check thoroughly", init) - eagerlyUsed = true - throwReturn(Errors.empty)(using capException) + traceIndented("Eagerly using " + eff.show + ", check thoroughly", init) + promoted = true + promote(pot, eff.source) else state.check(eff2)(using state2) } - val state2 = state.copy( - checking = Set(eff), // important to be empty as eager usage might be hidden under a visited node - checkFun = checkFun - ) - - val errors = returning { - capException = summon[ReturnThrowable[Errors]] - state2.check(Promote(outer)(eff.source))(using state2) - } + val state2 = state.copy(checkFun = checkFun) + val errors = state2.check(Promote(outer)(eff.source))(using state2) - if (errors.isEmpty && !eagerlyUsed) Errors.empty + if (errors.isEmpty) + state.safePromoted += pot + Errors.empty else - val errs = state.test { promote(pot, eff.source) } + val errs = if promoted then errors else state.test { promote(pot, eff.source) } if errs.nonEmpty then UnsafePromotion(pot, eff.source, state.path, errs).toErrors else state.safePromoted += pot From 16c4977c8724a9416196c8131d378e7872edd0a0 Mon Sep 17 00:00:00 2001 From: Liu Fengyun Date: Sun, 29 Nov 2020 13:58:54 +0100 Subject: [PATCH 21/34] Refactor promotion of this --- .../tools/dotc/transform/init/Checking.scala | 15 +++++++-------- tests/init/neg/inner11.scala | 2 ++ 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/transform/init/Checking.scala b/compiler/src/dotty/tools/dotc/transform/init/Checking.scala index 067b7c089933..01ad81050bc6 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Checking.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Checking.scala @@ -241,17 +241,16 @@ object Checking { pot match { case pot: ThisRef => val classRef = state.thisClass.typeRef - val sum = classRef.fields.foldLeft(0) { (sum, denot) => - if denot.symbol.isOneOf(Flags.Lazy | Flags.Deferred) then sum - else sum + 1 + val allInit = classRef.fields.forall { denot => + val sym = denot.symbol + sym.isOneOf(Flags.Lazy | Flags.Deferred) || state.fieldsInited.contains(sym) } - if sum < state.fieldsInited.size then - throw new Exception(s"$sum fields in " + state.thisClass + ", but " + state.fieldsInited.size + " initialized") - else if sum > state.fieldsInited.size then - PromoteThis(pot, eff.source, state.path).toErrors - else + + if allInit then state.safePromoted += pot Errors.empty + else + PromoteThis(pot, eff.source, state.path).toErrors case _: Cold => PromoteCold(eff.source, state.path).toErrors diff --git a/tests/init/neg/inner11.scala b/tests/init/neg/inner11.scala index 7a96a29f8a25..fdd6c38df7c5 100644 --- a/tests/init/neg/inner11.scala +++ b/tests/init/neg/inner11.scala @@ -13,6 +13,7 @@ object NameKinds { type ThisInfo = Info val info: Info = new Info println(info.kind) // error + val count: Int = 10 } } @@ -31,5 +32,6 @@ object NameKinds2 { type ThisInfo = Info val info: Info = new Info println(info.kind) // ok + val count: Int = 10 } } \ No newline at end of file From 62f03f3695a324f558bfe6961cec1c34257615d0 Mon Sep 17 00:00:00 2001 From: Liu Fengyun Date: Sun, 29 Nov 2020 14:08:20 +0100 Subject: [PATCH 22/34] Fix tests - Fix crash tests/init/crash/i1990b.scala - tests/init/neg/inner1.scala Now we support safe promotion --- compiler/src/dotty/tools/dotc/transform/init/Env.scala | 10 ++++++---- .../tools/dotc/transform/init/Summarization.scala | 10 +++++++--- tests/init/neg/inner1.scala | 2 +- tests/init/neg/inner17.scala | 2 +- tests/init/neg/t3273.check | 4 ++-- 5 files changed, 17 insertions(+), 11 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/transform/init/Env.scala b/compiler/src/dotty/tools/dotc/transform/init/Env.scala index ca200c2564a1..fecbe4630ec5 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Env.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Env.scala @@ -25,10 +25,12 @@ case class Env(ctx: Context) { /** Can the method call be ignored? */ def canIgnoreMethod(symbol: Symbol): Boolean = - val owner = symbol.owner - owner == defn.AnyClass || - owner == defn.ObjectClass || - owner == defn.TupleClass + !symbol.exists || // possible with outer selection, tests/init/crash/i1990b.scala + canIgnoreClass(symbol.owner) + + def canIgnoreClass(cls: Symbol): Boolean = + cls == defn.AnyClass || + cls == defn.ObjectClass def withCtx(newCtx: Context): Env = this.copy(ctx = newCtx) diff --git a/compiler/src/dotty/tools/dotc/transform/init/Summarization.scala b/compiler/src/dotty/tools/dotc/transform/init/Summarization.scala index 60ad6c2339df..30b0eea1571c 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Summarization.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Summarization.scala @@ -235,8 +235,12 @@ object Summarization { case tmref: TermRef if env.checkGlobal && tmref.symbol.is(Flags.Module) && tmref.symbol.isStatic => val cls = tmref.symbol.moduleClass - val pot = Global(tmref)(source) - Summary(pot) + AccessGlobal(pot) + MethodCall(pot, cls.primaryConstructor)(source) + if cls.primaryConstructor.exists then + val pot = Global(tmref)(source) + Summary(pot) + AccessGlobal(pot) + MethodCall(pot, cls.primaryConstructor)(source) + else + // Integer$ + Summary.empty case tmref: TermRef => val Summary(pots, effs) = analyze(tmref.prefix, source) @@ -287,7 +291,7 @@ object Summarization { val enclosing = cur.owner.lexicallyEnclosingClass.asClass // Dotty uses O$.this outside of the object O if (enclosing.is(Flags.Package) && cls.is(Flags.Module)) - if env.checkGlobal then + if env.checkGlobal && cls.primaryConstructor.exists then // ctor may not exist, tests/init/neg/inner19 val pot = Global(cls.sourceModule.termRef)(source) Summary(pot) + AccessGlobal(pot) + MethodCall(pot, cls.primaryConstructor)(source) else diff --git a/tests/init/neg/inner1.scala b/tests/init/neg/inner1.scala index 6e8077a500b0..08a93da54efd 100644 --- a/tests/init/neg/inner1.scala +++ b/tests/init/neg/inner1.scala @@ -4,7 +4,7 @@ class Foo { val list = List(1, 2, 3) // error, as Inner access `this.list` val inner: Inner = new this.Inner // ok, `list` is instantiated - lib.escape(inner) // error + lib.escape(inner) val name = "good" diff --git a/tests/init/neg/inner17.scala b/tests/init/neg/inner17.scala index feb1c2b10229..441ed02767b3 100644 --- a/tests/init/neg/inner17.scala +++ b/tests/init/neg/inner17.scala @@ -5,7 +5,7 @@ class A { val a = f } - println(new B) // error + println(new B) // error } class C extends A { diff --git a/tests/init/neg/t3273.check b/tests/init/neg/t3273.check index 399e186e62cd..74c016ef521f 100644 --- a/tests/init/neg/t3273.check +++ b/tests/init/neg/t3273.check @@ -7,7 +7,7 @@ | | The unsafe promotion may cause the following problem(s): | - | 1. Access non-initialized field num1. Calling trace: + | 1. Access non-initialized value num1. Calling trace: | -> val num1: LazyList[Int] = 1 #:: num1.map(_ + 1) // error [ t3273.scala:4 ] -- Error: tests/init/neg/t3273.scala:5:61 ------------------------------------------------------------------------------ 5 | val num2: LazyList[Int] = 1 #:: num2.iterator.map(_ + 1).to(LazyList) // error @@ -18,5 +18,5 @@ | | The unsafe promotion may cause the following problem(s): | - | 1. Access non-initialized field num2. Calling trace: + | 1. Access non-initialized value num2. Calling trace: | -> val num2: LazyList[Int] = 1 #:: num2.iterator.map(_ + 1).to(LazyList) // error [ t3273.scala:5 ] From d90dce2bee38808d009f8be85f7b0fcd46777e4d Mon Sep 17 00:00:00 2001 From: Liu Fengyun Date: Sun, 29 Nov 2020 15:33:12 +0100 Subject: [PATCH 23/34] Fix non-termination The non-termination is caused by typo in the level of `Outer`: it should be `pot.level` instead of `pot.size`. --- .../tools/dotc/transform/init/Checking.scala | 2 +- .../tools/dotc/transform/init/Potentials.scala | 16 +++++++--------- tests/init/neg/inner-loop.scala | 2 +- 3 files changed, 9 insertions(+), 11 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/transform/init/Checking.scala b/compiler/src/dotty/tools/dotc/transform/init/Checking.scala index 01ad81050bc6..2a527a71f8d5 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Checking.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Checking.scala @@ -227,7 +227,7 @@ object Checking { val cls = denot.symbol.asClass val potInner = pot match - case warm: Warm => Warm(cls, pot)(source) + case warm: Warm => Potentials.asSeenFrom(Warm(cls, ThisRef()(source))(source), pot) case _ => LocalHot(cls)(source) buffer += MethodCall(potInner, cls.primaryConstructor)(source) buffer += Promote(potInner)(source) diff --git a/compiler/src/dotty/tools/dotc/transform/init/Potentials.scala b/compiler/src/dotty/tools/dotc/transform/init/Potentials.scala index 3113fe0ddc9d..971d3457b219 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Potentials.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Potentials.scala @@ -116,7 +116,7 @@ object Potentials { case class Outer(pot: Potential, classSymbol: ClassSymbol)(val source: Tree) extends Potential { // be lenient with size of outer selection, no worry for non-termination override def size: Int = pot.size - override def level: Int = pot.size + override def level: Int = pot.level def show(using Context): String = pot.show + ".outer[" + classSymbol.show + "]" } @@ -125,7 +125,7 @@ object Potentials { assert(field != NoSymbol) override def size: Int = potential.size + 1 - override def level: Int = potential.size + override def level: Int = potential.level def show(using Context): String = potential.show + "." + field.name.show } @@ -134,7 +134,7 @@ object Potentials { assert(method != NoSymbol) override def size: Int = potential.size + 1 - override def level: Int = potential.size + override def level: Int = potential.level def show(using Context): String = potential.show + "." + method.name.show } @@ -224,13 +224,11 @@ object Potentials { case Warm(cls, outer2) => // widening to terminate - val thisValue2 = thisValue match { - case Warm(cls, outer) if outer.level > 2 => - Warm(cls, Cold()(outer2.source))(thisValue.source) - - case _ => + val thisValue2 = + if thisValue.level + outer2.level > 4 then + Cold()(outer2.source) + else thisValue - } val outer3 = asSeenFrom(outer2, thisValue2) Warm(cls, outer3)(pot.source) diff --git a/tests/init/neg/inner-loop.scala b/tests/init/neg/inner-loop.scala index a7ff5c153d32..c56f31a96757 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 From 2334a0d425df2b5fe0ff2b0dae494cf2859edb64 Mon Sep 17 00:00:00 2001 From: Liu Fengyun Date: Sun, 29 Nov 2020 16:53:35 +0100 Subject: [PATCH 24/34] Revert #9673 Handle NoPrefix properly --- .../dotty/tools/dotc/transform/init/Summarization.scala | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/transform/init/Summarization.scala b/compiler/src/dotty/tools/dotc/transform/init/Summarization.scala index 30b0eea1571c..c42a51afc12c 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Summarization.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Summarization.scala @@ -338,8 +338,11 @@ object Summarization { if (cls == defn.AnyClass || cls == defn.AnyValClass) Effects.empty else { val ctor = cls.primaryConstructor - Summarization.analyze(New(ref.tpe))(env.withOwner(ctor.owner))._2 + - MethodCall(ThisRef()(ref), ctor)(ref) + val prefixEff = + if tref.prefix == NoPrefix then Effects.empty + else Summarization.analyze(New(ref.tpe))(env.withOwner(ctor.owner)).effs + + prefixEff + MethodCall(ThisRef()(ref), ctor)(ref) } }) } From ef30873444a0fe0b9047075551eae38dd1818006 Mon Sep 17 00:00:00 2001 From: Liu Fengyun Date: Sun, 29 Nov 2020 16:56:11 +0100 Subject: [PATCH 25/34] Add promotion tests --- tests/init/neg/promote.scala | 9 +++++++++ tests/init/pos/promote.scala | 7 +++++++ 2 files changed, 16 insertions(+) create mode 100644 tests/init/neg/promote.scala create mode 100644 tests/init/pos/promote.scala diff --git a/tests/init/neg/promote.scala b/tests/init/neg/promote.scala new file mode 100644 index 000000000000..70e951d78f6f --- /dev/null +++ b/tests/init/neg/promote.scala @@ -0,0 +1,9 @@ +class Wrap { + def qux[T](e: E[T]) = e.foo + + abstract class E[+T] { def foo: T } + object E { + final val A: E[Nothing] = new E { def foo = ref } + val ref = qux(A) // error + } +} \ No newline at end of file diff --git a/tests/init/pos/promote.scala b/tests/init/pos/promote.scala new file mode 100644 index 000000000000..546e4ca4e65d --- /dev/null +++ b/tests/init/pos/promote.scala @@ -0,0 +1,7 @@ +class Wrap { + class E + object E { + final val A = new E {} + val $values = Array(A) + } +} \ No newline at end of file From d425f9dc7e59fdd7c21176b146ee30dbca5abea2 Mon Sep 17 00:00:00 2001 From: Liu Fengyun Date: Sun, 29 Nov 2020 17:17:00 +0100 Subject: [PATCH 26/34] Fix missing return --- .../src/dotty/tools/dotc/transform/init/Summarization.scala | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/transform/init/Summarization.scala b/compiler/src/dotty/tools/dotc/transform/init/Summarization.scala index c42a51afc12c..9be3e9839680 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Summarization.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Summarization.scala @@ -233,7 +233,7 @@ object Summarization { Summary.empty case tmref: TermRef - if env.checkGlobal && tmref.symbol.is(Flags.Module) && tmref.symbol.isStatic => + if env.checkGlobal && tmref.symbol.is(Flags.Module, butNot = Flags.Package) && tmref.symbol.isStatic => val cls = tmref.symbol.moduleClass if cls.primaryConstructor.exists then val pot = Global(tmref)(source) @@ -293,7 +293,7 @@ object Summarization { if (enclosing.is(Flags.Package) && cls.is(Flags.Module)) if env.checkGlobal && cls.primaryConstructor.exists then // ctor may not exist, tests/init/neg/inner19 val pot = Global(cls.sourceModule.termRef)(source) - Summary(pot) + AccessGlobal(pot) + MethodCall(pot, cls.primaryConstructor)(source) + return Summary(pot) + AccessGlobal(pot) + MethodCall(pot, cls.primaryConstructor)(source) else return Summary.empty From ca2ade2e38564ab0301fea36b8bce6b542d1a04e Mon Sep 17 00:00:00 2001 From: Liu Fengyun Date: Sun, 29 Nov 2020 18:49:31 +0100 Subject: [PATCH 27/34] Ignore constructor call on Any, Object and AnyVal --- compiler/src/dotty/tools/dotc/transform/init/Env.scala | 1 + compiler/src/dotty/tools/dotc/transform/init/Errors.scala | 2 +- .../dotty/tools/dotc/transform/init/Summarization.scala | 8 +++++--- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/transform/init/Env.scala b/compiler/src/dotty/tools/dotc/transform/init/Env.scala index fecbe4630ec5..654c677d5447 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Env.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Env.scala @@ -30,6 +30,7 @@ case class Env(ctx: Context) { def canIgnoreClass(cls: Symbol): Boolean = cls == defn.AnyClass || + cls == defn.AnyValClass || cls == defn.ObjectClass def withCtx(newCtx: Context): Env = this.copy(ctx = newCtx) diff --git a/compiler/src/dotty/tools/dotc/transform/init/Errors.scala b/compiler/src/dotty/tools/dotc/transform/init/Errors.scala index 4c9a220b8c00..73b8cd123033 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Errors.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Errors.scala @@ -63,7 +63,7 @@ object Errors { case class AccessNonInit(field: Symbol, trace: Vector[Tree]) extends Error { def source: Tree = trace.last def show(using Context): String = - "Access non-initialized field " + field.name.show + "." + "Access non-initialized " + field.show + "." override def issue(using Context): Unit = report.warning(show + stacktrace, field.srcPos) diff --git a/compiler/src/dotty/tools/dotc/transform/init/Summarization.scala b/compiler/src/dotty/tools/dotc/transform/init/Summarization.scala index 9be3e9839680..4b5c8dda1722 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Summarization.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Summarization.scala @@ -312,8 +312,10 @@ object Summarization { val effs = analyze(Block(tpl.body, unitLiteral))._2 def parentArgEffsWithInit(stats: List[Tree], ctor: Symbol, source: Tree): Effects = - val initCall = MethodCall(ThisRef()(source), ctor)(source) - stats.foldLeft(Set(initCall)) { (acc, stat) => + val init = + if env.canIgnoreMethod(ctor) then Effects.empty + else Effects.empty + MethodCall(ThisRef()(source), ctor)(source) + stats.foldLeft(init) { (acc, stat) => val summary = Summarization.analyze(stat) acc ++ summary.effs } @@ -335,7 +337,7 @@ object Summarization { case ref => val tref: TypeRef = ref.tpe.typeConstructor.asInstanceOf val cls = tref.classSymbol.asClass - if (cls == defn.AnyClass || cls == defn.AnyValClass) Effects.empty + if env.canIgnoreClass(cls) then Effects.empty else { val ctor = cls.primaryConstructor val prefixEff = From 990079c78cb83730f0475b221893cc9a62d854a2 Mon Sep 17 00:00:00 2001 From: Liu Fengyun Date: Sun, 29 Nov 2020 19:04:31 +0100 Subject: [PATCH 28/34] Ignore external global method calls Those calls cannot have impact on initialization status of current object. --- compiler/src/dotty/tools/dotc/transform/init/Checking.scala | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/transform/init/Checking.scala b/compiler/src/dotty/tools/dotc/transform/init/Checking.scala index 2a527a71f8d5..e88d0c99fec9 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Checking.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Checking.scala @@ -418,7 +418,7 @@ object Checking { val effs = pot.effectsOf(target).toList effs.flatMap { state.check(_) } } - else CallUnknown(target, eff.source, state.path).toErrors + else Errors.empty case lhot: LocalHot => if !state.isGlobalObject then @@ -431,7 +431,7 @@ object Checking { val effs = lhot.effectsOf(target).toList effs.flatMap { state.check(_) } } - else CallUnknown(target, eff.source, state.path).toErrors + else Errors.empty case _: Cold => CallCold(sym, eff.source, state.path).toErrors From b09798095526f278afa8bfb446c2b6eaaec5afde Mon Sep 17 00:00:00 2001 From: Liu Fengyun Date: Sun, 29 Nov 2020 19:41:09 +0100 Subject: [PATCH 29/34] Member promotion result override outer promotion result --- .../tools/dotc/transform/init/Checking.scala | 35 ++++++++++++------- tests/init/neg/inner19.scala | 2 +- 2 files changed, 24 insertions(+), 13 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/transform/init/Checking.scala b/compiler/src/dotty/tools/dotc/transform/init/Checking.scala index e88d0c99fec9..d14b76258eef 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Checking.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Checking.scala @@ -204,11 +204,19 @@ object Checking { case warm: Warm => warm.classSymbol.typeRef case _: ThisRef => ??? // impossible + // Fast check of public methods directly defined in class + // If errors found, there is no need to go further + classRef.decls.foreach { m => + if m.is(Flags.Method, butNot = Flags.Private) && m.hasSource then + val errors = state.check(MethodCall(pot, m)(source)) + if errors.nonEmpty then return errors + } + val excludedFlags = Flags.Deferred | Flags.Private | Flags.Protected classRef.fields.foreach { denot => val f = denot.symbol - if !f.isOneOf(excludedFlags) then + if !f.isOneOf(excludedFlags) && f.hasSource then buffer += Promote(FieldReturn(pot, f)(source))(source) buffer += FieldAccess(pot, f)(source) } @@ -218,19 +226,20 @@ object Checking { classRef.membersBasedOnFlags(Flags.Method, Flags.Deferred).foreach { denot => val m = denot.symbol - if !m.isConstructor && !theEnv.canIgnoreMethod(m) then + if !m.isConstructor && m.hasSource && !theEnv.canIgnoreMethod(m) then buffer += MethodCall(pot, m)(source) buffer += Promote(MethodReturn(pot, m)(source))(source) } classRef.memberClasses.foreach { denot => val cls = denot.symbol.asClass - val potInner = - pot match - case warm: Warm => Potentials.asSeenFrom(Warm(cls, ThisRef()(source))(source), pot) - case _ => LocalHot(cls)(source) - buffer += MethodCall(potInner, cls.primaryConstructor)(source) - buffer += Promote(potInner)(source) + if cls.hasSource then + val potInner = + pot match + case warm: Warm => Potentials.asSeenFrom(Warm(cls, ThisRef()(source))(source), pot) + case _ => LocalHot(cls)(source) + buffer += MethodCall(potInner, cls.primaryConstructor)(source) + buffer += Promote(potInner)(source) } buffer.toList.flatMap(eff => state.check(eff)) @@ -261,12 +270,12 @@ object Checking { // Use the assumption that `Promote(pot)` eagerly in the visited set is unsound. // It can only be safely used for checking the expanded effects. // tests/init/neg/hybrid2.scala - var promoted: Boolean = false + var promotedErrors: Errors = null val checkFun = { (eff2: Effect, state2: State) => if eff == eff2 then traceIndented("Eagerly using " + eff.show + ", check thoroughly", init) - promoted = true - promote(pot, eff.source) + promotedErrors = promote(pot, eff.source) + promotedErrors else state.check(eff2)(using state2) } @@ -278,7 +287,9 @@ object Checking { state.safePromoted += pot Errors.empty else - val errs = if promoted then errors else state.test { promote(pot, eff.source) } + val errs = + if promotedErrors != null then promotedErrors + else state.test { promote(pot, eff.source) } if errs.nonEmpty then UnsafePromotion(pot, eff.source, state.path, errs).toErrors else state.safePromoted += pot diff --git a/tests/init/neg/inner19.scala b/tests/init/neg/inner19.scala index 346ca175c802..f9c0a29a1649 100644 --- a/tests/init/neg/inner19.scala +++ b/tests/init/neg/inner19.scala @@ -14,6 +14,6 @@ class A { class B extends A { println((new O.B).f) - O.C(4) // error: leak due to potential length limit + O.C(4) override val n = 50 // error } \ No newline at end of file From fb88ebf0f348b759f6cb109926da650564708530 Mon Sep 17 00:00:00 2001 From: Liu Fengyun Date: Sun, 29 Nov 2020 21:20:54 +0100 Subject: [PATCH 30/34] Refactor --- .../src/dotty/tools/dotc/transform/init/Summarization.scala | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/transform/init/Summarization.scala b/compiler/src/dotty/tools/dotc/transform/init/Summarization.scala index 4b5c8dda1722..2e74933ef5a4 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Summarization.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Summarization.scala @@ -131,7 +131,7 @@ object Summarization { Summary(Fun(pots, effs)(expr)) case Block(stats, expr) => - val effs = stats.foldLeft(Effects.empty) { (acc, stat) => acc ++ analyze(stat)._2 } + val effs = stats.foldLeft(Effects.empty) { (acc, stat) => acc ++ analyze(stat).effs } val Summary(pots2, effs2) = analyze(expr) Summary(pots2, effs ++ effs2) @@ -186,7 +186,7 @@ object Summarization { case Inlined(call, bindings, expansion) => val effs = bindings.foldLeft(Effects.empty) { (acc, mdef) => - acc ++ analyze(mdef)._2 + acc ++ analyze(mdef).effs } analyze(expansion) ++ effs @@ -309,7 +309,7 @@ object Summarization { if (ctor.isPrimaryConstructor) { val cls = ctor.owner.asClass val tpl = ctor.owner.defTree.asInstanceOf[TypeDef].rhs.asInstanceOf[Template] - val effs = analyze(Block(tpl.body, unitLiteral))._2 + val effs = analyze(Block(tpl.body, unitLiteral)).effs def parentArgEffsWithInit(stats: List[Tree], ctor: Symbol, source: Tree): Effects = val init = From 299e048ed555bc2dedab5a3004f8abbb66f69ca5 Mon Sep 17 00:00:00 2001 From: Liu Fengyun Date: Sun, 29 Nov 2020 23:15:37 +0100 Subject: [PATCH 31/34] Introduce fast and full check modes --- .../tools/dotc/config/ScalaSettings.scala | 2 +- .../src/dotty/tools/dotc/core/Symbols.scala | 2 +- .../tools/dotc/transform/init/Checker.scala | 3 +- .../tools/dotc/transform/init/Checking.scala | 41 +++++++++---------- .../dotty/tools/dotc/transform/init/Env.scala | 2 +- .../dotc/transform/init/Summarization.scala | 10 ++--- .../dotty/tools/dotc/CompilationTests.scala | 31 +++++++------- tests/init/neg/inner17.scala | 2 +- 8 files changed, 45 insertions(+), 48 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/config/ScalaSettings.scala b/compiler/src/dotty/tools/dotc/config/ScalaSettings.scala index fbce5ad1f02b..d2787928ad25 100644 --- a/compiler/src/dotty/tools/dotc/config/ScalaSettings.scala +++ b/compiler/src/dotty/tools/dotc/config/ScalaSettings.scala @@ -173,7 +173,7 @@ class ScalaSettings extends Settings.SettingGroup { val YnoKindPolymorphism: Setting[Boolean] = BooleanSetting("-Yno-kind-polymorphism", "Disable kind polymorphism.") val YexplicitNulls: Setting[Boolean] = BooleanSetting("-Yexplicit-nulls", "Make reference types non-nullable. Nullable types can be expressed with unions: e.g. String|Null.") val YerasedTerms: Setting[Boolean] = BooleanSetting("-Yerased-terms", "Allows the use of erased terms.") - val YcheckInit: Setting[Boolean] = BooleanSetting("-Ycheck-init", "Check initialization of objects") + val YcheckInit: Setting[String] = ChoiceSetting("-Ycheck-init", "mode", "Check initialization of objects: fast | full", List("fast", "full"), "") val YrequireTargetName: Setting[Boolean] = BooleanSetting("-Yrequire-targetName", "Warn if an operator is defined without a @targetName annotation") /** Area-specific debug output */ diff --git a/compiler/src/dotty/tools/dotc/core/Symbols.scala b/compiler/src/dotty/tools/dotc/core/Symbols.scala index b6325ae645d7..9f11d12abde5 100644 --- a/compiler/src/dotty/tools/dotc/core/Symbols.scala +++ b/compiler/src/dotty/tools/dotc/core/Symbols.scala @@ -83,7 +83,7 @@ object Symbols { ctx.settings.YretainTrees.value || denot.owner.isTerm || // no risk of leaking memory after a run for these denot.isOneOf(InlineOrProxy) || // need to keep inline info - ctx.settings.YcheckInit.value // initialization check + ctx.settings.YcheckInit.value.nonEmpty // initialization check /** The last denotation of this symbol */ private var lastDenot: SymDenotation = _ diff --git a/compiler/src/dotty/tools/dotc/transform/init/Checker.scala b/compiler/src/dotty/tools/dotc/transform/init/Checker.scala index 6cf71ff4ee90..cb1abda7a908 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Checker.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Checker.scala @@ -28,7 +28,7 @@ class Checker extends MiniPhase { override val runsAfter = Set(Pickler.name) override def isEnabled(using Context): Boolean = - super.isEnabled && ctx.settings.YcheckInit.value + super.isEnabled && ctx.settings.YcheckInit.value.nonEmpty override def transformTypeDef(tree: TypeDef)(using Context): tpd.Tree = { if (!tree.isClassDef) return tree @@ -47,7 +47,6 @@ class Checker extends MiniPhase { // A concrete class may not be instantiated if the self type is not satisfied if (instantiable) { implicit val state: Checking.State = Checking.State( - checking = Set.empty, checked = Set.empty, path = Vector.empty, thisClass = cls, diff --git a/compiler/src/dotty/tools/dotc/transform/init/Checking.scala b/compiler/src/dotty/tools/dotc/transform/init/Checking.scala index d14b76258eef..e52a283a7702 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Checking.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Checking.scala @@ -31,8 +31,7 @@ object Checking { */ case class State( - var checking: Set[Effect], // effects that are being checked - var checked: Set[Effect], // effects that have been checked + var checked: Set[Effect], // effects that have been checked or are being checked path: Vector[Tree], // the path that leads to the current effect thisClass: ClassSymbol, // the concrete class of `this` fieldsInited: mutable.Set[Symbol], @@ -51,10 +50,8 @@ object Checking { def test(op: State ?=> Errors): Errors = { val savedChecked = checked - val savedChecking = checking val errors = op(using this) checked = savedChecked - checking = savedChecking errors } } @@ -66,24 +63,19 @@ object Checking { private val defaultCheck: (Effect, State) => Errors = { (eff: Effect, state: State) => given State = state trace("checking effect " + eff.show, init, errs => Errors.show(errs.asInstanceOf[Errors])) { - if (state.checked.contains(eff) || state.checking.contains(eff)) { + if (state.checked.contains(eff)) { traceIndented("Already checked " + eff.show, init) Errors.empty } else { - state.checking = state.checking + eff + state.checked = state.checked + eff val state2: State = state.copy(path = state.path :+ eff.source) - val errors = - eff match { - case eff: Promote => Checking.checkPromote(eff)(using state2) - case eff: FieldAccess => Checking.checkFieldAccess(eff)(using state2) - case eff: MethodCall => Checking.checkMethodCall(eff)(using state2) - case eff: AccessGlobal => Checking.checkAccessGlobal(eff)(using state2) - } - - state.checking -= eff - state.checked += eff - errors + eff match { + case eff: Promote => Checking.checkPromote(eff)(using state2) + case eff: FieldAccess => Checking.checkFieldAccess(eff)(using state2) + case eff: MethodCall => Checking.checkMethodCall(eff)(using state2) + case eff: AccessGlobal => Checking.checkAccessGlobal(eff)(using state2) + } } } } @@ -204,14 +196,21 @@ object Checking { case warm: Warm => warm.classSymbol.typeRef case _: ThisRef => ??? // impossible - // Fast check of public methods directly defined in class + // Fast check of public term members directly defined in class // If errors found, there is no need to go further - classRef.decls.foreach { m => - if m.is(Flags.Method, butNot = Flags.Private) && m.hasSource then - val errors = state.check(MethodCall(pot, m)(source)) + classRef.decls.foreach { sym => + if sym.is(Flags.Method, butNot = Flags.Private) && !sym.isConstructor && sym.hasSource then + val errors = state.check(MethodCall(pot, sym)(source)) if errors.nonEmpty then return errors + + // if sym.name.isTermName && !sym.isOneOf(Flags.Method | Flags.Private) && sym.hasSource then + // val errors = state.check(Promote(FieldReturn(pot, sym)(source))(source)) + // if errors.nonEmpty then return errors } + // best-effort in fast mode + // if !theEnv.isFullMode then return Errors.empty + val excludedFlags = Flags.Deferred | Flags.Private | Flags.Protected classRef.fields.foreach { denot => diff --git a/compiler/src/dotty/tools/dotc/transform/init/Env.scala b/compiler/src/dotty/tools/dotc/transform/init/Env.scala index 654c677d5447..4637fd0bffb4 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Env.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Env.scala @@ -37,7 +37,7 @@ case class Env(ctx: Context) { def withOwner(owner: Symbol) = this.copy(ctx = this.ctx.withOwner(owner)) - def checkGlobal: Boolean = false + def isFullMode: Boolean = ctx.settings.YcheckInit.value == "full" /** Whether values of a given type is always fully initialized? * diff --git a/compiler/src/dotty/tools/dotc/transform/init/Summarization.scala b/compiler/src/dotty/tools/dotc/transform/init/Summarization.scala index 2e74933ef5a4..d6e95c5a0bc4 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Summarization.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Summarization.scala @@ -92,7 +92,7 @@ object Summarization { val Summary(pots, effs) = resolveThis(enclosing, thisRef, cur, expr) val summary = Summary(effs) if pots.isEmpty then - if env.checkGlobal then summary + LocalHot(cls)(expr) + if env.isFullMode then summary + LocalHot(cls)(expr) else summary else { assert(pots.size == 1) @@ -102,13 +102,13 @@ object Summarization { else { val summary = analyze(tref.prefix, expr) if summary.pots.isEmpty then - if env.checkGlobal then summary + LocalHot(cls)(expr) + if env.isFullMode then summary + LocalHot(cls)(expr) else summary else { assert(summary.pots.size == 1) val pot = summary.pots.head val res = summary.dropPotentials - if env.checkGlobal && Potentials.isGlobalPath(pot) then + if env.isFullMode && Potentials.isGlobalPath(pot) then res + LocalHot(cls)(expr) else res + Warm(cls, pot)(expr) @@ -233,7 +233,7 @@ object Summarization { Summary.empty case tmref: TermRef - if env.checkGlobal && tmref.symbol.is(Flags.Module, butNot = Flags.Package) && tmref.symbol.isStatic => + if env.isFullMode && tmref.symbol.is(Flags.Module, butNot = Flags.Package) && tmref.symbol.isStatic => val cls = tmref.symbol.moduleClass if cls.primaryConstructor.exists then val pot = Global(tmref)(source) @@ -291,7 +291,7 @@ object Summarization { val enclosing = cur.owner.lexicallyEnclosingClass.asClass // Dotty uses O$.this outside of the object O if (enclosing.is(Flags.Package) && cls.is(Flags.Module)) - if env.checkGlobal && cls.primaryConstructor.exists then // ctor may not exist, tests/init/neg/inner19 + if env.isFullMode && cls.primaryConstructor.exists then // ctor may not exist, tests/init/neg/inner19 val pot = Global(cls.sourceModule.termRef)(source) return Summary(pot) + AccessGlobal(pot) + MethodCall(pot, cls.primaryConstructor)(source) else diff --git a/compiler/test/dotty/tools/dotc/CompilationTests.scala b/compiler/test/dotty/tools/dotc/CompilationTests.scala index 2515b5865b35..cb8a2347b323 100644 --- a/compiler/test/dotty/tools/dotc/CompilationTests.scala +++ b/compiler/test/dotty/tools/dotc/CompilationTests.scala @@ -306,24 +306,23 @@ class CompilationTests { }.checkRuns() // initialization tests - @Test def checkInitNeg: Unit = { - implicit val testGroup: TestGroup = TestGroup("checkInit") - val options = defaultOptions.and("-Ycheck-init", "-Xfatal-warnings") - compileFilesInDir("tests/init/neg/", options) - }.checkExpectedErrors() - - @Test def checkInitCrash: Unit = { - implicit val testGroup: TestGroup = TestGroup("checkInit") - val options = defaultOptions.and("-Ycheck-init") - compileFilesInDir("tests/init/crash", options) - }.checkCompile() + @Test def checkInitFast: Unit = { + implicit val testGroup: TestGroup = TestGroup("checkInitFast") + val options = defaultOptions.and("-Ycheck-init", "fast", "-Xfatal-warnings") + compileFilesInDir("tests/init/neg", options).checkExpectedErrors() + compileFilesInDir("tests/init/full/neg", options).checkCompile() + compileFilesInDir("tests/init/pos", options).checkCompile() + compileFilesInDir("tests/init/crash", options.without("-Xfatal-warnings")).checkCompile() + } - @Test def checkInitPos: Unit = { + @Test def checkInitFull: Unit = { implicit val testGroup: TestGroup = TestGroup("checkInit") - val options = defaultOptions.and("-Ycheck-init", "-Xfatal-warnings") - compileFilesInDir("tests/init/pos", options) - }.checkCompile() - + val options = defaultOptions.and("-Ycheck-init", "full", "-Xfatal-warnings") + compileFilesInDir("tests/init/neg", options).checkExpectedErrors() + compileFilesInDir("tests/init/full/neg", options).checkExpectedErrors() + compileFilesInDir("tests/init/pos", options).checkCompile() + compileFilesInDir("tests/init/crash", options.without("-Xfatal-warnings")).checkCompile() + } } object CompilationTests extends ParallelTesting { diff --git a/tests/init/neg/inner17.scala b/tests/init/neg/inner17.scala index 441ed02767b3..fb6c2c70b1ee 100644 --- a/tests/init/neg/inner17.scala +++ b/tests/init/neg/inner17.scala @@ -5,7 +5,7 @@ class A { val a = f } - println(new B) // error + println(new B) // leak is OK, accessing `f` is reported below } class C extends A { From be53c7910a0ccfad38664b9d63f70357f6867643 Mon Sep 17 00:00:00 2001 From: Liu Fengyun Date: Sun, 29 Nov 2020 23:39:23 +0100 Subject: [PATCH 32/34] Fix #9176: Handle parent call argument effects in full mode --- .../tools/dotc/transform/init/Checking.scala | 28 +++++++++++-------- .../dotty/tools/dotc/CompilationTests.scala | 1 - tests/init/full/neg/global-cycle1.scala | 10 +++++++ tests/init/{ => full}/neg/hybrid2.scala | 0 tests/init/full/neg/i9176.scala | 9 ++++++ 5 files changed, 36 insertions(+), 12 deletions(-) create mode 100644 tests/init/full/neg/global-cycle1.scala rename tests/init/{ => full}/neg/hybrid2.scala (100%) create mode 100644 tests/init/full/neg/i9176.scala diff --git a/compiler/src/dotty/tools/dotc/transform/init/Checking.scala b/compiler/src/dotty/tools/dotc/transform/init/Checking.scala index e52a283a7702..d5287d6612f4 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Checking.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Checking.scala @@ -141,11 +141,14 @@ object Checking { checkSecondaryConstructor(ctor) } - (stats :+ expr).foreach { stat => - val summary = Summarization.analyze(stat)(theEnv.withOwner(ctor)) + checkStats(stats :+ expr, ctor) + } + + def checkStats(stats: List[Tree], owner: Symbol)(using state: State): Unit = + stats.foreach { stat => + val summary = Summarization.analyze(stat)(theEnv.withOwner(owner)) checkEffects(summary.effs) } - } cls.paramAccessors.foreach { acc => if (!acc.is(Flags.Method)) { @@ -155,16 +158,19 @@ object Checking { } tpl.parents.foreach { - case tree @ Block(_, parent) => - val (ctor, _, _) = decomposeCall(parent) + case tree @ Block(stats, parent) => + val (ctor, _, argss) = decomposeCall(parent) + if theEnv.isFullMode then checkStats((stats :: argss).flatten, cls) checkConstructor(ctor.symbol, parent.tpe, tree) - case tree @ Apply(Block(_, parent), _) => - val (ctor, _, _) = decomposeCall(parent) + case tree @ Apply(Block(stats, parent), args) => + val (ctor, _, argss) = decomposeCall(parent) + if theEnv.isFullMode then checkStats((stats :: argss).flatten, cls) checkConstructor(ctor.symbol, tree.tpe, tree) case parent : Apply => val (ctor, _, argss) = decomposeCall(parent) + if theEnv.isFullMode then checkStats(argss.flatten, cls) checkConstructor(ctor.symbol, parent.tpe, parent) case ref => @@ -203,13 +209,13 @@ object Checking { val errors = state.check(MethodCall(pot, sym)(source)) if errors.nonEmpty then return errors - // if sym.name.isTermName && !sym.isOneOf(Flags.Method | Flags.Private) && sym.hasSource then - // val errors = state.check(Promote(FieldReturn(pot, sym)(source))(source)) - // if errors.nonEmpty then return errors + if sym.name.isTermName && !sym.isOneOf(Flags.Method | Flags.Private) && sym.hasSource then + val errors = state.check(Promote(FieldReturn(pot, sym)(source))(source)) + if errors.nonEmpty then return errors } // best-effort in fast mode - // if !theEnv.isFullMode then return Errors.empty + if !theEnv.isFullMode then return Errors.empty val excludedFlags = Flags.Deferred | Flags.Private | Flags.Protected diff --git a/compiler/test/dotty/tools/dotc/CompilationTests.scala b/compiler/test/dotty/tools/dotc/CompilationTests.scala index cb8a2347b323..9af6e664bf19 100644 --- a/compiler/test/dotty/tools/dotc/CompilationTests.scala +++ b/compiler/test/dotty/tools/dotc/CompilationTests.scala @@ -321,7 +321,6 @@ class CompilationTests { compileFilesInDir("tests/init/neg", options).checkExpectedErrors() compileFilesInDir("tests/init/full/neg", options).checkExpectedErrors() compileFilesInDir("tests/init/pos", options).checkCompile() - compileFilesInDir("tests/init/crash", options.without("-Xfatal-warnings")).checkCompile() } } diff --git a/tests/init/full/neg/global-cycle1.scala b/tests/init/full/neg/global-cycle1.scala new file mode 100644 index 000000000000..ebd667c51ba0 --- /dev/null +++ b/tests/init/full/neg/global-cycle1.scala @@ -0,0 +1,10 @@ +object A { + val a: Int = B.b // error +} + +object B { + val b: Int = A.a // error +} + +@main +def Test = print(A.a) \ No newline at end of file diff --git a/tests/init/neg/hybrid2.scala b/tests/init/full/neg/hybrid2.scala similarity index 100% rename from tests/init/neg/hybrid2.scala rename to tests/init/full/neg/hybrid2.scala diff --git a/tests/init/full/neg/i9176.scala b/tests/init/full/neg/i9176.scala new file mode 100644 index 000000000000..abb8a6394dd2 --- /dev/null +++ b/tests/init/full/neg/i9176.scala @@ -0,0 +1,9 @@ +class Foo(val opposite: Foo) +case object A extends Foo(B) // error +case object B extends Foo(A) // error +object Test { + def main(args: Array[String]): Unit = { + println(A.opposite) + println(B.opposite) + } +} \ No newline at end of file From cf468aa47e191ba0a969c5c30e355db416731856 Mon Sep 17 00:00:00 2001 From: Liu Fengyun Date: Sun, 29 Nov 2020 23:55:57 +0100 Subject: [PATCH 33/34] Add test cases --- tests/init/full/neg/global-cycle2.scala | 7 +++++++ tests/init/full/neg/global-cycle3.scala | 7 +++++++ tests/init/full/neg/global-cycle4.scala | 19 +++++++++++++++++++ 3 files changed, 33 insertions(+) create mode 100644 tests/init/full/neg/global-cycle2.scala create mode 100644 tests/init/full/neg/global-cycle3.scala create mode 100644 tests/init/full/neg/global-cycle4.scala diff --git a/tests/init/full/neg/global-cycle2.scala b/tests/init/full/neg/global-cycle2.scala new file mode 100644 index 000000000000..30792e58af6b --- /dev/null +++ b/tests/init/full/neg/global-cycle2.scala @@ -0,0 +1,7 @@ +object A { + val a: Int = B.foo() // error +} + +object B { + def foo(): Int = A.a * 2 +} diff --git a/tests/init/full/neg/global-cycle3.scala b/tests/init/full/neg/global-cycle3.scala new file mode 100644 index 000000000000..7fae20dbe894 --- /dev/null +++ b/tests/init/full/neg/global-cycle3.scala @@ -0,0 +1,7 @@ +class A(x: Int) { + def foo(): Int = B.a + 10 +} + +object B { + val a: Int = A(4).foo() // error +} diff --git a/tests/init/full/neg/global-cycle4.scala b/tests/init/full/neg/global-cycle4.scala new file mode 100644 index 000000000000..3de0533cb521 --- /dev/null +++ b/tests/init/full/neg/global-cycle4.scala @@ -0,0 +1,19 @@ +trait A { + def foo(): Int +} + +class B extends A { + def foo(): Int = 10 +} + +class C extends A { + def foo(): Int = O.a + 10 +} + +class D(x: Int) { + def bar(): A = if x > 0 then new B else new C +} + +object O { + val a: Int = D(5).bar().foo() // error +} From 9052e245385e4d7297619afc61a27f508d907605 Mon Sep 17 00:00:00 2001 From: Liu Fengyun Date: Mon, 30 Nov 2020 09:58:04 +0100 Subject: [PATCH 34/34] update documentation --- .../other-new-features/safe-initialization.md | 56 +++++++++++++++---- 1 file changed, 46 insertions(+), 10 deletions(-) diff --git a/docs/docs/reference/other-new-features/safe-initialization.md b/docs/docs/reference/other-new-features/safe-initialization.md index 80728393e75c..b5d2ad634731 100644 --- a/docs/docs/reference/other-new-features/safe-initialization.md +++ b/docs/docs/reference/other-new-features/safe-initialization.md @@ -3,7 +3,7 @@ layout: doc-page title: "Safe Initialization" --- -Dotty implements experimental safe initialization check, which can be enabled by the compiler option `-Ycheck-init`. +Dotty implements experimental safe initialization check, which can be enabled by the compiler option `-Ycheck-init fast` or `-Ycheck-init full`. ## A Quick Glance @@ -89,6 +89,39 @@ The checker reports: | -> def message: String = b [ features-high-order.scala:8 ] ``` +### Global Objects + +**Note**: checking for global objects is only available in `full` mode, i.e. `-Ycheck-init full`. + +Given the code below: + +```Scala +object A { + val a: Int = B.b // error +} + +object B { + val b: Int = A.a // error +} +``` + +The checker produces the following warning: + +```Scala +-- Warning: tests/init/full/neg/global-cycle1.scala:2:6 ------------------------ +2 | val a: Int = B.b // error + | ^ + | Access non-initialized value a. Calling trace: + | -> val a: Int = B.b // error [ global-cycle1.scala:2 ] + | -> val b: Int = A.a // error [ global-cycle1.scala:6 ] +-- Warning: tests/init/full/neg/global-cycle1.scala:6:6 ------------------------ +6 | val b: Int = A.a // error + | ^ + | Access non-initialized value b. Calling trace: + | -> val b: Int = A.a // error [ global-cycle1.scala:6 ] + | -> val a: Int = B.b // error [ global-cycle1.scala:2 ] +``` + ## Design Goals We establish the following design goals: @@ -161,10 +194,9 @@ as it may indirectly reach uninitialized fields. Monotonicity is based on a well-known technique called _heap monotonic typestate_ to ensure soundness in the presence of aliasing -[1]. Roughly, it means initialization state should not go backwards. +[1]. Roughly speaking, it means initialization state should not go backwards. -Scopability means that access to partially constructed objects should be -controlled by static scoping. Control effects like coroutines, delimited +Scopability means that there are no side channels to access to partially constructed objects. Control effects like coroutines, delimited control, resumable exceptions may break the property, as they can transport a value upper in the stack (not in scope) to be reachable from the current scope. Static fields can also serve as a teleport thus breaks this property. In the @@ -247,7 +279,7 @@ We can impose the following rules to enforce modularity: ## Theory -The theory is based on type-and-effect systems [2]. We introduce two concepts, +The theory is based on type-and-effect systems [2, 3]. We introduce two concepts, _effects_ and _potentials_: ``` @@ -292,6 +324,8 @@ the initialization and there is no leaking of values under initialization. Virtual method calls on `this` is not a problem, as they can always be resolved statically. +For a more detailed introduction of the theory, please refer to the paper _a type-and-effect system for safe initialization_ [3]. + ## Back Doors Occasionally you may want to suppress warnings reported by the @@ -301,11 +335,13 @@ mark some fields as lazy. ## Caveats -The system cannot handle static fields, nor does it provide safety -guarantee when extending Java or Scala 2 classes. Calling methods of -Java or Scala 2 is always safe. +- The system cannot provide safety guarantee when extending Java or Scala 2 classes. +- Safe initialization of global objects is only available under the _full_ mode (`-Ycheck-init full`) ## References -- Fähndrich, M. and Leino, K.R.M., 2003, July. _Heap monotonic typestates_. In International Workshop on Aliasing, Confinement and Ownership in object-oriented programming (IWACO). -- Lucassen, J.M. and Gifford, D.K., 1988, January. _Polymorphic effect systems_. In Proceedings of the 15th ACM SIGPLAN-SIGACT symposium on Principles of programming languages (pp. 47-57). ACM. +1. Manuel Fähndrich and K. Rustan M. Leino. 2003. _Heap monotonic typestates_. In International Workshop on Aliasing, Confinement and Ownership in object-oriented programming (IWACO). + +2. J. M. Lucassen and D. K. Gifford. 1988. _Polymorphic effect systems_. POPL, 1988. + +3. Fengyun Liu, Ondřej Lhoták, Aggelos Biboudis, Paolo G. Giarrusso, and Martin Odersky. 2020. _A type-and-effect system for object initialization_. OOPSLA, 2020.