diff --git a/compiler/src/dotty/tools/dotc/transform/init/Checker.scala b/compiler/src/dotty/tools/dotc/transform/init/Checker.scala index f604b851a18c..d7f0d074cb9d 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Checker.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Checker.scala @@ -23,7 +23,7 @@ class Checker extends MiniPhase { val phaseName = "initChecker" // cache of class summary - private val baseEnv = Env(null, mutable.Map.empty) + private val baseEnv = Env(null) override val runsAfter = Set(Pickler.name) @@ -52,7 +52,7 @@ class Checker extends MiniPhase { thisClass = cls, fieldsInited = mutable.Set.empty, parentsInited = mutable.Set.empty, - env = baseEnv.withCtx(ctx) + env = baseEnv.withCtx(ctx.withOwner(cls)) ) Checking.checkClassBody(tree) diff --git a/compiler/src/dotty/tools/dotc/transform/init/Checking.scala b/compiler/src/dotty/tools/dotc/transform/init/Checking.scala index 9e7735ff5da7..09742af9d0ae 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Checking.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Checking.scala @@ -30,7 +30,7 @@ object Checking { * */ case class State( - visited: mutable.Set[Effect], // effects that have been expanded + visited: mutable.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], @@ -41,6 +41,8 @@ object Checking { visited += eff copy(path = this.path :+ eff.source) } + + def withOwner(sym: Symbol): State = copy(env = env.withOwner(sym)) } private implicit def theEnv(implicit state: State): Env = state.env @@ -51,17 +53,19 @@ object Checking { * However, summarization can be done lazily on-demand to improve * performance. */ - def checkClassBody(cdef: TypeDef)(implicit state: State): Unit = traceOp("checking " + cdef.symbol.show, init) { + def checkClassBody(cdef: TypeDef)(implicit state: State): Unit = { + traceIndented("\n\n>>>> checking " + cdef.symbol.show, init) + val cls = cdef.symbol.asClass val tpl = cdef.rhs.asInstanceOf[Template] // mark current class as initialized, required for linearization state.parentsInited += cls - def checkClassBodyStat(tree: Tree)(using Context): Unit = traceOp("checking " + tree.show, init) { + 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.withOwner(vdef.symbol)) + val (pots, effs) = Summarization.analyze(vdef.rhs) theEnv.summaryOf(cls).cacheFor(vdef.symbol, (pots, effs)) if (!vdef.symbol.is(Flags.Lazy)) { checkEffectsIn(effs, cls) @@ -79,15 +83,31 @@ 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 checkCtor(ctor: Symbol, tp: Type, source: Tree)(using Context): Unit = { + def checkConstructor(ctor: Symbol, tp: Type, source: Tree)(implicit 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]) - else checkSecondaryConstructor(ctor) + if (ctor.isPrimaryConstructor) checkClassBody(classDef.asInstanceOf[TypeDef])(state.withOwner(cls)) + else checkSecondaryConstructor(ctor)(state.withOwner(cls)) + } + } + + def checkSecondaryConstructor(ctor: Symbol)(implicit state: State): Unit = traceOp("checking " + ctor.show, init) { + val Block(ctorCall :: stats, expr) = ctor.defTree.asInstanceOf[DefDef].rhs + val cls = ctor.owner.asClass + + traceOp("check ctor: " + ctorCall.show, init) { + val ctor = ctorCall.symbol + if (ctor.isPrimaryConstructor) + checkClassBody(cls.defTree.asInstanceOf[TypeDef]) + else + checkSecondaryConstructor(ctor) + } + + (stats :+ expr).foreach { stat => + val (_, effs) = Summarization.analyze(stat)(theEnv.withOwner(ctor)) + checkEffectsIn(effs, cls) } - else if (!cls.isOneOf(Flags.EffectivelyOpenFlags)) - report.warning("Inheriting non-open class may cause initialization errors", source.srcPos) } cls.paramAccessors.foreach { acc => @@ -100,61 +120,42 @@ object Checking { tpl.parents.foreach { case tree @ Block(_, parent) => val (ctor, _, _) = decomposeCall(parent) - checkCtor(ctor.symbol, parent.tpe, tree) + checkConstructor(ctor.symbol, parent.tpe, tree) case tree @ Apply(Block(_, parent), _) => val (ctor, _, _) = decomposeCall(parent) - checkCtor(ctor.symbol, tree.tpe, tree) + checkConstructor(ctor.symbol, tree.tpe, tree) case parent : Apply => val (ctor, _, argss) = decomposeCall(parent) - checkCtor(ctor.symbol, parent.tpe, parent) + checkConstructor(ctor.symbol, parent.tpe, parent) case ref => val cls = ref.tpe.classSymbol.asClass if (!state.parentsInited.contains(cls) && cls.primaryConstructor.exists) - checkCtor(cls.primaryConstructor, ref.tpe, ref) + checkConstructor(cls.primaryConstructor, ref.tpe, ref) } // check class body tpl.body.foreach { checkClassBodyStat(_) } } - def checkSecondaryConstructor(ctor: Symbol)(implicit state: State): Unit = traceOp("checking " + ctor.show, init) { - val Block(ctorCall :: stats, expr) = ctor.defTree.asInstanceOf[DefDef].rhs - val cls = ctor.owner.asClass - - traceOp("check ctor: " + ctorCall.show, init) { - val ctor = ctorCall.symbol - if (ctor.isPrimaryConstructor) - checkClassBody(cls.defTree.asInstanceOf[TypeDef]) - else - checkSecondaryConstructor(ctor) - } - - (stats :+ expr).foreach { stat => - val (_, effs) = Summarization.analyze(stat)(theEnv.withOwner(ctor)) - checkEffectsIn(effs, cls) - } - } - private def checkEffectsIn(effs: Effects, cls: ClassSymbol)(implicit state: State): Unit = traceOp("checking effects " + Effects.show(effs), init) { - val rebased = Effects.asSeenFrom(effs, ThisRef(state.thisClass)(null), cls, Potentials.empty) for { - eff <- rebased + eff <- effs error <- check(eff) } error.issue } private def check(eff: Effect)(implicit state: State): Errors = - if (state.visited.contains(eff)) Errors.empty else trace("checking effect " + eff.show, init, errs => Errors.show(errs.asInstanceOf[Errors])) { + if (state.visited.contains(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(cls) => - assert(cls == state.thisClass, "unexpected potential " + pot.show) + case pot: ThisRef => PromoteThis(pot, eff.source, state2.path).toErrors case _: Cold => @@ -180,18 +181,14 @@ object Checking { case FieldAccess(pot, field) => pot match { - case ThisRef(cls) => - assert(cls == state.thisClass, "unexpected potential " + pot.show) - - val target = resolve(cls, field) + case _: ThisRef => + 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 SuperRef(ThisRef(cls), supercls) => - assert(cls == state.thisClass, "unexpected potential " + pot.show) - - val target = resolveSuper(cls, supercls, field) + 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 @@ -217,10 +214,8 @@ object Checking { case MethodCall(pot, sym) => pot match { - case thisRef @ ThisRef(cls) => - assert(cls == state.thisClass, "unexpected potential " + pot.show) - - val target = resolve(cls, sym) + 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.isInternal) { @@ -229,10 +224,8 @@ object Checking { } else CallUnknown(target, eff.source, state2.path).toErrors - case SuperRef(thisRef @ ThisRef(cls), supercls) => - assert(cls == state.thisClass, "unexpected potential " + pot.show) - - val target = resolveSuper(cls, supercls, sym) + case SuperRef(thisRef: ThisRef, supercls) => + val target = resolveSuper(state.thisClass, supercls, sym) if (!target.is(Flags.Method)) check(FieldAccess(pot, target)(eff.source)) else if (target.isInternal) { @@ -272,17 +265,13 @@ object Checking { pot match { case MethodReturn(pot1, sym) => pot1 match { - case thisRef @ ThisRef(cls) => - assert(cls == state.thisClass, "unexpected potential " + pot.show) - - val target = resolve(cls, sym) + case thisRef: ThisRef => + val target = resolve(state.thisClass, sym) if (target.isInternal) (thisRef.potentialsOf(target), Effects.empty) else Summary.empty // warning already issued in call effect - case SuperRef(thisRef @ ThisRef(cls), supercls) => - assert(cls == state.thisClass, "unexpected potential " + pot.show) - - val target = resolveSuper(cls, supercls, sym) + case SuperRef(thisRef: ThisRef, supercls) => + val target = resolveSuper(state.thisClass, supercls, sym) if (target.isInternal) (thisRef.potentialsOf(target), Effects.empty) else Summary.empty // warning already issued in call effect @@ -314,17 +303,13 @@ object Checking { case FieldReturn(pot1, sym) => pot1 match { - case thisRef @ ThisRef(cls) => - assert(cls == state.thisClass, "unexpected potential " + pot.show) - - val target = resolve(cls, sym) + case thisRef: ThisRef => + val target = resolve(state.thisClass, sym) if (sym.isInternal) (thisRef.potentialsOf(target), Effects.empty) else (Cold()(pot.source).toPots, Effects.empty) - case SuperRef(thisRef @ ThisRef(cls), supercls) => - assert(cls == state.thisClass, "unexpected potential " + pot.show) - - val target = resolveSuper(cls, supercls, sym) + 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) @@ -347,19 +332,15 @@ object Checking { case Outer(pot1, cls) => pot1 match { - case ThisRef(cls) => - assert(cls == state.thisClass, "unexpected potential " + pot.show) - + case _: ThisRef => + // all outers for `this` are assumed to be hot Summary.empty case _: Fun => throw new Exception("Unexpected code reached") case warm: Warm => - (warm.outerFor(cls), Effects.empty) - - case _: Cold => - throw new Exception("Unexpected code reached") + (warm.resolveOuter(cls), Effects.empty) case _ => val (pots, effs) = expand(pot1) diff --git a/compiler/src/dotty/tools/dotc/transform/init/Effects.scala b/compiler/src/dotty/tools/dotc/transform/init/Effects.scala index 9b204c735397..b08bad00592d 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Effects.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Effects.scala @@ -18,10 +18,12 @@ object Effects { def show(effs: Effects)(using Context): String = effs.map(_.show).mkString(", ") - /** Effects that are related to safe initialization */ + /** Effects that are related to safe initialization performed on potentials */ sealed trait Effect { - def size: Int + def potential: Potential + def show(using Context): String + def source: Tree } @@ -37,25 +39,20 @@ object Effects { * - the selection chain on a potential is too long */ case class Promote(potential: Potential)(val source: Tree) extends Effect { - def size: Int = potential.size - def show(using Context): String = - potential.show + "↑" + def show(using Context): String = potential.show + "↑" } /** Field access, `a.f` */ case class FieldAccess(potential: Potential, field: Symbol)(val source: Tree) extends Effect { assert(field != NoSymbol) - def size: Int = potential.size - def show(using Context): String = - potential.show + "." + field.name.show + "!" + def show(using Context): String = potential.show + "." + field.name.show + "!" } /** Method call, `a.m()` */ case class MethodCall(potential: Potential, method: Symbol)(val source: Tree) extends Effect { assert(method != NoSymbol) - def size: Int = potential.size def show(using Context): String = potential.show + "." + method.name.show + "!" } @@ -63,22 +60,21 @@ object Effects { extension (eff: Effect) def toEffs: Effects = Effects.empty + eff - def asSeenFrom(eff: Effect, thisValue: Potential, currentClass: ClassSymbol, outer: Potentials)(implicit env: Env): Effects = - trace(eff.show + " asSeenFrom " + thisValue.show + ", current = " + currentClass.show + ", outer = " + Potentials.show(outer), init, effs => show(effs.asInstanceOf[Effects])) { eff match { + 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 { case Promote(pot) => - Potentials.asSeenFrom(pot, thisValue, currentClass, outer).promote(eff.source) + val pot1 = Potentials.asSeenFrom(pot, thisValue) + Promote(pot1)(eff.source) case FieldAccess(pot, field) => - Potentials.asSeenFrom(pot, thisValue, currentClass, outer).map { pot => - FieldAccess(pot, field)(eff.source) - } + val pot1 = Potentials.asSeenFrom(pot, thisValue) + FieldAccess(pot1, field)(eff.source) case MethodCall(pot, sym) => - Potentials.asSeenFrom(pot, thisValue, currentClass, outer).map { pot => - MethodCall(pot, sym)(eff.source) - } + val pot1 = Potentials.asSeenFrom(pot, thisValue) + MethodCall(pot1, sym)(eff.source) } } - def asSeenFrom(effs: Effects, thisValue: Potential, currentClass: ClassSymbol, outer: Potentials)(implicit env: Env): Effects = - effs.flatMap(asSeenFrom(_, thisValue, currentClass, outer)) + def asSeenFrom(effs: Effects, thisValue: Potential)(implicit env: Env): Effects = + effs.map(asSeenFrom(_, thisValue)) } \ No newline at end of file diff --git a/compiler/src/dotty/tools/dotc/transform/init/Env.scala b/compiler/src/dotty/tools/dotc/transform/init/Env.scala index 47ad287b4860..65b3684bd7f9 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Env.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Env.scala @@ -20,7 +20,7 @@ import Effects._, Potentials._, Summary._ implicit def theCtx(implicit env: Env): Context = env.ctx -case class Env(ctx: Context, summaryCache: mutable.Map[ClassSymbol, ClassSummary]) { +case class Env(ctx: Context) { private implicit def self: Env = this // Methods that should be ignored in the checking @@ -46,7 +46,8 @@ case class Env(ctx: Context, summaryCache: mutable.Map[ClassSymbol, ClassSummary sym.isPrimitiveValueClass || sym == defn.StringClass } - /** Summary of a method or field */ + /** Summary of a class */ + private val summaryCache = mutable.Map.empty[ClassSymbol, ClassSummary] def summaryOf(cls: ClassSymbol): ClassSummary = if (summaryCache.contains(cls)) summaryCache(cls) else trace("summary for " + cls.show, init, s => s.asInstanceOf[ClassSummary].show) { @@ -54,4 +55,16 @@ case class Env(ctx: Context, summaryCache: mutable.Map[ClassSymbol, ClassSummary summaryCache(cls) = summary summary } + + /** Cache for outer this */ + private case class OuterKey(warm: Warm, cls: ClassSymbol) + private val outerCache: mutable.Map[OuterKey, Potentials] = mutable.Map.empty + def resolveOuter(warm: Warm, cls: ClassSymbol)(implicit env: Env): Potentials = + val key = OuterKey(warm, cls) + if (outerCache.contains(key)) outerCache(key) + else { + val pots = Potentials.resolveOuter(warm.classSymbol, warm.outer.toPots, cls) + outerCache(key) = pots + pots + } } diff --git a/compiler/src/dotty/tools/dotc/transform/init/Errors.scala b/compiler/src/dotty/tools/dotc/transform/init/Errors.scala index 94fc4832c7f1..b0c9b4c6e29c 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Errors.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Errors.scala @@ -66,7 +66,7 @@ object Errors { "Access non-initialized field " + field.name.show + "." override def issue(using Context): Unit = - report.error(show + stacktrace, field.srcPos) + report.warning(show + stacktrace, field.srcPos) } /** Promote `this` under initialization to fully-initialized */ diff --git a/compiler/src/dotty/tools/dotc/transform/init/Potentials.scala b/compiler/src/dotty/tools/dotc/transform/init/Potentials.scala index 14022140e377..282e5344f7a3 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Potentials.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Potentials.scala @@ -22,39 +22,39 @@ object Potentials { /** A potential represents an aliasing of a value that is possibly under initialization */ sealed trait Potential { - def size: Int + /** Length of the potential. Used for widening */ + def size: Int = 1 + + /** Nested levels of the potential. Used for widening */ + def level: Int = 1 + def show(using Context): String def source: Tree } - /** The object pointed by `C.this` */ - case class ThisRef(classSymbol: ClassSymbol)(val source: Tree) extends Potential { - val size: Int = 1 - def show(using Context): String = classSymbol.name.show + ".this" + /** The object pointed by `this` */ + case class ThisRef()(val source: Tree) extends Potential { + def show(using Context): String = "this" /** Effects of a method call or a lazy val access - * - * It assumes all the outer `this` are fully initialized. */ 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, cls, Potentials.empty) + env.summaryOf(cls).effectsOf(sym) } /** Potentials of a field, a method call or a lazy val access - * */ 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, cls, Potentials.empty) + env.summaryOf(cls).potentialsOf(sym) } } /** The object pointed by `C.super.this`, mainly used for override resolution */ case class SuperRef(pot: Potential, supercls: ClassSymbol)(val source: Tree) extends Potential { - val size: Int = 1 + override def size: Int = pot.size + override def level: Int = pot.level def show(using Context): String = pot.show + ".super[" + supercls.name.show + "]" } @@ -65,42 +65,45 @@ object Potentials { * @param outer The potential for `this` of the enclosing class */ case class Warm(classSymbol: ClassSymbol, outer: Potential)(val source: Tree) extends Potential { - def size: Int = 1 + 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 and outer substitution + * 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) - val outer = Outer(this, cls)(this.source) - Effects.asSeenFrom(effs, this, cls, outer.toPots) + Effects.asSeenFrom(effs, this) } /** Potentials of a field, a method call or a lazy val access * - * The method performs prefix and outer substitution + * 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) - val outer = Outer(this, cls)(this.source) - Potentials.asSeenFrom(pots, this, cls, outer.toPots) + Potentials.asSeenFrom(pots, this) } - private val outerCache: mutable.Map[ClassSymbol, Potentials] = mutable.Map.empty - def outerFor(cls: ClassSymbol)(implicit env: Env): Potentials = - if (outerCache.contains(cls)) outerCache(cls) - else if (cls `eq` classSymbol) outer.toPots - else { - val bottomClsSummary = env.summaryOf(classSymbol) - val objPart = ObjectPart(this, classSymbol, outer.toPots, bottomClsSummary.parentOuter) - val pots = objPart.outerFor(cls) - outerCache(cls) = pots - pots + def resolveOuter(cls: ClassSymbol)(implicit env: Env): Potentials = + env.resolveOuter(this, cls) + } + + def resolveOuter(cur: ClassSymbol, outerPots: Potentials, cls: ClassSymbol)(implicit env: Env): Potentials = + trace("resolveOuter for " + cls.show + ", outer = " + show(outerPots) + ", cur = " + cur.show, init, s => Potentials.show(s.asInstanceOf[Potentials])) { + if (cur == cls) outerPots + else { + val bottomClsSummary = env.summaryOf(cur) + bottomClsSummary.parentOuter.find((k, v) => k.derivesFrom(cls)) match { + case Some((parentCls, pots)) => + val rebased: Potentials = outerPots.flatMap { Potentials.asSeenFrom(pots, _) } + resolveOuter(parentCls, rebased, cls) + case None => ??? // impossible } + } } /** The Outer potential for `classSymbol` of the object `pot` @@ -112,15 +115,18 @@ object Potentials { * and may be potentially faster. */ case class Outer(pot: Potential, classSymbol: ClassSymbol)(val source: Tree) extends Potential { - def size: Int = 1 - def show(using Context): String = "Outer[" + pot.show + ", " + classSymbol.show + "]" + // be lenient with size of outer selection, no worry for non-termination + override def size: Int = pot.size + override def level: Int = pot.size + def show(using Context): String = pot.show + ".outer[" + classSymbol.show + "]" } /** The object pointed by `this.f` */ case class FieldReturn(potential: Potential, field: Symbol)(val source: Tree) extends Potential { assert(field != NoSymbol) - def size: Int = potential.size + 1 + override def size: Int = potential.size + 1 + override def level: Int = potential.size def show(using Context): String = potential.show + "." + field.name.show } @@ -128,19 +134,26 @@ object Potentials { case class MethodReturn(potential: Potential, method: Symbol)(val source: Tree) extends Potential { assert(method != NoSymbol) - def size: Int = potential.size + 1 + override def size: Int = potential.size + 1 + override def level: Int = potential.size def show(using Context): String = potential.show + "." + method.name.show } /** The object whose initialization status is unknown */ case class Cold()(val source: Tree) extends Potential { - def size: Int = 1 def show(using Context): String = "Cold" } /** A function when called will produce the `effects` and return the `potentials` */ case class Fun(potentials: Potentials, effects: Effects)(val source: Tree) extends Potential { - def size: Int = 1 + override def size: Int = 1 + + override def level: Int = { + val max1 = potentials.map(_.level).max + val max2 = effects.map(_.potential.level).max + if max1 > max2 then max1 else max2 + } + def show(using Context): String = "Fun[pots = " + potentials.map(_.show).mkString(";") + ", effs = " + effects.map(_.show).mkString(";") + "]" } @@ -168,55 +181,50 @@ object Potentials { extension (ps: Potentials) def promote(source: Tree): Effects = ps.map(Promote(_)(source)) - def asSeenFrom(pot: Potential, thisValue: Potential, currentClass: ClassSymbol, outer: Potentials)(implicit env: Env): Potentials = - trace(pot.show + " asSeenFrom " + thisValue.show + ", current = " + currentClass.show + ", outer = " + show(outer), init, pots => show(pots.asInstanceOf[Potentials])) { pot match { + def asSeenFrom(pot: Potential, thisValue: Potential)(implicit env: Env): Potential = trace(pot.show + " asSeenFrom " + thisValue.show, init, pot => pot.asInstanceOf[Potential].show) { + pot match { case MethodReturn(pot1, sym) => - val pots = asSeenFrom(pot1, thisValue, currentClass, outer) - pots.map { MethodReturn(_, sym)(pot.source) } + val pot = asSeenFrom(pot1, thisValue) + MethodReturn(pot, sym)(pot.source) case FieldReturn(pot1, sym) => - val pots = asSeenFrom(pot1, thisValue, currentClass, outer) - pots.map { FieldReturn(_, sym)(pot.source) } + val pot = asSeenFrom(pot1, thisValue) + FieldReturn(pot, sym)(pot.source) case Outer(pot1, cls) => - val pots = asSeenFrom(pot1, thisValue, currentClass, outer) - pots map { Outer(_, cls)(pot.source) } - - case ThisRef(cls) => - if (cls `eq` currentClass) - thisValue.toPots - else if (currentClass.is(Flags.Package)) - Potentials.empty - else { - val outerCls = currentClass.owner.enclosingClass.asClass - outer.flatMap { out => - asSeenFrom(pot, out, outerCls, Outer(out, outerCls)(out.source).toPots) - } - } + val pot = asSeenFrom(pot1, thisValue) + Outer(pot, cls)(pot.source) + + case _: ThisRef => + thisValue case Fun(pots, effs) => - val pots1 = Potentials.asSeenFrom(pots, thisValue, currentClass, outer) - val effs1 = Effects.asSeenFrom(effs, thisValue, currentClass, outer) - Fun(pots1, effs1)(pot.source).toPots + val pots1 = Potentials.asSeenFrom(pots, thisValue) + val effs1 = Effects.asSeenFrom(effs, thisValue) + Fun(pots1, effs1)(pot.source) case Warm(cls, outer2) => // widening to terminate val thisValue2 = thisValue match { - case Warm(cls, outer) => Warm(cls, Cold()(outer.source))(thisValue.source) - case _ => thisValue + case Warm(cls, outer) if outer.level > 2 => + Warm(cls, Cold()(outer2.source))(thisValue.source) + + case _ => + thisValue } - val outer3 = asSeenFrom(outer2, thisValue2, currentClass, outer) - outer3.map { Warm(cls, _)(pot.source) } + val outer3 = asSeenFrom(outer2, thisValue2) + Warm(cls, outer3)(pot.source) case _: Cold => - pot.toPots + pot - case SuperRef(pot, supercls) => - val pots = asSeenFrom(pot, thisValue, currentClass, outer) - pots.map { SuperRef(_, supercls)(pot.source) } - } } + case SuperRef(potThis, supercls) => + val pot1 = asSeenFrom(potThis, thisValue) + SuperRef(pot1, supercls)(pot.source) + } + } - def asSeenFrom(pots: Potentials, thisValue: Potential, currentClass: ClassSymbol, outer: Potentials)(implicit env: Env): Potentials = - pots.flatMap(asSeenFrom(_, thisValue, currentClass, outer)) + def asSeenFrom(pots: Potentials, thisValue: Potential)(implicit env: Env): Potentials = + pots.map(asSeenFrom(_, thisValue)) } \ No newline at end of file diff --git a/compiler/src/dotty/tools/dotc/transform/init/Summarization.scala b/compiler/src/dotty/tools/dotc/transform/init/Summarization.scala index 0584971aac97..ca797ac81f53 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Summarization.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Summarization.scala @@ -37,10 +37,7 @@ object Summarization { analyze(expr.tpe, expr) case supert: Super => - val SuperType(thisTp, superTp) = supert.tpe.asInstanceOf[SuperType] - val thisRef = ThisRef(thisTp.widen.classSymbol.asClass)(supert) - val pots = superTp.classSymbols.map { cls => SuperRef(thisRef, cls.asClass)(supert) } - (pots.toSet, Effects.empty) + analyze(supert.tpe, supert) case Select(qualifier, name) => val (pots, effs) = analyze(qualifier) @@ -54,17 +51,7 @@ object Summarization { } case _: This => - // With self type, the type can be `A & B`. - def classes(tp: Type): Set[ClassSymbol] = tp.widen match { - case AndType(tp1, tp2) => - classes(tp1) ++ classes(tp2) - - case tp => - Set(tp.classSymbol.asClass) - } - - val pots: Potentials = classes(expr.tpe).map{ ThisRef(_)(expr) } - (pots, Effects.empty) + analyze(expr.tpe, expr) case Apply(fun, args) => val summary = analyze(fun) @@ -99,9 +86,15 @@ object Summarization { val cls = tref.classSymbol.asClass // local class may capture, thus we need to track it if (tref.prefix == NoPrefix) { - val enclosingCls = cls.enclosingClass.asClass - val thisRef = ThisRef(enclosingCls)(expr) - Summary.empty + Warm(cls, thisRef)(expr) + val cur = theCtx.owner.lexicallyEnclosingClass.asClass + 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) + else { + assert(pots.size == 1) + (Warm(cls, pots.head)(expr).toPots, effs) + } } else { val (pots, effs) = analyze(tref.prefix, expr) @@ -233,17 +226,18 @@ object Summarization { (pots2, effs ++ effs2) } - case ThisType(tref: TypeRef) if tref.classSymbol.is(Flags.Package) => - Summary.empty - - case thisTp: ThisType => - val cls = thisTp.tref.classSymbol.asClass - Summary.empty + ThisRef(cls)(source) + case ThisType(tref) => + val enclosing = env.ctx.owner.lexicallyEnclosingClass.asClass + val cls = tref.symbol.asClass + resolveThis(cls, ThisRef()(source), enclosing, source) case SuperType(thisTp, superTp) => - val thisRef = ThisRef(thisTp.classSymbol.asClass)(source) - val pot = SuperRef(thisRef, superTp.classSymbol.asClass)(source) - Summary.empty + pot + val (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) case _ => throw new Exception("unexpected type: " + tp.show) @@ -264,6 +258,22 @@ object Summarization { analyze(vdef.rhs)(env.withOwner(sym)) } + 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])) { + 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 { + 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 + + assert(!enclosing.is(Flags.Package), "enclosing = " + enclosing.show + ", cls = " + cls.show + ", pot = " + pot.show + ", cur = " + cur.show) + val pot2 = Outer(pot, cur)(pot.source) + resolveThis(cls, pot2, enclosing, source) + } + } + /** 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])) { @@ -273,7 +283,7 @@ object Summarization { val effs = analyze(Block(tpl.body, unitLiteral))._2 def parentArgEffsWithInit(stats: List[Tree], ctor: Symbol, source: Tree): Effects = - val initCall = MethodCall(ThisRef(cls)(source), ctor)(source) + val initCall = MethodCall(ThisRef()(source), ctor)(source) stats.foldLeft(Set(initCall)) { (acc, stat) => val (_, effs) = Summarization.analyze(stat) acc ++ effs @@ -300,7 +310,7 @@ object Summarization { else { val ctor = cls.primaryConstructor Summarization.analyze(tref.prefix, ref)._2 + - MethodCall(ThisRef(cls)(ref), ctor)(ref) + MethodCall(ThisRef()(ref), ctor)(ref) } }) } @@ -313,14 +323,15 @@ object Summarization { } } - def classSummary(cls: ClassSymbol)(implicit env: Env): ClassSummary = + def classSummary(cls: ClassSymbol)(implicit env: Env): ClassSummary = trace("summarizing " + cls.show, init) { def extractParentOuters(parent: Type, source: Tree): (ClassSymbol, Potentials) = { val tref = parent.typeConstructor.stripAnnots.asInstanceOf[TypeRef] val parentCls = tref.classSymbol.asClass + val env2: Env = env.withOwner(cls.owner.lexicallyEnclosingClass) if (tref.prefix != NoPrefix) - parentCls ->analyze(tref.prefix, source)._1 + parentCls -> analyze(tref.prefix, source)(env2)._1 else - parentCls -> analyze(cls.enclosingClass.thisType, source)._1 + parentCls -> analyze(cls.owner.lexicallyEnclosingClass.thisType, source)(env2)._1 } if (cls.defTree.isEmpty) @@ -341,5 +352,6 @@ object Summarization { val parentOuter = parents.map { parent => extractParentOuters(parent.tpe, parent) } ClassSummary(cls, parentOuter.toMap) } + } } diff --git a/compiler/src/dotty/tools/dotc/transform/init/Summary.scala b/compiler/src/dotty/tools/dotc/transform/init/Summary.scala index 35c264751434..0338f1cb0a5d 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Summary.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Summary.scala @@ -32,6 +32,7 @@ object Summary { 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) @@ -52,34 +53,6 @@ object Summary { ", parents = " + parentOuter.map { case (k, v) => k.show + "->" + "[" + Potentials.show(v) + "]" } } - /** Part of object. - * - * It makes prefix and outer substitution easier in effect checking. - */ - case class ObjectPart( - thisValue: Warm, // the potential for `this`, it can be Warm or ThisRef - currentClass: ClassSymbol, // current class - currentOuter: Potentials, // the immediate outer for current class, empty for local and top-level classes - parentOuter: Map[ClassSymbol, Potentials] // outers for direct parents - ) { - private val summaryCache: mutable.Map[Symbol, Summary] = mutable.Map.empty - - def outerFor(cls: ClassSymbol)(implicit env: Env): Potentials = - if (cls `eq` currentClass) currentOuter - else parentOuter.find((k, v) => k.derivesFrom(cls)) match { - case Some((parentCls, pots)) => - val bottomClsSummary = env.summaryOf(parentCls) - val rebased: Potentials = Potentials.asSeenFrom(pots, thisValue, currentClass, currentOuter) - val objPart = ObjectPart(thisValue, parentCls, rebased, bottomClsSummary.parentOuter) - objPart.outerFor(cls) - case None => ??? // impossible - } - - def show(using Context): String = - "ObjectPart(this = " + thisValue.show + "," + currentClass.name.show + ", outer = " + Potentials.show(currentOuter) + - "parents = " + parentOuter.map { case (k, v) => k.show + "->" + "[" + Potentials.show(v) + "]" } - } - def show(summary: Summary)(using Context): String = { val pots = Potentials.show(summary._1) val effs = Effects.show(summary._2) diff --git a/docs/docs/reference/other-new-features/safe-initialization.md b/docs/docs/reference/other-new-features/safe-initialization.md index 0ef119f67b37..98cd71ad5699 100644 --- a/docs/docs/reference/other-new-features/safe-initialization.md +++ b/docs/docs/reference/other-new-features/safe-initialization.md @@ -107,17 +107,15 @@ By _reasonable usage_, we include the following use cases (but not restricted to - Instantiate inner class and call methods on such instances during initialization - Capture fields in functions -## Principles and Rules +## Principles To achieve the goals, we uphold three fundamental principles: _stackability_, _monotonicity_ and _scopability_. -Stackability means that objects are initialized in stack order: if the -object `b` is created during the initialization of object `a`, then -all fields of `b` should become initialized before or at the same time -as `a`. Scala enforces this property in syntax by demanding that all -fields are initialized at the end of the primary constructor, except -for the language feature below: +Stackability means that all fields of a class are initialized at the end of the +class body. Scala enforces this property in syntax by demanding that all fields +are initialized at the end of the primary constructor, except for the language +feature below: ``` scala var x: T = _ @@ -163,22 +161,29 @@ 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]. Otherwise, either soundness will be compromised or we have to -disallow the usage of already initialized fields. - -Scopability means that an expression may only access existing objects via formal -parameters and `this`. More precisely, given any environment `ρ` (which are the -value bindings for method parameters and `this`) and heap `σ` for evaluating an expression -`e`, if the resulting value reaches an object `o` pre-existent in `σ`, then `o` -is reachable from `ρ` in `σ`. Control effects like coroutines, delimited +[1]. Roughly, 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 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 implementation, we need to enforce that teleported values are transitively initialized. +The principles enable _local reasoning_ of initialization, which means: + +> An initialized environment can only produce initialized values. + +For example, if the arguments to an `new`-expression are transitively +initialized, so is the result. If the receiver and arguments in a method call +are transitively initialized, so is the result. + +## Rules + With the established principles and design goals, following rules are imposed: + 1. In an assignment `o.x = e`, the expression `e` may only point to transitively initialized objects. This is how monotonicity is enforced in the system. Note that in an @@ -203,11 +208,15 @@ With the established principles and design goals, following rules are imposed: reasoning about initialization: programmers may safely assume that all local definitions only point to transitively initialized objects. -## Modularity +## Modularity (considered) + +Currently, the analysis works across project boundaries based on TASTy. +The following is a proposal to make the checking more modular. +The feedback from the community is welcome. -For modularity, we forbid subtle initialization interaction beyond project -boundaries. For example, the following code passes the check when the two -classes are defined in the same project: +For modularity, we need to forbid subtle initialization interaction beyond +project boundaries. For example, the following code passes the check when the +two classes are defined in the same project: ```Scala class Base { @@ -221,14 +230,14 @@ class Child extends Base { ``` However, when the class `Base` and `Child` are defined in two different -projects, the check will emit a warning for the calls to `enter` in the class +projects, the check can emit a warning for the calls to `enter` in the class `Child`. This restricts subtle initialization within project boundaries, and avoids accidental violation of contracts across library versions. -We impose the following rules to enforce modularity: +We can impose the following rules to enforce modularity: 1. A class or trait that may be extended in another project should not - call virtual methods on `this` in its template/mixin evaluation, + call _virtual_ methods on `this` in its template/mixin evaluation, directly or indirectly. 2. The method call `o.m(args)` is forbidden if `o` is not transitively @@ -237,32 +246,26 @@ We impose the following rules to enforce modularity: 3. The expression `new p.C(args)` is forbidden, if `p` is not transitively initialized and `C` is defined in an external project. -Theoretically, we may analyze across project boundaries based on tasty. However, -from our experience with Dotty community projects, most subtle initialization -patterns are restricted in the same project. As the rules only report warnings -instead of errors, we think it is good to first impose more strict rules, The -feedback from the community is welcome. - ## Theory The theory is based on type-and-effect systems [2]. We introduce two concepts, _effects_ and _potentials_: ``` -π = C.this | Warm(C, π) | π.f | π.m | π.super[D] | Cold | Fun(Π, Φ) | Outer(C, π) +π = this | Warm(C, π) | π.f | π.m | π.super[D] | Cold | Fun(Π, Φ) | π.outer[C] ϕ = π↑ | π.f! | π.m! ``` Potentials (π) represent values that are possibly under initialization. -- `C.this`: current object +- `this`: current object - `Warm(C, π)`: an object of type `C` where all its fields are assigned, and the potential for `this` of its enclosing class is `π`. - `π.f`: the potential of the field `f` in the potential `π` - `π.m`: the potential of the field `f` in the potential `π` - `π.super[D]`: essentially the object π, used for virtual method resolution - `Cold`: an object with unknown initialization status - `Fun(Π, Φ)`: a function, when called produce effects Φ and return potentials Π. -- `Outer(C, π)`: the potential of `this` for the enclosing class of `C` when `C.this` is `π`. +- `π.outer[C]`: the potential of `this` for the enclosing class of `C` when `C.this` is `π`. Effects are triggered from potentials: @@ -290,8 +293,6 @@ 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. -More details can be found in a forthcoming paper. - ## Back Doors Occasionally you may want to suppress warnings reported by the diff --git a/tests/init/neg/local-class.scala b/tests/init/neg/local-class.scala new file mode 100644 index 000000000000..20e77912dce8 --- /dev/null +++ b/tests/init/neg/local-class.scala @@ -0,0 +1,16 @@ +class Outer { + def foo = { + class C { + val x = n + } + class D { + new C + } + + new D + } + + foo + + val n = 10 // error +} diff --git a/tests/init/pos/by-name-inline.scala b/tests/init/pos/by-name-inline.scala.bak similarity index 100% rename from tests/init/pos/by-name-inline.scala rename to tests/init/pos/by-name-inline.scala.bak diff --git a/tests/init/pos/explicitOuter.scala b/tests/init/pos/explicitOuter.scala new file mode 100644 index 000000000000..e330e554f614 --- /dev/null +++ b/tests/init/pos/explicitOuter.scala @@ -0,0 +1,10 @@ +class Outer(elem: Int, val next: Outer) { + def inner2 = { + class C { + val x = elem + } + class D { + new C + } + } +}