diff --git a/compiler/src/dotty/tools/dotc/transform/init/Cache.scala b/compiler/src/dotty/tools/dotc/transform/init/Cache.scala deleted file mode 100644 index 763c7b70b52e..000000000000 --- a/compiler/src/dotty/tools/dotc/transform/init/Cache.scala +++ /dev/null @@ -1,43 +0,0 @@ -package dotty.tools.dotc -package transform -package init - -import core._ -import Contexts._ -import Types._ -import Symbols._ -import Decorators._ - -import ast.Trees._ -import ast.tpd - -import reporting.trace -import config.Printers.init - -import scala.collection.mutable - -import Effects._, Potentials._, Summary._ - -class Cache { - /** Summary of a class */ - private val summaryCache = mutable.Map.empty[ClassSymbol, ClassSummary] - def summaryOf(cls: ClassSymbol)(using Env): ClassSummary = - if (summaryCache.contains(cls)) summaryCache(cls) - else trace("summary for " + cls.show, init, s => s.asInstanceOf[ClassSummary].show) { - val summary = Summarization.classSummary(cls) - 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)(using 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/Checker.scala b/compiler/src/dotty/tools/dotc/transform/init/Checker.scala index 23b8d826a77c..2c92f446047f 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Checker.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Checker.scala @@ -23,9 +23,6 @@ class Checker extends MiniPhase { val phaseName = "initChecker" - // cache of class summary - private val cache = new Cache - private val semantic = new Semantic override val runsAfter = Set(Pickler.name) @@ -49,18 +46,6 @@ class Checker extends MiniPhase { // A concrete class may not be instantiated if the self type is not satisfied if (instantiable && cls.enclosingPackageClass != defn.StdLibPatchesPackage.moduleClass) { - implicit val state: Checking.State = Checking.State( - visited = Set.empty, - path = Vector.empty, - thisClass = cls, - fieldsInited = mutable.Set.empty, - parentsInited = mutable.Set.empty, - safePromoted = mutable.Set.empty, - env = Env(ctx.withOwner(cls), cache) - ) - - // Checking.checkClassBody(tree) - import semantic._ val tpl = tree.rhs.asInstanceOf[Template] val thisRef = ThisRef(cls) diff --git a/compiler/src/dotty/tools/dotc/transform/init/Checking.scala b/compiler/src/dotty/tools/dotc/transform/init/Checking.scala deleted file mode 100644 index 1695b0c069ca..000000000000 --- a/compiler/src/dotty/tools/dotc/transform/init/Checking.scala +++ /dev/null @@ -1,489 +0,0 @@ -package dotty.tools.dotc -package transform -package init - -import scala.collection.mutable - -import core._ -import Contexts._ -import ast.tpd._ -import Decorators._ -import Symbols._ -import Constants.Constant -import Types._ -import util.NoSourcePosition -import reporting.trace -import config.Printers.init - -import Effects._, Potentials._, Summary._, Util._, Errors._ - -object Checking { - /** The checking state - * - * Why `visited` is a set of effects instead of `Symbol`? Think the following program: - * - * class C(x: Int, a: A @cold) { - * val n = if (x > 0) new C(x - 1, a).m() else 0 - * val b: Int = this.m() - * def m(): Int = b - * } - * - */ - - case class State( - var visited: 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], - parentsInited: mutable.Set[ClassSymbol], - safePromoted: mutable.Set[Potential], // Potentials that can be safely promoted - env: Env - ) { - def withOwner[T](sym: Symbol)(op: State ?=> T): T = - val state = this.copy(env = env.withOwner(sym)) - val res = op(using state) - this.visited = state.visited - res - - - def visit[T](eff: Effect)(op: State ?=> T): T = - val state: State = this.copy(path = path :+ eff.source, visited = this.visited + eff) - val res = op(using state) - this.visited = state.visited - res - - def test(op: State ?=> Errors): Errors = { - val savedVisited = visited - val errors = op(using this) - visited = savedVisited - errors - } - } - - given theEnv(using State): Env = summon[State].env - given theCtx(using State): Context = summon[State].env.ctx - - private def check(eff: Effect)(using state: State): Errors = { - trace("checking effect " + eff.show, init, errs => Errors.show(errs.asInstanceOf[Errors])) { - if (state.visited.contains(eff)) { - traceIndented("Already checked " + eff.show, init) - Errors.empty - } - else - state.visit(eff) { - eff match { - case eff: Promote => Checking.checkPromote(eff) - case eff: FieldAccess => Checking.checkFieldAccess(eff) - case eff: MethodCall => Checking.checkMethodCall(eff) - } - } - } - } - - private def checkEffects(effs: Effects)(using state: State): Unit = traceOp("checking effects " + Effects.show(effs), init) { - for { - eff <- effs - error <- check(eff) - } error.issue - } - - /** Check that the given concrete class may be initialized safely - * - * It assumes that all definitions are properly summarized before-hand. - * However, summarization can be done lazily on-demand to improve - * performance. - */ - def checkClassBody(cdef: TypeDef)(using state: State): Unit = { - traceIndented("\n\n>>>> checking " + cdef.symbol.show, init) - - val cls = cdef.symbol.asClass - val tpl = cdef.rhs.asInstanceOf[Template] - - if state.parentsInited.contains(cls) then return - - // mark current class as initialized, required for linearization - state.parentsInited += cls - - def checkClassBodyStat(tree: Tree)(using state: State): Unit = traceOp("checking " + tree.show, init) { - tree match { - case vdef : ValDef => - val summary = Summarization.analyze(vdef.rhs) - theEnv.summaryOf(cls).cacheFor(vdef.symbol, summary) - if (!vdef.symbol.isOneOf(Flags.Lazy | Flags.Deferred)) { - checkEffects(summary.effs) - traceIndented(vdef.symbol.show + " initialized", init) - state.fieldsInited += vdef.symbol - } - - case tree => - val summary = Summarization.analyze(tree) - checkEffects(summary.effs) - } - } - - // check parent calls : follows linearization ordering - // 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)(using state: State): Unit = traceOp("checking " + ctor.show, init) { - val cls = ctor.owner - val classDef = cls.defTree - if (!classDef.isEmpty) - state.withOwner(cls) { - if (ctor.isPrimaryConstructor) checkClassBody(classDef.asInstanceOf[TypeDef]) - else checkSecondaryConstructor(ctor) - } - } - - 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 - - traceOp("check ctor: " + ctorCall.show, init) { - val ctor = ctorCall.symbol - if (ctor.isPrimaryConstructor) - checkClassBody(cls.defTree.asInstanceOf[TypeDef]) - else - checkSecondaryConstructor(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)) { - traceIndented(acc.show + " initialized", init) - state.fieldsInited += acc - } - } - - tpl.parents.foreach { - case tree @ Block(_, parent) => - checkConstructor(funPart(parent).symbol, parent.tpe, tree) - - case tree @ Apply(Block(_, parent), _) => - checkConstructor(funPart(parent).symbol, tree.tpe, tree) - - case parent : Apply => - checkConstructor(funPart(parent).symbol, parent.tpe, parent) - - case ref => - val cls = ref.tpe.classSymbol.asClass - if (cls.primaryConstructor.exists) - checkConstructor(cls.primaryConstructor, ref.tpe, ref) - } - - // check class body - tpl.body.foreach { checkClassBodyStat(_) } - } - - 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)) - check(FieldAccess(pot, target)(eff.source)) - else if (target.hasSource) { - val effs = thisRef.effectsOf(target).toList - effs.flatMap { 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)) - check(FieldAccess(pot, target)(eff.source)) - else if (target.hasSource) { - val effs = thisRef.effectsOf(target).toList - effs.flatMap { check(_) } - } - 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(_) } - } - else if (!sym.isConstructor) - CallUnknown(target, eff.source, state.path).toErrors - else - Errors.empty - - 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 { 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(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)) 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, 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)) check(MethodCall(pot, target)(eff.source)) - else Errors.empty - - case _: Cold => - AccessCold(field, eff.source, state.path).toErrors - - case Fun(pots, effs) => - throw new Exception("Unexpected effect " + eff.show) - - case pot => - val Summary(pots, effs) = expand(pot) - val effs2 = pots.map(FieldAccess(_, field)(eff.source)) - (effs2 ++ effs).toList.flatMap(check(_)) - } - - /** - * Check if we can just directly promote a potential. - * A potential can be (currently) directly promoted if and only if: - * - `pot == this` and all fields of this are initialized, or - * - `pot == Warm(C, outer)` where `outer` can be directly promoted. - */ - private def canDirectlyPromote(pot: Potential, visited: Set[Potential] = Set.empty)(using state: State): Boolean = trace("checking direct promotion of " + pot.show, init) { - if (state.safePromoted.contains(pot)) true - // If this potential's promotion depends on itself, we cannot directly promote it. - else if (visited.contains(pot)) false - else pot match { - case pot: ThisRef => - // If we have all fields initialized, then we can promote This to hot. - val classRef = state.thisClass.info.asInstanceOf[ClassInfo].appliedRef - classRef.fields.forall { denot => - val sym = denot.symbol - sym.isOneOf(Flags.Lazy | Flags.Deferred) || state.fieldsInited.contains(sym) - } - case Warm(cls, outer) => - canDirectlyPromote(outer) - case _ => - val summary = expand(pot) - if (!summary.effs.isEmpty) - false // max depth of expansion reached - else summary.pots.forall(canDirectlyPromote(_, visited + pot)) - } - } - - /** - * Check the Promotion of a Warm object, according to "Rule 2": - * - * Rule 2: Promote(pot) - * - * for all concrete methods `m` of D - * pot.m!, Promote(pot.m) - * - * for all concrete fields `f` of D - * Promote(pot.f) - * - * for all inner classes `F` of D - * Warm[F, pot].init!, Promote(Warm[F, pot]) - */ - private def checkPromoteWarm(warm: Warm, eff: Effect)(using state: State): Errors = - val Warm(cls, outer) = warm - val source = eff.source - // Errors.empty - val classRef = cls.info.asInstanceOf[ClassInfo].appliedRef - // All members of class must be promotable. - val buffer = new mutable.ArrayBuffer[Effect] - val excludedFlags = Flags.Deferred | Flags.Private | Flags.Protected - - classRef.fields.foreach { denot => - val f = denot.symbol - if !f.isOneOf(excludedFlags) && f.hasSource then - buffer += Promote(FieldReturn(warm, f)(source))(source) - } - - classRef.membersBasedOnFlags(Flags.Method, Flags.Deferred).foreach { denot => - val m = denot.symbol - if !m.isConstructor && m.hasSource && !theEnv.canIgnoreMethod(m) then - buffer += MethodCall(warm, m)(source) - buffer += Promote(MethodReturn(warm, m)(source))(source) - } - - classRef.memberClasses.foreach { denot => - val cls = denot.symbol.asClass - if cls.hasSource then - val potInner = Potentials.asSeenFrom(Warm(cls, ThisRef()(source))(source), warm) - buffer += MethodCall(potInner, cls.primaryConstructor)(source) - buffer += Promote(potInner)(source) - } - - for (eff <- buffer.toList) { - val errs = check(eff) - if !errs.isEmpty then - return UnsafePromotion(eff.source, state.path, errs.toList).toErrors - } - Errors.empty - - private def checkPromote(eff: Promote)(using state: State): Errors = - if (state.safePromoted.contains(eff.potential)) Errors.empty - else { - val pot = eff.potential - val errs = - if canDirectlyPromote(pot) then - Errors.empty - else pot match { - case pot: ThisRef => - PromoteThis(eff.source, state.path).toErrors - - case _: Cold => - PromoteCold(eff.source, state.path).toErrors - - case pot @ Warm(cls, outer) => - checkPromoteWarm(pot, eff) - - case Fun(pots, effs) => - val errs1 = state.test { - effs.toList.flatMap(check(_)) - } - val errs2 = state.test { - pots.toList.flatMap { pot => - checkPromote(Promote(pot)(eff.source)) - } - } - - if (errs1.nonEmpty || errs2.nonEmpty) - UnsafePromotion(eff.source, state.path, errs1 ++ errs2).toErrors - else - Errors.empty - - case pot => - val Summary(pots, effs) = expand(pot) - val effs2 = pots.map(Promote(_)(eff.source)) - (effs2 ++ effs).toList.flatMap(check(_)) - } - // If we can safely promote, then we don't need to check again - if (errs.isEmpty) - state.safePromoted += pot - errs - } - - 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 { - case thisRef: ThisRef => - val target = resolve(state.thisClass, sym) - 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.hasSource) 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") Summary(pots) - else if (name == "tupled") Summary(pot1) - else if (name == "curried") { - val arity = defn.functionArity(sym.info.finalResultType) - val pots = (1 until arity).foldLeft(Vector(pot1)) { (acc, i) => - Vector(Fun(acc, Effects.empty)(pot1.source)) - } - Summary(pots) - } - else Summary.empty - - case warm : Warm => - val target = resolve(warm.classSymbol, sym) - if (target.hasSource) Summary(warm.potentialsOf(target), Effects.empty) - else Summary.empty // warning already issued in call effect - - case _: Cold => - Summary.empty // error already reported, ignore - - case _ => - val Summary(pots, effs) = expand(pot1) - val Summary(pots2, effs2) = pots.select(sym, pot.source, ignoreSelectEffect = false) - Summary(pots2, effs ++ effs2) - } - - case FieldReturn(pot1, sym) => - pot1 match { - case thisRef: ThisRef => - val target = resolve(state.thisClass, sym) - 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.hasSource) 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.hasSource) Summary(warm.potentialsOf(target), Effects.empty) - else Summary(Cold()(pot.source)) - - case _: Cold => - Summary.empty // error already reported, ignore - - case _ => - val Summary(pots, effs) = expand(pot1) - val Summary(pots2, effs2) = pots.select(sym, pot.source, ignoreSelectEffect = false) - Summary(pots2, effs ++ effs2) - } - - case Outer(pot1, cls) => - pot1 match { - case _: ThisRef => - // all outers for `this` are assumed to be hot - Summary.empty - - case _: Fun => - throw new Exception("Unexpected code reached") - - case warm: Warm => - Summary(warm.resolveOuter(cls)) - - case _ => - val Summary(pots, effs) = expand(pot1) - val pots2 = pots.map { Outer(_, cls)(pot.source): Potential } - Summary(pots2, effs) - } - - case _: ThisRef | _: Fun | _: Warm | _: Cold => - Summary(pot) - - case SuperRef(pot1, supercls) => - val Summary(pots, effs) = expand(pot1) - val pots2 = pots.map { SuperRef(_, supercls)(pot.source): Potential } - Summary(pots2, effs) - } - } -} diff --git a/compiler/src/dotty/tools/dotc/transform/init/Effects.scala b/compiler/src/dotty/tools/dotc/transform/init/Effects.scala deleted file mode 100644 index ce9d8d8aa497..000000000000 --- a/compiler/src/dotty/tools/dotc/transform/init/Effects.scala +++ /dev/null @@ -1,80 +0,0 @@ -package dotty.tools.dotc -package transform -package init - -import ast.tpd._ -import reporting.trace -import config.Printers.init -import core.Types._ -import core.Symbols._ -import core.Contexts._ - -import Potentials._ - -object Effects { - type Effects = Vector[Effect] - val empty: Effects = Vector.empty - - def show(effs: Effects)(using Context): String = - effs.map(_.show).mkString(", ") - - /** Effects that are related to safe initialization performed on potentials */ - sealed trait Effect { - def potential: Potential - - def show(using Context): String - - def source: Tree - - def toEffs: Effects = Vector(this) - } - - /** A promotion effect means that a value that's possibly under initialization - * is promoted from the initializing world to the fully-initialized world. - * - * Essentially, this effect enforces that the object pointed to by - * `potential` is transitively initialized. - * - * This effect is trigger in several scenarios: - * - a potential is used as arguments to method calls or new-expressions - * - a potential is assigned (not initialize) to a field - * - the selection chain on a potential is too long - */ - case class Promote(potential: Potential)(val source: Tree) extends Effect { - 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 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 show(using Context): String = potential.show + "." + method.name.show + "!" - } - - // ------------------ operations on effects ------------------ - - def asSeenFrom(eff: Effect, thisValue: Potential)(implicit env: Env): Effect = - 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) - - case FieldAccess(pot, field) => - val pot1 = Potentials.asSeenFrom(pot, thisValue) - FieldAccess(pot1, field)(eff.source) - - case MethodCall(pot, sym) => - val pot1 = Potentials.asSeenFrom(pot, thisValue) - MethodCall(pot1, sym)(eff.source) - } } - - 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 deleted file mode 100644 index 02ff4aeef2cc..000000000000 --- a/compiler/src/dotty/tools/dotc/transform/init/Env.scala +++ /dev/null @@ -1,44 +0,0 @@ -package dotty.tools.dotc -package transform -package init - -import core._ -import Contexts._ -import Types._ -import Symbols._ -import Decorators._ - -import Effects._, Potentials._, Summary._ - -given theCtx(using Env): Context = summon[Env].ctx - -case class Env(ctx: Context, cache: Cache) { - private implicit def self: Env = this - - /** Can the method call be ignored? */ - def canIgnoreMethod(symbol: Symbol): Boolean = - !symbol.exists || // possible with outer selection, tests/init/crash/i1990b.scala - canIgnoreClass(symbol.owner) - - def canIgnoreClass(cls: Symbol): Boolean = - cls == defn.AnyClass || - cls == defn.AnyValClass || - cls == defn.ObjectClass - - def withCtx(newCtx: Context): Env = this.copy(ctx = newCtx) - - def withOwner(owner: Symbol) = this.copy(ctx = this.ctx.withOwner(owner)) - - /** Whether values of a given type is always fully initialized? - * - * It's true for primitive values - */ - def isAlwaysInitialized(tp: Type)(implicit env: Env): Boolean = { - val sym = tp.widen.finalResultType.typeSymbol - sym.isPrimitiveValueClass || sym == defn.StringClass - } - - def summaryOf(cls: ClassSymbol): ClassSummary = cache.summaryOf(cls) - - def resolveOuter(warm: Warm, cls: ClassSymbol): Potentials = cache.resolveOuter(warm, cls) -} diff --git a/compiler/src/dotty/tools/dotc/transform/init/Errors.scala b/compiler/src/dotty/tools/dotc/transform/init/Errors.scala index 143d53c9d2a7..57f860561d4a 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Errors.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Errors.scala @@ -9,8 +9,6 @@ import core._ import Decorators._, printing.SyntaxHighlighting import Types._, Symbols._, Contexts._ -import Effects._, Potentials._ - object Errors { type Errors = Seq[Error] val empty: Errors = Nil @@ -20,7 +18,7 @@ object Errors { sealed trait Error { def source: Tree - def trace: Vector[Tree] + def trace: Seq[Tree] def show(using Context): String def issue(using Context): Unit = @@ -60,7 +58,7 @@ object Errors { } /** Access non-initialized field */ - case class AccessNonInit(field: Symbol, trace: Vector[Tree]) extends Error { + case class AccessNonInit(field: Symbol, trace: Seq[Tree]) extends Error { def source: Tree = trace.last def show(using Context): String = "Access non-initialized " + field.show + "." @@ -70,40 +68,28 @@ object Errors { } /** Promote `this` under initialization to fully-initialized */ - case class PromoteThis(source: Tree, trace: Vector[Tree]) extends Error { - def show(using Context): String = "Promote the value under initialization to fully-initialized." - } - - /** Promote `this` under initialization to fully-initialized */ - case class PromoteWarm(source: Tree, trace: Vector[Tree]) extends Error { - def show(using Context): String = - "Promoting the value under initialization to fully-initialized." - } - - /** Promote a cold value under initialization to fully-initialized */ - case class PromoteCold(source: Tree, trace: Vector[Tree]) extends Error { - def show(using Context): String = - "Promoting the value " + source.show + " to fully-initialized while it is under initialization" + "." + case class PromoteError(msg: String, source: Tree, trace: Seq[Tree]) extends Error { + def show(using Context): String = "Promote the value under initialization to fully-initialized. " + msg } - case class AccessCold(field: Symbol, source: Tree, trace: Vector[Tree]) extends Error { + case class AccessCold(field: Symbol, source: Tree, trace: Seq[Tree]) extends Error { def show(using Context): String = "Access field " + source.show + " on a value with an unknown initialization status" + "." } - case class CallCold(meth: Symbol, source: Tree, trace: Vector[Tree]) extends Error { + case class CallCold(meth: Symbol, source: Tree, trace: Seq[Tree]) extends Error { def show(using Context): String = "Call method " + source.show + " on a value with an unknown initialization" + "." } - case class CallUnknown(meth: Symbol, source: Tree, trace: Vector[Tree]) extends Error { + case class CallUnknown(meth: Symbol, source: Tree, trace: Seq[Tree]) extends Error { def show(using Context): String = val prefix = if meth.is(Flags.Method) then "Calling the external method " else "Accessing the external field" prefix + meth.show + " may cause initialization errors" + "." } /** Promote a value under initialization to fully-initialized */ - case class UnsafePromotion(source: Tree, trace: Vector[Tree], errors: Errors) extends Error { + case class UnsafePromotion(msg: String, source: Tree, trace: Seq[Tree], errors: Errors) extends Error { assert(errors.nonEmpty) override def issue(using Context): Unit = @@ -111,7 +97,7 @@ object Errors { def show(using Context): String = { var index = 0 - "Promoting the value to fully-initialized is unsafe.\n" + stacktrace + + "Promoting the value to fully-initialized is unsafe. " + msg + "\n" + stacktrace + "\nThe unsafe promotion may cause the following problem(s):\n" + (errors.flatMap(_.flatten).map { error => index += 1 diff --git a/compiler/src/dotty/tools/dotc/transform/init/Potentials.scala b/compiler/src/dotty/tools/dotc/transform/init/Potentials.scala deleted file mode 100644 index 39eac29a2741..000000000000 --- a/compiler/src/dotty/tools/dotc/transform/init/Potentials.scala +++ /dev/null @@ -1,232 +0,0 @@ -package dotty.tools -package dotc -package transform -package init - -import scala.collection.mutable - -import ast.tpd._ -import reporting.trace -import config.Printers.init - -import core._ -import Types._, Symbols._, Contexts._ - -import Effects._, Summary._ - -object Potentials { - type Potentials = Vector[Potential] - val empty: Potentials = Vector.empty - - def show(pots: Potentials)(using Context): String = - pots.map(_.show).mkString(", ") - - /** A potential represents an aliasing of a value that is possibly under initialization */ - sealed trait Potential { - /** 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 - - def toPots: Potentials = Vector(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)) { - val cls = sym.owner.asClass - 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 - 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 - override def level: Int = pot.level - def show(using Context): String = pot.show + ".super[" + supercls.name.show + "]" - } - - /** A warm potential represents an object of which all fields are initialized, but it may contain - * reference to objects under initialization. - * - * @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 Refinable { - override def level: Int = 1 + outer.level - def show(using Context): String = "Warm[" + classSymbol.show + ", outer = " + outer.show + "]" - - 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 => unreachable() - } - } - } - - /** The Outer potential for `classSymbol` of the object `pot` - * - * It's only used internally for expansion of potentials. - * - * Note: Usage of `Type.baseType(cls)` may simplify the code. - * Current implementation avoids using complex type machinary, - * and may be potentially faster. - */ - 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.level - 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) - - override def size: Int = potential.size + 1 - override def level: Int = potential.level - def show(using Context): String = potential.show + "." + field.name.show - } - - /** The object returned by `this.m()` */ - case class MethodReturn(potential: Potential, method: Symbol)(val source: Tree) extends Potential { - assert(method != NoSymbol) - - override def size: Int = potential.size + 1 - override def level: Int = potential.level - 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 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 { - 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(";") + "]" - } - - // ------------------ operations on potentials ------------------ - - /** Selection on a set of potentials - * - * @param ignoreSelectEffect Where selection effects should be ignored - * - * During expansion of potentials, we ignore select effects and only care - * about promotion effects. This is because the selection effects have - * already been checked. - */ - extension (ps: Potentials) def select (symbol: Symbol, source: Tree, ignoreSelectEffect: 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 + Promote(pot)(pot.source) - else if (symbol.isConstructor) - val res = summary + pot - if ignoreSelectEffect then res + MethodCall(pot, symbol)(source) - else res - else if (symbol.isOneOf(Flags.Method | Flags.Lazy)) - val res = summary + MethodReturn(pot, symbol)(source) - if ignoreSelectEffect then res + MethodCall(pot, symbol)(source) - else res - else - val res = summary + FieldReturn(pot, symbol)(source) - if ignoreSelectEffect then res + FieldAccess(pot, symbol)(source) - else res - } - - 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, _.asInstanceOf[Potential].show) { - pot match { - case MethodReturn(pot1, sym) => - val pot = asSeenFrom(pot1, thisValue) - MethodReturn(pot, sym)(pot.source) - - case FieldReturn(pot1, sym) => - val pot = asSeenFrom(pot1, thisValue) - FieldReturn(pot, sym)(pot.source) - - case Outer(pot1, cls) => - val pot = asSeenFrom(pot1, thisValue) - Outer(pot, cls)(pot.source) - - case _: ThisRef => - thisValue - - case Fun(pots, effs) => - 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 = - if thisValue.level + outer2.level > 4 then - Cold()(outer2.source) - else - thisValue - - val outer3 = asSeenFrom(outer2, thisValue2) - Warm(cls, outer3)(pot.source) - - case _: Cold => - pot - - case SuperRef(potThis, supercls) => - val pot1 = asSeenFrom(potThis, thisValue) - SuperRef(pot1, supercls)(pot.source) - } - } - - 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/Semantic.scala b/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala index bd24cfbe5775..ccbdac812342 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala @@ -168,8 +168,6 @@ class Semantic { extension (trace: Trace) def add(node: Tree): Trace = trace :+ node - val noErrors = Nil - // ----- Operations on domains ----------------------------- extension (a: Value) def join(b: Value): Value = @@ -199,7 +197,7 @@ class Semantic { def select(field: Symbol, source: Tree): Contextual[Result] = value match { case Hot => - Result(Hot, noErrors) + Result(Hot, Errors.empty) case Cold => val error = AccessCold(field, source, trace) @@ -236,7 +234,7 @@ class Semantic { def call(meth: Symbol, superType: Type, source: Tree): Contextual[Result] = value match { case Hot => - Result(Hot, noErrors) + Result(Hot, Errors.empty) case Cold => val error = CallCold(meth, source, trace) @@ -306,7 +304,7 @@ class Semantic { def instantiate(klass: ClassSymbol, ctor: Symbol, source: Tree): Contextual[Result] = value match { case Hot => - Result(Hot, noErrors) + Result(Hot, Errors.empty) case Cold => val error = CallCold(ctor, source, trace) @@ -390,12 +388,12 @@ class Semantic { value match case Hot => Nil - case Cold => PromoteCold(source, trace) :: Nil + case Cold => PromoteError(msg, source, trace) :: Nil case thisRef: ThisRef => if heap.promotedValues.contains(thisRef) then Nil else if thisRef.canPromoteExtrinsic then Nil - else PromoteThis(source, trace) :: Nil + else PromoteError(msg, source, trace) :: Nil case warm: Warm => if heap.promotedValues.contains(warm) then Nil @@ -413,7 +411,7 @@ class Semantic { val res = eval(body, thisV, klass) val errors2 = res.value.promote(msg, source) if (res.errors.nonEmpty || errors2.nonEmpty) - UnsafePromotion(source, trace, res.errors ++ errors2) :: Nil + UnsafePromotion(msg, source, trace, res.errors ++ errors2) :: Nil else heap.promotedValues += fun Nil @@ -443,7 +441,7 @@ class Semantic { def tryPromote(msg: String, source: Tree): Contextual[List[Error]] = log("promote " + warm.show, printer) { val classRef = warm.klass.appliedRef if classRef.memberClasses.nonEmpty then - return PromoteWarm(source, trace) :: Nil + return PromoteError(msg, source, trace) :: Nil val fields = classRef.fields val methods = classRef.membersBasedOnFlags(Flags.Method, Flags.Deferred | Flags.Accessor) @@ -466,7 +464,7 @@ class Semantic { } if buffer.isEmpty then Nil - else UnsafePromotion(source, trace, buffer.toList) :: Nil + else UnsafePromotion(msg, source, trace, buffer.toList) :: Nil } end extension @@ -502,7 +500,7 @@ class Semantic { */ def eval(expr: Tree, thisV: Value, klass: ClassSymbol, cacheResult: Boolean = false): Contextual[Result] = log("evaluating " + expr.show + ", this = " + thisV.show, printer, res => res.asInstanceOf[Result].show) { val innerMap = cache.getOrElseUpdate(thisV, new EqHashMap[Tree, Value]) - if (innerMap.contains(expr)) Result(innerMap(expr), noErrors) + if (innerMap.contains(expr)) Result(innerMap(expr), Errors.empty) else { // no need to compute fix-point, because // 1. the result is decided by `cfg` for a legal program @@ -544,7 +542,7 @@ class Semantic { expr match { case Ident(nme.WILDCARD) => // TODO: disallow `var x: T = _` - Result(Hot, noErrors) + Result(Hot, Errors.empty) case id @ Ident(name) if !id.symbol.is(Flags.Method) => assert(name.isTermName, "type trees should not reach here") @@ -596,10 +594,10 @@ class Semantic { cases(expr.tpe, thisV, klass, expr) case Literal(_) => - Result(Hot, noErrors) + Result(Hot, Errors.empty) case Typed(expr, tpt) => - if (tpt.tpe.hasAnnotation(defn.UncheckedAnnot)) Result(Hot, noErrors) + if (tpt.tpe.hasAnnotation(defn.UncheckedAnnot)) Result(Hot, Errors.empty) else eval(expr, thisV, klass) ++ checkTermUsage(tpt, thisV, klass) case NamedArg(name, arg) => @@ -609,9 +607,9 @@ class Semantic { lhs match case Select(qual, _) => val res = eval(qual, thisV, klass) - eval(rhs, thisV, klass).ensureHot("May only assign initialized value", rhs) ++ res.errors + eval(rhs, thisV, klass).ensureHot("May only assign fully initialized value", rhs) ++ res.errors case id: Ident => - eval(rhs, thisV, klass).ensureHot("May only assign initialized value", rhs) + eval(rhs, thisV, klass).ensureHot("May only assign fully initialized value", rhs) case closureDef(ddef) => thisV match @@ -645,11 +643,11 @@ class Semantic { Result(value, errors) case Annotated(arg, annot) => - if (expr.tpe.hasAnnotation(defn.UncheckedAnnot)) Result(Hot, noErrors) + if (expr.tpe.hasAnnotation(defn.UncheckedAnnot)) Result(Hot, Errors.empty) else eval(arg, thisV, klass) case Match(selector, cases) => - val res1 = eval(selector, thisV, klass).ensureHot("The value to be matched needs to be initialized", selector) + val res1 = eval(selector, thisV, klass).ensureHot("The value to be matched needs to be fully initialized", selector) val ress = eval(cases.map(_.body), thisV, klass) val value = ress.map(_.value).join val errors = res1.errors ++ ress.flatMap(_.errors) @@ -678,7 +676,7 @@ class Semantic { case SeqLiteral(elems, elemtpt) => val ress = elems.map { elem => - eval(elem, thisV, klass).ensureHot("May only use initialized value as arguments", elem) + eval(elem, thisV, klass).ensureHot("May only use initialized value as method arguments", elem) } Result(Hot, ress.flatMap(_.errors)) @@ -688,7 +686,7 @@ class Semantic { case Thicket(List()) => // possible in try/catch/finally, see tests/crash/i6914.scala - Result(Hot, noErrors) + Result(Hot, Errors.empty) case vdef : ValDef => // local val definition @@ -697,11 +695,11 @@ class Semantic { case ddef : DefDef => // local method - Result(Hot, noErrors) + Result(Hot, Errors.empty) case tdef: TypeDef => // local type definition - if tdef.isClassDef then Result(Hot, noErrors) + if tdef.isClassDef then Result(Hot, Errors.empty) else Result(Hot, checkTermUsage(tdef.rhs, thisV, klass)) case tpl: Template => @@ -710,7 +708,7 @@ class Semantic { case _ => ??? // impossible case _: Import | _: Export => - Result(Hot, noErrors) + Result(Hot, Errors.empty) case _ => throw new Exception("unexpected tree: " + expr.show) @@ -720,23 +718,23 @@ class Semantic { def cases(tp: Type, thisV: Value, klass: ClassSymbol, source: Tree): Contextual[Result] = log("evaluating " + tp.show, printer, res => res.asInstanceOf[Result].show) { tp match { case _: ConstantType => - Result(Hot, noErrors) + Result(Hot, Errors.empty) case tmref: TermRef if tmref.prefix == NoPrefix => - Result(Hot, noErrors) + Result(Hot, Errors.empty) case tmref: TermRef => cases(tmref.prefix, thisV, klass, source).select(tmref.symbol, source) case tp @ ThisType(tref) => - if tref.symbol.is(Flags.Package) then Result(Hot, noErrors) + if tref.symbol.is(Flags.Package) then Result(Hot, Errors.empty) else val value = resolveThis(tref.classSymbol.asClass, thisV, klass, source) - Result(value, noErrors) + Result(value, Errors.empty) case _: TermParamRef | _: RecThis => // possible from checking effects of types - Result(Hot, noErrors) + Result(Hot, Errors.empty) case _ => throw new Exception("unexpected type: " + tp) @@ -774,7 +772,7 @@ class Semantic { if tref.prefix == NoPrefix then val enclosing = cls.owner.lexicallyEnclosingClass.asClass val outerV = resolveThis(enclosing, thisV, klass, source) - Result(outerV, noErrors) + Result(outerV, Errors.empty) else cases(tref.prefix, thisV, klass, source) diff --git a/compiler/src/dotty/tools/dotc/transform/init/Summarization.scala b/compiler/src/dotty/tools/dotc/transform/init/Summarization.scala deleted file mode 100644 index 8da415bf82bb..000000000000 --- a/compiler/src/dotty/tools/dotc/transform/init/Summarization.scala +++ /dev/null @@ -1,382 +0,0 @@ -package dotty.tools.dotc -package transform -package init - -import core._ -import Contexts._ -import Decorators._ -import StdNames._ -import Symbols._ -import Constants.Constant -import Types._ - -import ast.tpd._ -import config.Printers.init -import reporting.trace - -import Effects._, Potentials._, Summary._, Util._ - -object Summarization { - - /** Summarization of potentials and effects for an expression - * - * Optimization: - * - * 1. potentials for expression of primitive value types can be - * safely abandoned, as they are always fully initialized. - */ - def analyze(expr: Tree)(implicit env: Env): 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 = _` - Summary.empty - - case Ident(name) => - assert(name.isTermName, "type trees should not reach here") - analyze(expr.tpe, expr) - - case supert: Super => - analyze(supert.tpe, supert) - - case Select(qualifier, name) => - val Summary(pots, effs) = analyze(qualifier) - if (env.canIgnoreMethod(expr.symbol)) Summary(effs) - else if (!expr.symbol.exists) { // polymorphic function apply and structural types - Summary(pots.promote(expr) ++ effs) - } - else { - val Summary(pots2, effs2) = pots.select(expr.symbol, expr) - Summary(pots2, effs ++ effs2) - } - - case _: This => - analyze(expr.tpe, expr) - - case Apply(fun, args) => - val summary = analyze(fun) - val ignoredCall = env.canIgnoreMethod(expr.symbol) - - val argTps = fun.tpe.widen match - case mt: MethodType => mt.paramInfos - - val res = args.zip(argTps).foldLeft(summary) { case (sum, (arg, argTp)) => - val Summary(pots1, effs1) = analyze(arg) - if (ignoredCall) sum ++ effs1 - else if (argTp.isInstanceOf[ExprType]) sum + Promote(Fun(pots1, effs1)(arg))(arg) - else sum ++ pots1.promote(arg) ++ effs1 - } - - if (ignoredCall) Summary(res.effs) - else res - - case TypeApply(fun, args) => - analyze(fun) - - case Literal(const) => - Summary.empty - - case New(tpt) => - def typeRefOf(tp: Type): TypeRef = tp.dealias.typeConstructor match { - case tref: TypeRef => tref - case hklambda: HKTypeLambda => typeRefOf(hklambda.resType) - } - - val tref = typeRefOf(tpt.tpe) - val cls = tref.classSymbol.asClass - // local class may capture, thus we need to track it - if (tref.prefix == NoPrefix) { - val cur = theCtx.owner.lexicallyEnclosingClass.asClass - val thisRef = ThisRef()(expr) - val enclosing = cls.owner.lexicallyEnclosingClass.asClass - val summary = resolveThis(enclosing, thisRef, cur, expr) - if summary.pots.isEmpty then summary - else { - assert(summary.pots.size == 1) - summary.dropPotentials + Warm(cls, summary.pots.head)(expr) - } - } - else { - val summary = analyze(tref.prefix, expr) - if summary.pots.isEmpty then summary - else { - assert(summary.pots.size == 1) - summary.dropPotentials + Warm(cls, summary.pots.head)(expr) - } - } - - case Typed(expr, tpt) => - if (tpt.tpe.hasAnnotation(defn.UncheckedAnnot)) Summary.empty - else analyze(expr) ++ effectsOfType(tpt.tpe, tpt) - - case NamedArg(name, arg) => - analyze(arg) - - case Assign(lhs, rhs) => - val Summary(pots, effs) = analyze(rhs) - Summary(pots.promote(expr) ++ effs) - - case closureDef(ddef) => // must be before `Block` - 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).effs } - val Summary(pots2, effs2) = analyze(expr) - Summary(pots2, effs ++ effs2) - - case If(cond, thenp, elsep) => - 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 - else analyze(arg) - - case Match(selector, cases) => - // possible for switches - val Summary(pots, effs) = analyze(selector) - val init = Summary(Potentials.empty, pots.promote(selector) ++ effs) - cases.foldLeft(init) { (acc, cas) => - acc + analyze(cas.body) - } - - // case CaseDef(pat, guard, body) => - // Summary.empty - - case Return(expr, from) => - 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 Summary(_, effs1) = if (cond.isEmpty) Summary.empty else analyze(cond) - val Summary(_, effs2) = analyze(body) - Summary(effs1 ++ effs2) - - case Labeled(_, expr) => - val summary = analyze(expr) - summary.dropPotentials - - case Try(block, cases, finalizer) => - val Summary(pots, effs) = cases.foldLeft(analyze(block)) { (acc, cas) => - acc + analyze(cas.body) - } - 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 Summary(pots1, effs1) = analyze(elem) - pots1.promote(expr) ++ effs1 ++ effs - } - Summary(effsAll) - - case Inlined(call, bindings, expansion) => - val effs = bindings.foldLeft(Effects.empty) { (acc, mdef) => - acc ++ analyze(mdef).effs - } - analyze(expansion) ++ effs - - case vdef : ValDef => - val Summary(pots, effs) = analyze(vdef.rhs) - - if (vdef.symbol.owner.isClass) - if (vdef.symbol.is(Flags.Lazy)) Summary.empty else Summary(effs) - else - Summary(pots.promote(vdef) ++ effs) - - case Thicket(List()) => - // possible in try/catch/finally, see tests/crash/i6914.scala - Summary.empty - - case ddef : DefDef => - if (ddef.symbol.owner.isClass) Summary.empty - else { - val Summary(pots, effs) = analyze(ddef.rhs) - Summary(pots.promote(ddef) ++ effs) - } - - case tdef: TypeDef => - if tdef.isClassDef then Summary.empty - else Summary(effectsOfType(tdef.symbol.info, tdef.rhs)) - - case _: Import | _: Export => - Summary.empty - - case _ => - throw new Exception("unexpected tree: " + expr.show) - } - - if (env.isAlwaysInitialized(expr.tpe)) Summary(Potentials.empty, summary.effs) - else summary - } - - private def effectsOfType(tp: Type, source: Tree)(implicit env: Env): Effects = - var summary = Summary.empty - val traverser = new TypeTraverser { - def traverse(tp: Type): Unit = tp match { - case TermRef(_: SingletonType, _) => - summary = summary + analyze(tp, source) - case _ => - traverseChildren(tp) - } - } - traverser.traverse(tp) - summary.effs - - def analyze(tp: Type, source: Tree)(implicit env: Env): Summary = - trace("summarizing " + tp.show, init, s => s.asInstanceOf[Summary].show) { - val summary: Summary = tp match { - case _: ConstantType => - Summary.empty - - case tmref: TermRef if tmref.prefix == NoPrefix => - Summary.empty - - case tmref: TermRef => - val Summary(pots, effs) = analyze(tmref.prefix, source) - if (env.canIgnoreMethod(tmref.symbol)) Summary(effs) - else { - val summary = pots.select(tmref.symbol, source) - summary ++ effs - } - - 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 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 - } - Summary(pots2, effs) - - case _: TermParamRef | _: RecThis => - // possible from checking effects of types - Summary.empty - - case _ => - throw new Exception("unexpected type: " + tp) - } - - if (env.isAlwaysInitialized(tp)) Summary(Potentials.empty, summary.effs) - else summary - } - - def analyzeMethod(sym: Symbol)(implicit env: Env): Summary = { - val ddef = sym.defTree.asInstanceOf[DefDef] - traceIndented(sym.show + " = " + ddef.show, init) - analyze(ddef.rhs)(env.withOwner(sym)) - } - - def analyzeField(sym: Symbol)(implicit env: Env): Summary = { - val vdef = sym.defTree.asInstanceOf[ValDef] - 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 => s.asInstanceOf[Summary].show) { - if (cls.is(Flags.Package)) Summary.empty - 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)) - 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 => s.asInstanceOf[Summary].show) { - 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)).effs - - def parentArgEffsWithInit(stats: List[Tree], ctor: Symbol, source: Tree): Effects = - 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 - } - - val effsAll = tpl.parents.foldLeft(effs) { (effs, parent) => - effs ++ (parent match { - case tree @ Block(stats, parent) => - val ctor @ Select(qual, _) = funPart(parent) - parentArgEffsWithInit(qual :: stats ++ termArgss(parent).flatten, ctor.symbol, tree) - - case tree @ Apply(Block(stats, parent), args) => - val ctor @ Select(qual, _) = funPart(parent) - parentArgEffsWithInit(qual :: stats ++ args ++ termArgss(parent).flatten, ctor.symbol, tree) - - case parent : Apply => - val ctor @ Select(qual, _) = funPart(parent) - parentArgEffsWithInit(qual :: termArgss(parent).flatten, ctor.symbol, parent) - - case ref => - val tref: TypeRef = ref.tpe.typeConstructor.asInstanceOf - val cls = tref.classSymbol.asClass - if env.canIgnoreClass(cls) then Effects.empty - else { - val ctor = cls.primaryConstructor - val prefixEff = - if tref.prefix == NoPrefix then Effects.empty - else Summarization.analyze(tref.prefix, ref).effs - - prefixEff :+ MethodCall(ThisRef()(ref), ctor)(ref) - } - }) - } - - Summary(effsAll) - } - else { - val ddef = ctor.defTree.asInstanceOf[DefDef] - analyze(ddef.rhs)(env.withOwner(ctor)) - } - } - - 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)(env2).pots - else - parentCls -> analyze(cls.owner.lexicallyEnclosingClass.thisType, source)(env2).pots - } - - if (cls.defTree.isEmpty) - val source = { - implicit val ctx2: Context = theCtx.withSource(cls.source(using theCtx)) - TypeTree(cls.typeRef).withSpan(cls.span) - } - - val parentOuter = cls.info.parents.map { extractParentOuters(_, source) }.toMap - ClassSummary(cls, parentOuter) - else { - val tpl = cls.defTree.asInstanceOf[TypeDef] - val parents = tpl.rhs.asInstanceOf[Template].parents - - 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 deleted file mode 100644 index 1c7a852d4847..000000000000 --- a/compiler/src/dotty/tools/dotc/transform/init/Summary.scala +++ /dev/null @@ -1,92 +0,0 @@ -package dotty.tools.dotc -package transform -package init - -import scala.collection.mutable -import scala.annotation.targetName - -import core._ -import Contexts._ -import Symbols._ -import reporting.trace -import config.Printers.init - -import Potentials._, Effects._, Util._ - -case class Summary(pots: Potentials, effs: Effects) { - def +(summary2: Summary): Summary = - Summary(pots ++ summary2.pots, this.effs ++ summary2.effs) - - def +(pot: Potential): Summary = - Summary(pots :+ pot, effs) - - def +(eff: Effect): Summary = - Summary(pots, effs :+ eff) - - 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])" - } -} - -object Summary { - val empty: Summary = Summary(Potentials.empty, Effects.empty) - - def apply(pots: Potentials): Summary = empty ++ pots - - @targetName("withEffects") - def apply(effs: Effects): Summary = empty ++ effs - - def apply(pot: Potential): Summary = empty + pot - - def apply(eff: Effect): Summary = 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 - } - - def effectsOf(member: Symbol)(implicit env: Env): Effects = summaryOf(member).effs - def potentialsOf(member: Symbol)(implicit env: Env): Potentials = summaryOf(member).pots - - def show(using Context): String = - "ClassSummary(" + currentClass.name.show + - ", parents = " + parentOuter.map { case (k, v) => k.show + "->" + "[" + Potentials.show(v) + "]" } -} diff --git a/compiler/src/dotty/tools/dotc/transform/init/Util.scala b/compiler/src/dotty/tools/dotc/transform/init/Util.scala deleted file mode 100644 index 218a873be888..000000000000 --- a/compiler/src/dotty/tools/dotc/transform/init/Util.scala +++ /dev/null @@ -1,42 +0,0 @@ -package dotty.tools.dotc -package transform -package init - -import core._ -import Contexts._ -import Symbols._ -import config.Printers.Printer - -import annotation.tailrec - -object Util { - def traceIndented(msg: => String, printer: Printer)(using Context): Unit = - printer.println(s"${ctx.base.indentTab * ctx.base.indent} $msg") - - def traceOp(msg: => String, printer: Printer)(op: => Unit)(using Context): Unit = { - lazy val computedMsg = msg // Make sure we only compute msg once - traceIndented(s"==> ${computedMsg}", printer) - op - traceIndented(s"<== ${computedMsg}", printer) - } - - extension (symbol: Symbol) def hasSource(using Context): Boolean = - !symbol.defTree.isEmpty - - def resolve(cls: ClassSymbol, sym: Symbol)(using Context): Symbol = - if (sym.isEffectivelyFinal || sym.isConstructor) sym - else sym.matchingMember(cls.appliedRef) - - def resolveSuper(cls: ClassSymbol, superCls: ClassSymbol, sym: Symbol)(using Context): Symbol = { - // println(s"bases of $cls: " + cls.info.baseClasses) - @tailrec def loop(bcs: List[ClassSymbol]): Symbol = bcs match { - case bc :: bcs1 => - val cand = sym.matchingDecl(bcs.head, cls.thisType) - .suchThat(alt => !alt.is(Flags.Deferred)).symbol - if (cand.exists) cand else loop(bcs.tail) - case _ => - NoSymbol - } - loop(cls.info.baseClasses.dropWhile(sym.owner != _)) - } -} diff --git a/docs/docs/reference/other-new-features/safe-initialization.md b/docs/docs/reference/other-new-features/safe-initialization.md index 18cfeae62b63..8a8ed938f868 100644 --- a/docs/docs/reference/other-new-features/safe-initialization.md +++ b/docs/docs/reference/other-new-features/safe-initialization.md @@ -201,91 +201,29 @@ 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 (considered) +## Modularity -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. +The analysis takes the primary constructor of concrete classes as entry points. +It follows the constructors of super classes, which might be defined in another project. +The analysis takes advantage of TASTy for analyzing super classes defined in another 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: +The crossing of project boundary raises a concern about modularity. It is +well-known in object-oriented programming that superclass and subclass are +tightly coupled. For example, adding a method in the superclass requires +recompiling the child class for checking safe overriding. -```Scala -class Base: - private val map: mutable.Map[Int, String] = mutable.Map.empty - def enter(k: Int, v: String) = map(k) = v +Initialization is no exception in this respect. The initialization of an object +essentially invovles close interaction between subclass and superclass. If the +superclass is defined in another project, the crossing of project boundary +cannot be avoided for soundness of the analysis. -class Child extends Base: - enter(1, "one") - enter(2, "two") -``` - -However, when the class `Base` and `Child` are defined in two different -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 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, - directly or indirectly. - -2. The method call `o.m(args)` is forbidden if `o` is not transitively - initialized and the target of `m` is defined in an external project. - -3. The expression `new p.C(args)` is forbidden, if `p` is not transitively - initialized and `C` is defined in an external project. - -## Theory - -The theory is based on type-and-effect systems [2, 3]. We introduce two concepts, -_effects_ and _potentials_: - -``` -π = this | Warm(C, π) | π.f | π.m | π.super[D] | Cold | Fun(Π, Φ) | π.outer[C] -ϕ = π↑ | π.f! | π.m! -``` - -Potentials (π) represent values that are possibly under initialization. - -- `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 `π`. - -Effects are triggered from potentials: - -- `π↑`: promote the object pointed to by the potential `π` to fully-initialized -- `π.f!`: access field `f` on the potential `π` -- `π.m!`: call the method `m` on the potential `π` - -To ensure that the checking always terminate and for better -performance, we restrict the length of potentials to be finite (by -default 2). If the potential is too long, the checker stops -tracking it by checking that the potential is actually transitively -initialized. - -For an expression `e`, it may be summarized by the pair `(Π, Φ)`, -which means evaluation of `e` may produce the effects Φ and return the -potentials Π. Each field and method is associated with such a pair. -We call such a pair _summary_. The expansion of proxy potentials and effects, -such as `π.f`, `π.m` and `π.m!`, will take advantage of the summaries. -Depending on the potential `π` for `this`, the summaries need to be rebased (`asSeenFrom`) before usage. - -The checking treats the templates of concrete classes as entry points. -It maintains the set of initialized fields as initialization -progresses, and check that only initialized fields are accessed during -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. +Meanwhile, inheritance across project boundary has been under scrutiny and the +introduction of [open classes](./open-classes.html) mitigate the concern here. +For example, the initialization check could enforce that the constructors of +open classes may not contain method calls on `this` or introduce annotations as +a contract. -For a more detailed introduction of the theory, please refer to the paper _a type-and-effect system for safe initialization_ [3]. +The feedback from the community on the topic is welcome. ## Back Doors @@ -302,5 +240,4 @@ mark some fields as lazy. ## References 1. Fähndrich, M. and Leino, K.R.M., 2003, July. [_Heap monotonic typestates_](https://www.microsoft.com/en-us/research/publication/heap-monotonic-typestate/). In International Workshop on Aliasing, Confinement and Ownership in object-oriented programming (IWACO). -2. Lucassen, J.M. and Gifford, D.K., 1988, January. [_Polymorphic effect systems_](https://dl.acm.org/doi/10.1145/73560.73564). In Proceedings of the 15th ACM SIGPLAN-SIGACT symposium on Principles of programming languages (pp. 47-57). ACM. -3. Fengyun Liu, Ondřej Lhoták, Aggelos Biboudis, Paolo G. Giarrusso, and Martin Odersky. 2020. [_A type-and-effect system for object initialization_](https://dl.acm.org/doi/10.1145/3428243). OOPSLA, 2020. +2. Fengyun Liu, Ondřej Lhoták, Aggelos Biboudis, Paolo G. Giarrusso, and Martin Odersky. 2020. [_A type-and-effect system for object initialization_](https://dl.acm.org/doi/10.1145/3428243). OOPSLA, 2020. diff --git a/tests/init/neg/promotion-loop.check b/tests/init/neg/promotion-loop.check index 66e74da4de81..beba9a3d3197 100644 --- a/tests/init/neg/promotion-loop.check +++ b/tests/init/neg/promotion-loop.check @@ -1,9 +1,9 @@ -- Error: tests/init/neg/promotion-loop.scala:16:10 -------------------------------------------------------------------- 16 | println(b) // error | ^ - | Promoting the value to fully-initialized is unsafe. + |Promoting the value to fully-initialized is unsafe. May only use initialized value as arguments | - | The unsafe promotion may cause the following problem(s): + |The unsafe promotion may cause the following problem(s): | - | 1. Promote the value under initialization to fully-initialized. Calling trace: - | -> val outer = test [ promotion-loop.scala:12 ] + |1. Promote the value under initialization to fully-initialized. May only use initialized value as arguments Calling trace: + | -> val outer = test [ promotion-loop.scala:12 ] diff --git a/tests/init/neg/t3273.check b/tests/init/neg/t3273.check index 9598c274c9b2..4034a3cc0c38 100644 --- a/tests/init/neg/t3273.check +++ b/tests/init/neg/t3273.check @@ -1,7 +1,7 @@ -- Error: tests/init/neg/t3273.scala:4:42 ------------------------------------------------------------------------------ 4 | val num1: LazyList[Int] = 1 #:: num1.map(_ + 1) // error | ^^^^^^^^^^^^^^^ - | Promoting the value to fully-initialized is unsafe. + | Promoting the value to fully-initialized is unsafe. May only use initialized value as arguments | | The unsafe promotion may cause the following problem(s): | @@ -10,7 +10,7 @@ -- Error: tests/init/neg/t3273.scala:5:61 ------------------------------------------------------------------------------ 5 | val num2: LazyList[Int] = 1 #:: num2.iterator.map(_ + 1).to(LazyList) // error | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | Promoting the value to fully-initialized is unsafe. + | Promoting the value to fully-initialized is unsafe. May only use initialized value as arguments | | The unsafe promotion may cause the following problem(s): |