From fbff070cb2aaae1b73fe3b3223caca2a80f983c0 Mon Sep 17 00:00:00 2001 From: Liu Fengyun Date: Fri, 27 Nov 2020 15:04:23 +0100 Subject: [PATCH 01/16] Remove unused file --- .../dotc/transform/init/SetDefTree.scala | 33 ------------------- 1 file changed, 33 deletions(-) delete mode 100644 compiler/src/dotty/tools/dotc/transform/init/SetDefTree.scala diff --git a/compiler/src/dotty/tools/dotc/transform/init/SetDefTree.scala b/compiler/src/dotty/tools/dotc/transform/init/SetDefTree.scala deleted file mode 100644 index 8b8ceacc053f..000000000000 --- a/compiler/src/dotty/tools/dotc/transform/init/SetDefTree.scala +++ /dev/null @@ -1,33 +0,0 @@ -package dotty.tools.dotc -package transform -package init - -import MegaPhase._ -import ast.tpd -import core.Contexts._ - -/** Set the `defTree` property of symbols */ -class SetDefTree extends MiniPhase { - import tpd._ - - override val phaseName: String = SetDefTree.name - override val runsAfter = Set(Pickler.name) - - override def isEnabled(using Context): Boolean = - super.isEnabled && ctx.settings.YcheckInit.value - - override def runOn(units: List[CompilationUnit])(using Context): List[CompilationUnit] = { - val ctx2 = ctx.fresh.setSetting(ctx.settings.YretainTrees, true) - super.runOn(units)(using ctx2) - } - - override def transformValDef(tree: ValDef)(using Context): Tree = tree.setDefTree - - override def transformDefDef(tree: DefDef)(using Context): Tree = tree.setDefTree - - override def transformTypeDef(tree: TypeDef)(using Context): Tree = tree.setDefTree -} - -object SetDefTree { - val name: String = "SetDefTree" -} From 081aa78eb9b6978852dea112df4fdc478e912ec1 Mon Sep 17 00:00:00 2001 From: Liu Fengyun Date: Fri, 27 Nov 2020 23:08:11 +0100 Subject: [PATCH 02/16] Refactor check - Make summary a proper class - Rename isInternal to hasSource - Refactor: remove useless parameter - Split effect check into multiple methods - Refactor ignored methods handling Ignore constructor call on Any, Object and AnyVal - Fix missing return --- .../tools/dotc/transform/init/Checking.scala | 333 ++++++++++-------- .../dotty/tools/dotc/transform/init/Env.scala | 19 +- .../tools/dotc/transform/init/Errors.scala | 10 +- .../dotc/transform/init/Potentials.scala | 72 ++-- .../dotc/transform/init/Summarization.scala | 157 +++++---- .../tools/dotc/transform/init/Summary.scala | 120 ++++--- .../tools/dotc/transform/init/Util.scala | 2 +- .../dotty/tools/dotc/CompilationTests.scala | 21 +- .../other-new-features/safe-initialization.md | 20 +- tests/init/full/neg/global-cycle1.scala | 10 + tests/init/full/neg/global-cycle2.scala | 7 + tests/init/full/neg/global-cycle3.scala | 7 + tests/init/full/neg/global-cycle4.scala | 19 + tests/init/{ => full}/neg/hybrid2.scala | 0 tests/init/full/neg/i9176.scala | 9 + tests/init/neg/inner-loop.scala | 2 +- tests/init/neg/inner1.scala | 2 +- tests/init/neg/inner11.scala | 1 + tests/init/neg/inner17.scala | 2 +- tests/init/neg/inner19.scala | 2 +- tests/init/neg/promote.scala | 9 + tests/init/neg/t3273.check | 4 +- 22 files changed, 457 insertions(+), 371 deletions(-) create mode 100644 tests/init/full/neg/global-cycle1.scala create mode 100644 tests/init/full/neg/global-cycle2.scala create mode 100644 tests/init/full/neg/global-cycle3.scala create mode 100644 tests/init/full/neg/global-cycle4.scala rename tests/init/{ => full}/neg/hybrid2.scala (100%) create mode 100644 tests/init/full/neg/i9176.scala create mode 100644 tests/init/neg/promote.scala diff --git a/compiler/src/dotty/tools/dotc/transform/init/Checking.scala b/compiler/src/dotty/tools/dotc/transform/init/Checking.scala index b08b95581c24..4f81a02f7ed7 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Checking.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Checking.scala @@ -29,8 +29,9 @@ object Checking { * } * */ + case class State( - private var visited: Set[Effect], // effects that have been checked + 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], @@ -38,26 +39,43 @@ object Checking { safePromoted: mutable.Set[Potential], // Potentials that can be safely promoted env: Env ) { - - def withVisited(eff: Effect): State = { - visited = visited + eff - copy(path = this.path :+ eff.source) - } - - def hasVisited(eff: Effect): Boolean = - visited.contains(eff) - def withOwner(sym: Symbol): State = copy(env = env.withOwner(sym)) def test(op: State ?=> Errors): Errors = { - val saved = visited + val savedVisited = visited val errors = op(using this) - visited = saved + visited = savedVisited errors } } - private implicit def theEnv(implicit state: State): Env = state.env + 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.visited = state.visited + eff + val state2: State = state.copy(path = state.path :+ eff.source) + eff match { + case eff: Promote => Checking.checkPromote(eff)(using state2) + case eff: FieldAccess => Checking.checkFieldAccess(eff)(using state2) + case eff: MethodCall => Checking.checkMethodCall(eff)(using state2) + } + } + } + } + + 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 * @@ -65,7 +83,7 @@ object Checking { * However, summarization can be done lazily on-demand to improve * performance. */ - def checkClassBody(cdef: TypeDef)(implicit state: State): Unit = { + def checkClassBody(cdef: TypeDef)(using state: State): Unit = { traceIndented("\n\n>>>> checking " + cdef.symbol.show, init) val cls = cdef.symbol.asClass @@ -74,20 +92,20 @@ object Checking { // mark current class as initialized, required for linearization state.parentsInited += cls - def checkClassBodyStat(tree: Tree)(implicit state: State): Unit = traceOp("checking " + tree.show, init) { + def checkClassBodyStat(tree: Tree)(using state: State): Unit = traceOp("checking " + tree.show, init) { tree match { case vdef : ValDef => - val (pots, effs) = Summarization.analyze(vdef.rhs) - theEnv.summaryOf(cls).cacheFor(vdef.symbol, (pots, effs)) + val summary = Summarization.analyze(vdef.rhs) + theEnv.summaryOf(cls).cacheFor(vdef.symbol, summary) if (!vdef.symbol.is(Flags.Lazy)) { - checkEffectsIn(effs, cls) + checkEffects(summary.effs) traceIndented(vdef.symbol.show + " initialized", init) state.fieldsInited += vdef.symbol } case tree => - val (_, effs) = Summarization.analyze(tree) - checkEffectsIn(effs, cls) + val summary = Summarization.analyze(tree) + checkEffects(summary.effs) } } @@ -95,16 +113,17 @@ object Checking { // see spec 5.1 about "Template Evaluation". // https://www.scala-lang.org/files/archive/spec/2.13/05-classes-and-objects.html - def checkConstructor(ctor: Symbol, tp: Type, source: Tree)(implicit state: State): Unit = traceOp("checking " + ctor.show, init) { + def checkConstructor(ctor: Symbol, tp: Type, source: Tree)(using state: State): Unit = traceOp("checking " + ctor.show, init) { val cls = ctor.owner val classDef = cls.defTree if (!classDef.isEmpty) { - if (ctor.isPrimaryConstructor) checkClassBody(classDef.asInstanceOf[TypeDef])(state.withOwner(cls)) - else checkSecondaryConstructor(ctor)(state.withOwner(cls)) + given State = state.withOwner(cls) + if (ctor.isPrimaryConstructor) checkClassBody(classDef.asInstanceOf[TypeDef]) + else checkSecondaryConstructor(ctor) } } - def checkSecondaryConstructor(ctor: Symbol)(implicit state: State): Unit = traceOp("checking " + ctor.show, init) { + def checkSecondaryConstructor(ctor: Symbol)(using state: State): Unit = traceOp("checking " + ctor.show, init) { val Block(ctorCall :: stats, expr) = ctor.defTree.asInstanceOf[DefDef].rhs val cls = ctor.owner.asClass @@ -116,12 +135,15 @@ object Checking { checkSecondaryConstructor(ctor) } - (stats :+ expr).foreach { stat => - val (_, effs) = Summarization.analyze(stat)(theEnv.withOwner(ctor)) - checkEffectsIn(effs, cls) - } + 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) @@ -149,107 +171,95 @@ object Checking { tpl.body.foreach { checkClassBodyStat(_) } } - private def checkEffectsIn(effs: Effects, cls: ClassSymbol)(implicit state: State): Unit = traceOp("checking effects " + Effects.show(effs), init) { - for { - eff <- effs - error <- check(eff) - } error.issue - } - - private def check(eff: Effect)(implicit state: State): Errors = - if (state.hasVisited(eff)) Errors.empty - else trace("checking effect " + eff.show, init, errs => Errors.show(errs.asInstanceOf[Errors])) { - implicit val state2: State = state.withVisited(eff) - - eff match { - case Promote(pot) => checkPromote(pot, eff.source) - - case FieldAccess(pot, field) => - - 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, state2.path).toErrors - else Errors.empty - - case SuperRef(_: ThisRef, supercls) => - val target = resolveSuper(state.thisClass, supercls, field) - if (target.is(Flags.Lazy)) check(MethodCall(pot, target)(eff.source)) - else if (!state.fieldsInited.contains(target)) AccessNonInit(target, state2.path).toErrors - else Errors.empty - - case 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 + 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 _: Cold => - AccessCold(field, eff.source, state2.path).toErrors + case warm @ Warm(cls, outer) => + val target = resolve(cls, sym) - case Fun(pots, effs) => - throw new Exception("Unexpected effect " + eff.show) + 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 pot => - val (pots, effs) = expand(pot) - val effs2 = pots.map(FieldAccess(_, field)(eff.source)) - (effs2 ++ effs).flatMap(check(_)) + 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 MethodCall(pot, sym) => - pot match { - case thisRef: ThisRef => - val target = resolve(state.thisClass, sym) - if (!target.isOneOf(Flags.Method | Flags.Lazy)) - check(FieldAccess(pot, target)(eff.source)) - else if (target.isInternal) { - val effs = thisRef.effectsOf(target) - effs.flatMap { check(_) } - } - else CallUnknown(target, eff.source, state2.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.isInternal) { - val effs = thisRef.effectsOf(target) - effs.flatMap { check(_) } - } - else CallUnknown(target, eff.source, state2.path).toErrors + case pot => + val Summary(pots, effs) = expand(pot) + val effs2 = pots.map(MethodCall(_, sym)(eff.source)) + (effs2 ++ effs).toList.flatMap(check(_)) + } - case warm @ Warm(cls, outer) => - val target = resolve(cls, sym) + 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(_)) - if (target.isInternal) { - val effs = warm.effectsOf(target) - effs.flatMap { check(_) } - } - else if (!sym.isConstructor) CallUnknown(target, eff.source, state2.path).toErrors - else Errors.empty - - case _: Cold => - CallCold(sym, eff.source, state2.path).toErrors - - case Fun(pots, effs) => - // TODO: assertion might be false, due to SAM - if (sym.name.toString == "apply") effs.flatMap { check(_) } - else Errors.empty - // curried, tupled, toString are harmless - - case pot => - val (pots, effs) = expand(pot) - val effs2 = pots.map(MethodCall(_, sym)(eff.source)) - (effs2 ++ effs).flatMap(check(_)) - } - } } - private def checkPromote(pot: Potential, source: Tree)(implicit state: State): Errors = - if (state.safePromoted.contains(pot)) Errors.empty - else + + private def checkPromote(eff: Promote)(using state: State): Errors = + if (state.safePromoted.contains(eff.potential)) Errors.empty + else { + val pot = eff.potential val errs = pot match { case pot: ThisRef => // If we have all fields initialized, then we can promote This to hot. @@ -261,100 +271,111 @@ object Checking { if (allFieldsInited) Errors.empty else - PromoteThis(pot, source, state.path).toErrors + PromoteThis(pot, eff.source, state.path).toErrors + case _: Cold => - PromoteCold(source, state.path).toErrors + PromoteCold(eff.source, state.path).toErrors case pot @ Warm(cls, outer) => - val errors = state.test { checkPromote(outer, source) } + val errors = state.test { checkPromote(Promote(outer)(eff.source)) } if (errors.isEmpty) Errors.empty - else PromoteWarm(pot, source, state.path).toErrors + else PromoteWarm(pot, eff.source, state.path).toErrors case Fun(pots, effs) => - val errs1 = state.test { effs.flatMap { check(_) } } - val errs2 = state.test { pots.flatMap { pot => checkPromote(pot, source)(state.copy(path = Vector.empty)) } } + 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(pot, source, state.path, errs1 ++ errs2).toErrors + UnsafePromotion(pot, eff.source, state.path, errs1 ++ errs2).toErrors else Errors.empty case pot => - val (pots, effs) = expand(pot) - val effs2 = pots.map(Promote(_)(source)) - (effs2 ++ effs).flatMap(check(_)) + 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)(implicit state: State): Summary = trace("expand " + pot.show, init, sum => Summary.show(sum.asInstanceOf[Summary])) { + 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.isInternal) (thisRef.potentialsOf(target), Effects.empty) + if (target.hasSource) Summary(thisRef.potentialsOf(target), Effects.empty) else Summary.empty // warning already issued in call effect case SuperRef(thisRef: ThisRef, supercls) => val target = resolveSuper(state.thisClass, supercls, sym) - if (target.isInternal) (thisRef.potentialsOf(target), Effects.empty) + 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") (pots, Effects.empty) - else if (name == "tupled") (Set(pot1), Effects.empty) + if (name == "apply") Summary(pots, Effects.empty) + else if (name == "tupled") Summary(Set(pot1), Effects.empty) else if (name == "curried") { val arity = defn.functionArity(sym.info.finalResultType) - val pots = (1 until arity).foldLeft(Set(pot1)) { (acc, i) => Set(Fun(acc, Effects.empty)(pot1.source)) } - (pots, Effects.empty) + val pots = (1 until arity).foldLeft(Set(pot1)) { (acc, i) => + Set(Fun(acc, Effects.empty)(pot1.source)) + } + Summary(pots, Effects.empty) } else Summary.empty case warm : Warm => val target = resolve(warm.classSymbol, sym) - if (target.isInternal) (warm.potentialsOf(target), Effects.empty) + if (target.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 (pots, effs) = expand(pot1) - val (pots2, effs2) = pots.select(sym, pot.source) - (pots2, effs ++ effs2) + val Summary(pots, effs) = expand(pot1) + val Summary(pots2, effs2) = pots.select(sym, pot.source) + Summary(pots2, effs ++ effs2) } case FieldReturn(pot1, sym) => pot1 match { case thisRef: ThisRef => val target = resolve(state.thisClass, sym) - if (sym.isInternal) (thisRef.potentialsOf(target), Effects.empty) - else (Cold()(pot.source).toPots, Effects.empty) + if (sym.hasSource) Summary(thisRef.potentialsOf(target), Effects.empty) + else Summary(Cold()(pot.source)) case SuperRef(thisRef: ThisRef, supercls) => val target = resolveSuper(state.thisClass, supercls, sym) - if (target.isInternal) (thisRef.potentialsOf(target), Effects.empty) - else (Cold()(pot.source).toPots, Effects.empty) + 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.isInternal) (warm.potentialsOf(target), Effects.empty) - else (Cold()(pot.source).toPots, Effects.empty) + if (target.hasSource) Summary(warm.potentialsOf(target), Effects.empty) + else Summary(Cold()(pot.source)) case _: Cold => Summary.empty // error already reported, ignore case _ => - val (pots, effs) = expand(pot1) - val (pots2, effs2) = pots.select(sym, pot.source) - (pots2, effs ++ effs2) + val Summary(pots, effs) = expand(pot1) + val Summary(pots2, effs2) = pots.select(sym, pot.source) + Summary(pots2, effs ++ effs2) } case Outer(pot1, cls) => @@ -367,21 +388,21 @@ object Checking { throw new Exception("Unexpected code reached") case warm: Warm => - (warm.resolveOuter(cls), Effects.empty) + Summary(warm.resolveOuter(cls)) case _ => - val (pots, effs) = expand(pot1) + val Summary(pots, effs) = expand(pot1) val pots2 = pots.map { Outer(_, cls)(pot.source): Potential } - (pots2, effs) + Summary(pots2, effs) } case _: ThisRef | _: Fun | _: Warm | _: Cold => - (Set(pot), Effects.empty) + Summary(pot) case SuperRef(pot1, supercls) => - val (pots, effs) = expand(pot1) + val Summary(pots, effs) = expand(pot1) val pots2 = pots.map { SuperRef(_, supercls)(pot.source): Potential } - (pots2, effs) + Summary(pots2, effs) } } } diff --git a/compiler/src/dotty/tools/dotc/transform/init/Env.scala b/compiler/src/dotty/tools/dotc/transform/init/Env.scala index cd36dc1d867d..3dc522d097f2 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Env.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Env.scala @@ -18,19 +18,20 @@ import scala.collection.mutable import Effects._, Potentials._, Summary._ -implicit def theCtx(implicit env: Env): Context = env.ctx +given theCtx(using Env): Context = summon[Env].ctx case class Env(ctx: Context) { private implicit def self: Env = this - // Methods that should be ignored in the checking - lazy val ignoredMethods: Set[Symbol] = Set( - defn.Any_getClass, - defn.Any_isInstanceOf, - defn.Object_eq, - defn.Object_ne, - defn.Object_synchronized - ) + /** Can the method call be ignored? */ + def canIgnoreMethod(symbol: Symbol): Boolean = + !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) diff --git a/compiler/src/dotty/tools/dotc/transform/init/Errors.scala b/compiler/src/dotty/tools/dotc/transform/init/Errors.scala index b0c9b4c6e29c..73b8cd123033 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Errors.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Errors.scala @@ -12,8 +12,8 @@ import Types._, Symbols._, Contexts._ import Effects._, Potentials._ object Errors { - type Errors = Set[Error] - val empty: Errors = Set.empty + type Errors = List[Error] + val empty: Errors = Nil def show(errs: Errors)(using Context): String = errs.map(_.show).mkString(", ") @@ -26,7 +26,7 @@ object Errors { def issue(using Context): Unit = report.warning(show + stacktrace, source.srcPos) - def toErrors: Errors = Set(this) + def toErrors: Errors = this :: Nil def stacktrace(using Context): String = if (trace.isEmpty) "" else " Calling trace:\n" + { var indentCount = 0 @@ -55,7 +55,7 @@ object Errors { */ def flatten: Errors = this match { case unsafe: UnsafePromotion => unsafe.errors.flatMap(_.flatten) - case _ => Set(this) + case _ => this :: Nil } } @@ -63,7 +63,7 @@ object Errors { case class AccessNonInit(field: Symbol, trace: Vector[Tree]) extends Error { def source: Tree = trace.last def show(using Context): String = - "Access non-initialized field " + field.name.show + "." + "Access non-initialized " + field.show + "." override def issue(using Context): Unit = report.warning(show + stacktrace, field.srcPos) diff --git a/compiler/src/dotty/tools/dotc/transform/init/Potentials.scala b/compiler/src/dotty/tools/dotc/transform/init/Potentials.scala index b6ee6fb9dfca..b98811a15f65 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Potentials.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Potentials.scala @@ -33,25 +33,37 @@ object Potentials { def source: Tree } - /** The object pointed by `this` */ - case class ThisRef()(val source: Tree) extends Potential { - def show(using Context): String = "this" - + sealed trait Refinable extends Potential { /** Effects of a method call or a lazy val access + * + * The method performs prefix substitution */ def effectsOf(sym: Symbol)(implicit env: Env): Effects = trace("effects of " + sym.show, init, r => Effects.show(r.asInstanceOf)) { val cls = sym.owner.asClass - env.summaryOf(cls).effectsOf(sym) + val effs = env.summaryOf(cls).effectsOf(sym) + this match + case _: ThisRef => effs + case _ => Effects.asSeenFrom(effs, this) } /** Potentials of a field, a method call or a lazy val access + * + * The method performs prefix substitution */ def potentialsOf(sym: Symbol)(implicit env: Env): Potentials = trace("potentials of " + sym.show, init, r => Potentials.show(r.asInstanceOf)) { val cls = sym.owner.asClass - env.summaryOf(cls).potentialsOf(sym) + val pots = env.summaryOf(cls).potentialsOf(sym) + this match + case _: ThisRef => pots + case _ => Potentials.asSeenFrom(pots, this) } } + /** The object pointed by `this` */ + case class ThisRef()(val source: Tree) extends Refinable { + def show(using Context): String = "this" + } + /** The object pointed by `C.super.this`, mainly used for override resolution */ case class SuperRef(pot: Potential, supercls: ClassSymbol)(val source: Tree) extends Potential { override def size: Int = pot.size @@ -65,30 +77,10 @@ object Potentials { * @param classSymbol The concrete class of the object * @param outer The potential for `this` of the enclosing class */ - case class Warm(classSymbol: ClassSymbol, outer: Potential)(val source: Tree) extends Potential { + case class Warm(classSymbol: ClassSymbol, outer: Potential)(val source: Tree) extends Refinable { override def level: Int = 1 + outer.level def show(using Context): String = "Warm[" + classSymbol.show + ", outer = " + outer.show + "]" - /** Effects of a method call or a lazy val access - * - * The method performs prefix substitution - */ - def effectsOf(sym: Symbol)(implicit env: Env): Effects = trace("effects of " + sym.show, init, r => Effects.show(r.asInstanceOf)) { - val cls = sym.owner.asClass - val effs = env.summaryOf(cls).effectsOf(sym) - Effects.asSeenFrom(effs, this) - } - - /** Potentials of a field, a method call or a lazy val access - * - * The method performs prefix substitution - */ - def potentialsOf(sym: Symbol)(implicit env: Env): Potentials = trace("potentials of " + sym.show, init, r => Potentials.show(r.asInstanceOf)) { - val cls = sym.owner.asClass - val pots = env.summaryOf(cls).potentialsOf(sym) - Potentials.asSeenFrom(pots, this) - } - def resolveOuter(cls: ClassSymbol)(implicit env: Env): Potentials = env.resolveOuter(this, cls) } @@ -118,7 +110,7 @@ object Potentials { case class Outer(pot: Potential, classSymbol: ClassSymbol)(val source: Tree) extends Potential { // be lenient with size of outer selection, no worry for non-termination override def size: Int = pot.size - override def level: Int = pot.size + override def level: Int = pot.level def show(using Context): String = pot.show + ".outer[" + classSymbol.show + "]" } @@ -127,7 +119,7 @@ object Potentials { assert(field != NoSymbol) override def size: Int = potential.size + 1 - override def level: Int = potential.size + override def level: Int = potential.level def show(using Context): String = potential.show + "." + field.name.show } @@ -136,7 +128,7 @@ object Potentials { assert(method != NoSymbol) override def size: Int = potential.size + 1 - override def level: Int = potential.size + override def level: Int = potential.level def show(using Context): String = potential.show + "." + method.name.show } @@ -164,20 +156,20 @@ object Potentials { extension (pot: Potential) def toPots: Potentials = Potentials.empty + pot extension (ps: Potentials) def select (symbol: Symbol, source: Tree)(using Context): Summary = - ps.foldLeft(Summary.empty) { case ((pots, effs), pot) => + ps.foldLeft(Summary.empty) { case (Summary(pots, effs), pot) => // max potential length // TODO: it can be specified on a project basis via compiler options if (pot.size > 2) - (pots, effs + Promote(pot)(source)) + summary + Promote(pot)(pot.source) else if (symbol.isConstructor) - (pots + pot, effs + MethodCall(pot, symbol)(source)) + Summary(pots + pot, effs + MethodCall(pot, symbol)(source)) else if (symbol.isOneOf(Flags.Method | Flags.Lazy)) - ( + Summary( pots + MethodReturn(pot, symbol)(source), effs + MethodCall(pot, symbol)(source) ) else - (pots + FieldReturn(pot, symbol)(source), effs + FieldAccess(pot, symbol)(source)) + Summary(pots + FieldReturn(pot, symbol)(source), effs + FieldAccess(pot, symbol)(source)) } extension (ps: Potentials) def promote(source: Tree): Effects = ps.map(Promote(_)(source)) @@ -206,13 +198,11 @@ object Potentials { case Warm(cls, outer2) => // widening to terminate - val thisValue2 = thisValue match { - case Warm(cls, outer) if outer.level > 2 => - Warm(cls, Cold()(outer2.source))(thisValue.source) - - case _ => + val thisValue2 = + if thisValue.level + outer2.level > 4 then + Cold()(outer2.source) + else thisValue - } val outer3 = asSeenFrom(outer2, thisValue2) Warm(cls, outer3)(pot.source) diff --git a/compiler/src/dotty/tools/dotc/transform/init/Summarization.scala b/compiler/src/dotty/tools/dotc/transform/init/Summarization.scala index 6597011f4eee..1ae2822a0c18 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Summarization.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Summarization.scala @@ -26,7 +26,7 @@ object Summarization { * safely abandoned, as they are always fully initialized. */ def analyze(expr: Tree)(implicit env: Env): Summary = - trace("summarizing " + expr.show, init, s => Summary.show(s.asInstanceOf[Summary])) { + trace("summarizing " + expr.show, init, s => s.asInstanceOf[Summary].show) { val summary: Summary = expr match { case Ident(nme.WILDCARD) => // TODO: disallow `var x: T = _` @@ -40,14 +40,14 @@ object Summarization { analyze(supert.tpe, supert) case Select(qualifier, name) => - val (pots, effs) = analyze(qualifier) - if (env.ignoredMethods.contains(expr.symbol)) (Potentials.empty, effs) + val Summary(pots, effs) = analyze(qualifier) + if (env.canIgnoreMethod(expr.symbol)) Summary(effs) else if (!expr.symbol.exists) { // polymorphic function apply and structural types - (Potentials.empty, pots.promote(expr) ++ effs) + Summary(pots.promote(expr) ++ effs) } else { - val (pots2, effs2) = pots.select(expr.symbol, expr) - (pots2, effs ++ effs2) + val Summary(pots2, effs2) = pots.select(expr.symbol, expr) + Summary(pots2, effs ++ effs2) } case _: This => @@ -55,19 +55,19 @@ object Summarization { case Apply(fun, args) => val summary = analyze(fun) - val ignoredCall = env.ignoredMethods.contains(expr.symbol) + val ignoredCall = env.canIgnoreMethod(expr.symbol) val argTps = fun.tpe.widen match case mt: MethodType => mt.paramInfos val res = args.zip(argTps).foldLeft(summary) { case (sum, (arg, argTp)) => - val (pots1, effs1) = analyze(arg) - if (ignoredCall) sum.withEffs(effs1) + val Summary(pots1, effs1) = analyze(arg) + if (ignoredCall) sum ++ effs1 else if (argTp.isInstanceOf[ExprType]) sum + Promote(Fun(pots1, effs1)(arg))(arg) - else sum.withEffs(pots1.promote(arg) ++ effs1) + else sum ++ pots1.promote(arg) ++ effs1 } - if (ignoredCall) (Potentials.empty, res._2) + if (ignoredCall) Summary(res.effs) else res case TypeApply(fun, args) => @@ -89,19 +89,19 @@ object Summarization { val cur = theCtx.owner.lexicallyEnclosingClass.asClass val thisRef = ThisRef()(expr) val enclosing = cls.owner.lexicallyEnclosingClass.asClass - val (pots, effs) = resolveThis(enclosing, thisRef, cur, expr) - if pots.isEmpty then (Potentials.empty, effs) + val summary = resolveThis(enclosing, thisRef, cur, expr) + if summary.pots.isEmpty then summary else { - assert(pots.size == 1) - (Warm(cls, pots.head)(expr).toPots, effs) + assert(summary.pots.size == 1) + summary.dropPotentials + Warm(cls, summary.pots.head)(expr) } } else { - val (pots, effs) = analyze(tref.prefix, expr) - if (pots.isEmpty) Summary.empty.withEffs(effs) + val summary = analyze(tref.prefix, expr) + if summary.pots.isEmpty then summary else { - assert(pots.size == 1) - (Warm(cls, pots.head)(expr).toPots, effs) + assert(summary.pots.size == 1) + summary.dropPotentials + Warm(cls, summary.pots.head)(expr) } } @@ -113,23 +113,23 @@ object Summarization { analyze(arg) case Assign(lhs, rhs) => - val (pots, effs) = analyze(rhs) - (Potentials.empty, pots.promote(expr) ++ effs) + val Summary(pots, effs) = analyze(rhs) + Summary(pots.promote(expr) ++ effs) case closureDef(ddef) => // must be before `Block` - val (pots, effs) = analyze(ddef.rhs) - Summary.empty + Fun(pots, effs)(expr) + val Summary(pots, effs) = analyze(ddef.rhs) + Summary(Fun(pots, effs)(expr)) case Block(stats, expr) => - val effs = stats.foldLeft(Effects.empty) { (acc, stat) => acc ++ analyze(stat)._2 } - val (pots2, effs2) = analyze(expr) - (pots2, effs ++ effs2) + val 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 (pots0, effs0) = analyze(cond) - val (pots1, effs1) = analyze(thenp) - val (pots2, effs2) = analyze(elsep) - (pots0 ++ pots1 ++ pots2, effs0 ++ effs1 ++ effs2) + val Summary(_, effs0) = analyze(cond) + val Summary(pots1, effs1) = analyze(thenp) + val Summary(pots2, effs2) = analyze(elsep) + Summary(pots1 ++ pots2, effs0 ++ effs1 ++ effs2) case Annotated(arg, annot) => if (expr.tpe.hasAnnotation(defn.UncheckedAnnot)) Summary.empty @@ -137,8 +137,9 @@ object Summarization { case Match(selector, cases) => // possible for switches - val (pots, effs) = analyze(selector) - cases.foldLeft((Potentials.empty, pots.promote(selector) ++ effs)) { (acc, cas) => + val Summary(pots, effs) = analyze(selector) + val init = Summary(Potentials.empty, pots.promote(selector) ++ effs) + cases.foldLeft(init) { (acc, cas) => acc union analyze(cas.body) } @@ -146,54 +147,57 @@ object Summarization { // Summary.empty case Return(expr, from) => - val (pots, effs) = analyze(expr) - (Potentials.empty, effs ++ pots.promote(expr)) + val Summary(pots, effs) = analyze(expr) + Summary(effs ++ pots.promote(expr)) case WhileDo(cond, body) => // for lazy fields, the translation may result in `while ()` - val (_, effs1) = if (cond.isEmpty) Summary.empty else analyze(cond) - val (_, effs2) = analyze(body) - (Potentials.empty, effs1 ++ effs2) + val Summary(_, effs1) = if (cond.isEmpty) Summary.empty else analyze(cond) + val Summary(_, effs2) = analyze(body) + Summary(effs1 ++ effs2) case Labeled(_, expr) => - val (_, effs1) = analyze(expr) - (Potentials.empty, effs1) + val summary = analyze(expr) + summary.dropPotentials case Try(block, cases, finalizer) => - val (pots, effs) = cases.foldLeft(analyze(block)) { (acc, cas) => + val Summary(pots, effs) = cases.foldLeft(analyze(block)) { (acc, cas) => acc union analyze(cas.body) } - val (_, eff2) = if (finalizer.isEmpty) Summary.empty else analyze(finalizer) - (pots, effs ++ eff2) + val Summary(_, eff2) = if (finalizer.isEmpty) Summary.empty else analyze(finalizer) + Summary(pots, effs ++ eff2) case SeqLiteral(elems, elemtpt) => val effsAll: Effects = elems.foldLeft(Effects.empty) { (effs, elem) => - val (pots1, effs1) = analyze(elem) + val Summary(pots1, effs1) = analyze(elem) pots1.promote(expr) ++ effs1 ++ effs } - (Potentials.empty, effsAll) + Summary(effsAll) case Inlined(call, bindings, expansion) => - val effs = bindings.foldLeft(Effects.empty) { (acc, mdef) => acc ++ analyze(mdef)._2 } - analyze(expansion).withEffs(effs) + val effs = bindings.foldLeft(Effects.empty) { (acc, mdef) => + acc ++ analyze(mdef).effs + } + analyze(expansion) ++ effs case vdef : ValDef => - lazy val (pots, effs) = analyze(vdef.rhs) + val Summary(pots, effs) = analyze(vdef.rhs) if (vdef.symbol.owner.isClass) - (Potentials.empty, if (vdef.symbol.is(Flags.Lazy)) Effects.empty else effs) + if (vdef.symbol.is(Flags.Lazy)) Summary.empty else Summary(effs) else - (Potentials.empty, pots.promote(vdef) ++ effs) + Summary(pots.promote(vdef) ++ effs) case Thicket(List()) => // possible in try/catch/finally, see tests/crash/i6914.scala Summary.empty case ddef : DefDef => - lazy val (pots, effs) = analyze(ddef.rhs) - if (ddef.symbol.owner.isClass) Summary.empty - else (Potentials.empty, pots.promote(ddef) ++ effs) + else { + val Summary(pots, effs) = analyze(ddef.rhs) + Summary(pots.promote(ddef) ++ effs) + } case _: TypeDef => Summary.empty @@ -205,12 +209,12 @@ object Summarization { throw new Exception("unexpected tree: " + expr.show) } - if (env.isAlwaysInitialized(expr.tpe)) (Potentials.empty, summary._2) + if (env.isAlwaysInitialized(expr.tpe)) Summary(Potentials.empty, summary.effs) else summary } def analyze(tp: Type, source: Tree)(implicit env: Env): Summary = - trace("summarizing " + tp.show, init, s => Summary.show(s.asInstanceOf[Summary])) { + trace("summarizing " + tp.show, init, s => s.asInstanceOf[Summary].show) { val summary: Summary = tp match { case _: ConstantType => Summary.empty @@ -219,11 +223,11 @@ object Summarization { Summary.empty case tmref: TermRef => - val (pots, effs) = analyze(tmref.prefix, source) - if (env.ignoredMethods.contains(tmref.symbol)) (Potentials.empty, effs) + val Summary(pots, effs) = analyze(tmref.prefix, source) + if (env.canIgnoreMethod(tmref.symbol)) Summary(effs) else { - val (pots2, effs2) = pots.select(tmref.symbol, source) - (pots2, effs ++ effs2) + val summary = pots.select(tmref.symbol, source) + summary ++ effs } case ThisType(tref) => @@ -232,18 +236,18 @@ object Summarization { resolveThis(cls, ThisRef()(source), enclosing, source) case SuperType(thisTp, superTp) => - val (pots, effs) = analyze(thisTp, source) + val Summary(pots, effs) = analyze(thisTp, source) val pots2 = pots.map { // TODO: properly handle super of the form A & B SuperRef(_, superTp.classSymbols.head.asClass)(source): Potential } - (pots2, effs) + Summary(pots2, effs) case _ => throw new Exception("unexpected type: " + tp.show) } - if (env.isAlwaysInitialized(tp)) (Potentials.empty, summary._2) + if (env.isAlwaysInitialized(tp)) Summary(Potentials.empty, summary.effs) else summary } @@ -259,14 +263,15 @@ object Summarization { } def resolveThis(cls: ClassSymbol, pot: Potential, cur: ClassSymbol, source: Tree)(implicit env: Env): Summary = - trace("resolve " + cls.show + ", pot = " + pot.show + ", cur = " + cur.show, init, s => Summary.show(s.asInstanceOf[Summary])) { + trace("resolve " + cls.show + ", pot = " + pot.show + ", cur = " + cur.show, init, s => s.asInstanceOf[Summary].show) { if (cls.is(Flags.Package)) Summary.empty - else if (cls == cur) (pot.toPots, Effects.empty) - else if (pot.size > 2) (Potentials.empty, Promote(pot)(source).toEffs) + else if (cls == cur) Summary(pot) + else if (pot.size > 2) Summary(Promote(pot)(source)) else { val enclosing = cur.owner.lexicallyEnclosingClass.asClass // Dotty uses O$.this outside of the object O - if (enclosing.is(Flags.Package) && cls.is(Flags.Module)) return Summary.empty + 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) @@ -276,17 +281,19 @@ object Summarization { /** Summarize secondary constructors or class body */ def analyzeConstructor(ctor: Symbol)(implicit env: Env): Summary = - trace("summarizing constructor " + ctor.owner.show, init, s => Summary.show(s.asInstanceOf[Summary])) { + trace("summarizing constructor " + ctor.owner.show, init, s => s.asInstanceOf[Summary].show) { if (ctor.isPrimaryConstructor) { val cls = ctor.owner.asClass val tpl = ctor.owner.defTree.asInstanceOf[TypeDef].rhs.asInstanceOf[Template] - val effs = analyze(Block(tpl.body, unitLiteral))._2 + val effs = analyze(Block(tpl.body, unitLiteral)).effs def parentArgEffsWithInit(stats: List[Tree], ctor: Symbol, source: Tree): Effects = - val initCall = MethodCall(ThisRef()(source), ctor)(source) - stats.foldLeft(Set(initCall)) { (acc, stat) => - val (_, effs) = Summarization.analyze(stat) - acc ++ effs + 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) => @@ -306,7 +313,7 @@ object Summarization { case ref => val tref: TypeRef = ref.tpe.typeConstructor.asInstanceOf val cls = tref.classSymbol.asClass - if (cls == defn.AnyClass || cls == defn.AnyValClass) Effects.empty + if env.canIgnoreClass(cls) then Effects.empty else { val ctor = cls.primaryConstructor Summarization.analyze(New(ref.tpe))(env.withOwner(ctor.owner))._2 + @@ -315,7 +322,7 @@ object Summarization { }) } - (Potentials.empty, effsAll) + Summary(effsAll) } else { val ddef = ctor.defTree.asInstanceOf[DefDef] @@ -329,9 +336,9 @@ object Summarization { val parentCls = tref.classSymbol.asClass val env2: Env = env.withOwner(cls.owner.lexicallyEnclosingClass) if (tref.prefix != NoPrefix) - parentCls -> analyze(tref.prefix, source)(env2)._1 + parentCls -> analyze(tref.prefix, source)(env2).pots else - parentCls -> analyze(cls.owner.lexicallyEnclosingClass.thisType, source)(env2)._1 + parentCls -> analyze(cls.owner.lexicallyEnclosingClass.thisType, source)(env2).pots } if (cls.defTree.isEmpty) diff --git a/compiler/src/dotty/tools/dotc/transform/init/Summary.scala b/compiler/src/dotty/tools/dotc/transform/init/Summary.scala index 0338f1cb0a5d..d4b72d4c0214 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Summary.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Summary.scala @@ -3,6 +3,7 @@ package transform package init import scala.collection.mutable +import scala.annotation.targetName import core._ import Contexts._ @@ -12,65 +13,80 @@ import config.Printers.init import Potentials._, Effects._, Util._ -object Summary { - type Summary = (Potentials, Effects) - val empty: Summary = (Potentials.empty, Effects.empty) - - /** Summary of class. - * - * It makes ObjectPart construction easier with already established raw outer for parents. - */ - case class ClassSummary(currentClass: ClassSymbol, parentOuter: Map[ClassSymbol, Potentials]) { - private val summaryCache: mutable.Map[Symbol, Summary] = mutable.Map.empty - - def cacheFor(member: Symbol, summary: Summary)(using Context): Unit = { - traceIndented("cache for " + member.show + ", summary = " + Summary.show(summary), init) - assert(member.owner == currentClass, "owner = " + member.owner.show + ", current = " + currentClass.show) - summaryCache(member) = summary - } +case class Summary(pots: Potentials, effs: Effects) { + def union(summary2: Summary): Summary = + Summary(pots ++ summary2.pots, this.effs ++ summary2.effs) - def summaryOf(member: Symbol)(implicit env: Env): Summary = - if (summaryCache.contains(member)) summaryCache(member) - else trace("summary for " + member.show, init, s => Summary.show(s.asInstanceOf[Summary])) { - implicit val env2 = env.withOwner(member) - val summary = - if (member.isConstructor) - Summarization.analyzeConstructor(member) - else if (member.is(Flags.Method)) - Summarization.analyzeMethod(member) - else // field - Summarization.analyzeField(member) - - summaryCache(member) = summary - summary - } - - def effectsOf(member: Symbol)(implicit env: Env): Effects = summaryOf(member)._2 - def potentialsOf(member: Symbol)(implicit env: Env): Potentials = summaryOf(member)._1 - - def show(using Context): String = - "ClassSummary(" + currentClass.name.show + - ", parents = " + parentOuter.map { case (k, v) => k.show + "->" + "[" + Potentials.show(v) + "]" } - } + def +(pot: Potential): Summary = + Summary(pots + pot, effs) + + def +(eff: Effect): Summary = + Summary(pots, effs + eff) - def show(summary: Summary)(using Context): String = { - val pots = Potentials.show(summary._1) - val effs = Effects.show(summary._2) + def dropPotentials: Summary = + Summary(Potentials.empty, effs) + + @targetName("withPotentials") + def ++(pots: Potentials): Summary = + Summary(this.pots ++ pots, effs) + + @targetName("withEffects") + def ++(effs: Effects): Summary = + Summary(pots, this.effs ++ effs) + + def show(using Context): String = { + val pots = Potentials.show(this.pots) + val effs = Effects.show(this.effs) s"([$pots], [$effs])" } +} - extension (summary1: Summary) def union (summary2: Summary): Summary = - (summary1._1 ++ summary2._1, summary1._2 ++ summary2._2) +object Summary { + val empty: Summary = Summary(Potentials.empty, Effects.empty) - extension (summary: Summary) def + (pot: Potential): Summary = - (summary._1 + pot, summary._2) + def apply(pots: Potentials): Summary = new Summary(pots, Effects.empty) - extension (summary: Summary) def + (eff: Effect): Summary = - (summary._1, summary._2 + eff) + @targetName("withEffects") + def apply(effs: Effects): Summary = new Summary(Potentials.empty, effs) + + def apply(pot: Potential): Summary = new Summary(Potentials.empty + pot, Effects.empty) + + def apply(eff: Effect): Summary = new Summary(Potentials.empty, Effects.empty + eff) +} + +/** Summary of class. + * + * It makes ObjectPart construction easier with already established raw outer for parents. + */ +case class ClassSummary(currentClass: ClassSymbol, parentOuter: Map[ClassSymbol, Potentials]) { + private val summaryCache: mutable.Map[Symbol, Summary] = mutable.Map.empty + + def cacheFor(member: Symbol, summary: Summary)(using Context): Unit = { + traceIndented("cache for " + member.show + ", summary = " + summary.show, init) + assert(member.owner == currentClass, "owner = " + member.owner.show + ", current = " + currentClass.show) + summaryCache(member) = summary + } + + def summaryOf(member: Symbol)(implicit env: Env): Summary = + if (summaryCache.contains(member)) summaryCache(member) + else trace("summary for " + member.show, init, s => s.asInstanceOf[Summary].show) { + implicit val env2 = env.withOwner(member) + val summary = + if (member.isConstructor) + Summarization.analyzeConstructor(member) + else if (member.is(Flags.Method)) + Summarization.analyzeMethod(member) + else // field + Summarization.analyzeField(member) + + summaryCache(member) = summary + summary + } - extension (summary: Summary) def withPots (pots: Potentials): Summary = - (summary._1 ++ pots, summary._2) + def effectsOf(member: Symbol)(implicit env: Env): Effects = summaryOf(member).effs + def potentialsOf(member: Symbol)(implicit env: Env): Potentials = summaryOf(member).pots - extension (summary: Summary) def withEffs (effs: Effects): Summary = - (summary._1, summary._2 ++ effs) + def show(using Context): String = + "ClassSummary(" + currentClass.name.show + + ", parents = " + parentOuter.map { case (k, v) => k.show + "->" + "[" + Potentials.show(v) + "]" } } diff --git a/compiler/src/dotty/tools/dotc/transform/init/Util.scala b/compiler/src/dotty/tools/dotc/transform/init/Util.scala index ac3d12364d26..41d77a8fe608 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Util.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Util.scala @@ -19,7 +19,7 @@ object Util { traceIndented(s"<== ${msg}", printer) } - extension (symbol: Symbol) def isInternal(using Context): Boolean = + extension (symbol: Symbol) def hasSource(using Context): Boolean = !symbol.defTree.isEmpty def resolve(cls: ClassSymbol, sym: Symbol)(using Context): Symbol = diff --git a/compiler/test/dotty/tools/dotc/CompilationTests.scala b/compiler/test/dotty/tools/dotc/CompilationTests.scala index 6aece6004d05..2ce99b0fe61b 100644 --- a/compiler/test/dotty/tools/dotc/CompilationTests.scala +++ b/compiler/test/dotty/tools/dotc/CompilationTests.scala @@ -310,24 +310,13 @@ class CompilationTests { }.checkRuns() // initialization tests - @Test def checkInitNeg: Unit = { + @Test def checkInit: Unit = { implicit val testGroup: TestGroup = TestGroup("checkInit") val options = defaultOptions.and("-Ycheck-init", "-Xfatal-warnings") - compileFilesInDir("tests/init/neg/", options) - }.checkExpectedErrors() - - @Test def checkInitCrash: Unit = { - implicit val testGroup: TestGroup = TestGroup("checkInit") - val options = defaultOptions.and("-Ycheck-init") - compileFilesInDir("tests/init/crash", options) - }.checkCompile() - - @Test def checkInitPos: Unit = { - implicit val testGroup: TestGroup = TestGroup("checkInit") - val options = defaultOptions.and("-Ycheck-init", "-Xfatal-warnings") - compileFilesInDir("tests/init/pos", options) - }.checkCompile() - + compileFilesInDir("tests/init/neg", options).checkExpectedErrors() + compileFilesInDir("tests/init/pos", options).checkCompile() + compileFilesInDir("tests/init/crash", options.without("-Xfatal-warnings")).checkCompile() + } } object CompilationTests extends ParallelTesting { diff --git a/docs/docs/reference/other-new-features/safe-initialization.md b/docs/docs/reference/other-new-features/safe-initialization.md index 2d115745bb57..5112c7dbae58 100644 --- a/docs/docs/reference/other-new-features/safe-initialization.md +++ b/docs/docs/reference/other-new-features/safe-initialization.md @@ -84,7 +84,6 @@ The checker reports: | -> val f: () => String = () => this.message [ features-high-order.scala:2 ] | -> def message: String = b [ features-high-order.scala:8 ] ``` - ## Design Goals We establish the following design goals: @@ -157,10 +156,9 @@ as it may indirectly reach uninitialized fields. Monotonicity is based on a well-known technique called _heap monotonic typestate_ to ensure soundness in the presence of aliasing -[1]. Roughly, it means initialization state should not go backwards. +[1]. Roughly speaking, it means initialization state should not go backwards. -Scopability means that access to partially constructed objects should be -controlled by static scoping. Control effects like coroutines, delimited +Scopability means that there are no side channels to access to partially constructed objects. Control effects like coroutines, delimited control, resumable exceptions may break the property, as they can transport a value upper in the stack (not in scope) to be reachable from the current scope. Static fields can also serve as a teleport thus breaks this property. In the @@ -242,7 +240,7 @@ We can impose the following rules to enforce modularity: ## Theory -The theory is based on type-and-effect systems [2]. We introduce two concepts, +The theory is based on type-and-effect systems [2, 3]. We introduce two concepts, _effects_ and _potentials_: ``` @@ -287,6 +285,8 @@ the initialization and there is no leaking of values under initialization. Virtual method calls on `this` is not a problem, as they can always be resolved statically. +For a more detailed introduction of the theory, please refer to the paper _a type-and-effect system for safe initialization_ [3]. + ## Back Doors Occasionally you may want to suppress warnings reported by the @@ -296,11 +296,11 @@ mark some fields as lazy. ## Caveats -The system cannot handle static fields, nor does it provide safety -guarantee when extending Java or Scala 2 classes. Calling methods of -Java or Scala 2 is always safe. +- The system cannot provide safety guarantee when extending Java or Scala 2 classes. +- Safe initialization of global objects is only partially checked. ## References -- 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). -- 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. +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. diff --git a/tests/init/full/neg/global-cycle1.scala b/tests/init/full/neg/global-cycle1.scala new file mode 100644 index 000000000000..ebd667c51ba0 --- /dev/null +++ b/tests/init/full/neg/global-cycle1.scala @@ -0,0 +1,10 @@ +object A { + val a: Int = B.b // error +} + +object B { + val b: Int = A.a // error +} + +@main +def Test = print(A.a) \ No newline at end of file diff --git a/tests/init/full/neg/global-cycle2.scala b/tests/init/full/neg/global-cycle2.scala new file mode 100644 index 000000000000..30792e58af6b --- /dev/null +++ b/tests/init/full/neg/global-cycle2.scala @@ -0,0 +1,7 @@ +object A { + val a: Int = B.foo() // error +} + +object B { + def foo(): Int = A.a * 2 +} diff --git a/tests/init/full/neg/global-cycle3.scala b/tests/init/full/neg/global-cycle3.scala new file mode 100644 index 000000000000..7fae20dbe894 --- /dev/null +++ b/tests/init/full/neg/global-cycle3.scala @@ -0,0 +1,7 @@ +class A(x: Int) { + def foo(): Int = B.a + 10 +} + +object B { + val a: Int = A(4).foo() // error +} diff --git a/tests/init/full/neg/global-cycle4.scala b/tests/init/full/neg/global-cycle4.scala new file mode 100644 index 000000000000..3de0533cb521 --- /dev/null +++ b/tests/init/full/neg/global-cycle4.scala @@ -0,0 +1,19 @@ +trait A { + def foo(): Int +} + +class B extends A { + def foo(): Int = 10 +} + +class C extends A { + def foo(): Int = O.a + 10 +} + +class D(x: Int) { + def bar(): A = if x > 0 then new B else new C +} + +object O { + val a: Int = D(5).bar().foo() // error +} diff --git a/tests/init/neg/hybrid2.scala b/tests/init/full/neg/hybrid2.scala similarity index 100% rename from tests/init/neg/hybrid2.scala rename to tests/init/full/neg/hybrid2.scala diff --git a/tests/init/full/neg/i9176.scala b/tests/init/full/neg/i9176.scala new file mode 100644 index 000000000000..abb8a6394dd2 --- /dev/null +++ b/tests/init/full/neg/i9176.scala @@ -0,0 +1,9 @@ +class Foo(val opposite: Foo) +case object A extends Foo(B) // error +case object B extends Foo(A) // error +object Test { + def main(args: Array[String]): Unit = { + println(A.opposite) + println(B.opposite) + } +} \ No newline at end of file diff --git a/tests/init/neg/inner-loop.scala b/tests/init/neg/inner-loop.scala index a7ff5c153d32..c56f31a96757 100644 --- a/tests/init/neg/inner-loop.scala +++ b/tests/init/neg/inner-loop.scala @@ -1,6 +1,6 @@ class Outer { outer => class Inner extends Outer { - val x = 5 + outer.n + val x = 5 + outer.n // error } val inner = new Inner val n = 6 // error diff --git a/tests/init/neg/inner1.scala b/tests/init/neg/inner1.scala index 6e8077a500b0..bef2fd2d159e 100644 --- a/tests/init/neg/inner1.scala +++ b/tests/init/neg/inner1.scala @@ -4,7 +4,7 @@ class Foo { val list = List(1, 2, 3) // error, as Inner access `this.list` val inner: Inner = new this.Inner // ok, `list` is instantiated - lib.escape(inner) // error + lib.escape(inner) // error val name = "good" diff --git a/tests/init/neg/inner11.scala b/tests/init/neg/inner11.scala index df5eb52a55c8..04098be8a3fd 100644 --- a/tests/init/neg/inner11.scala +++ b/tests/init/neg/inner11.scala @@ -32,5 +32,6 @@ object NameKinds2 { type ThisInfo = Info val info: Info = new Info println(info.kind) // ok + val count: Int = 10 } } diff --git a/tests/init/neg/inner17.scala b/tests/init/neg/inner17.scala index feb1c2b10229..441ed02767b3 100644 --- a/tests/init/neg/inner17.scala +++ b/tests/init/neg/inner17.scala @@ -5,7 +5,7 @@ class A { val a = f } - println(new B) // error + println(new B) // error } class C extends A { diff --git a/tests/init/neg/inner19.scala b/tests/init/neg/inner19.scala index 346ca175c802..2089f777dc52 100644 --- a/tests/init/neg/inner19.scala +++ b/tests/init/neg/inner19.scala @@ -14,6 +14,6 @@ class A { class B extends A { println((new O.B).f) - O.C(4) // error: leak due to potential length limit + O.C(4) // error override val n = 50 // error } \ No newline at end of file diff --git a/tests/init/neg/promote.scala b/tests/init/neg/promote.scala new file mode 100644 index 000000000000..70e951d78f6f --- /dev/null +++ b/tests/init/neg/promote.scala @@ -0,0 +1,9 @@ +class Wrap { + def qux[T](e: E[T]) = e.foo + + abstract class E[+T] { def foo: T } + object E { + final val A: E[Nothing] = new E { def foo = ref } + val ref = qux(A) // error + } +} \ No newline at end of file diff --git a/tests/init/neg/t3273.check b/tests/init/neg/t3273.check index 399e186e62cd..74c016ef521f 100644 --- a/tests/init/neg/t3273.check +++ b/tests/init/neg/t3273.check @@ -7,7 +7,7 @@ | | The unsafe promotion may cause the following problem(s): | - | 1. Access non-initialized field num1. Calling trace: + | 1. Access non-initialized value num1. Calling trace: | -> val num1: LazyList[Int] = 1 #:: num1.map(_ + 1) // error [ t3273.scala:4 ] -- Error: tests/init/neg/t3273.scala:5:61 ------------------------------------------------------------------------------ 5 | val num2: LazyList[Int] = 1 #:: num2.iterator.map(_ + 1).to(LazyList) // error @@ -18,5 +18,5 @@ | | The unsafe promotion may cause the following problem(s): | - | 1. Access non-initialized field num2. Calling trace: + | 1. Access non-initialized value num2. Calling trace: | -> val num2: LazyList[Int] = 1 #:: num2.iterator.map(_ + 1).to(LazyList) // error [ t3273.scala:5 ] From 93a81847c2be7bef945ec1c9dd40b3673ec6f572 Mon Sep 17 00:00:00 2001 From: Liu Fengyun Date: Sat, 28 Nov 2020 22:06:10 +0100 Subject: [PATCH 03/16] Fix debugging code --- compiler/src/dotty/tools/dotc/transform/init/Effects.scala | 2 +- compiler/src/dotty/tools/dotc/transform/init/Potentials.scala | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/transform/init/Effects.scala b/compiler/src/dotty/tools/dotc/transform/init/Effects.scala index cc5ddd75cce4..9486263a8af0 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Effects.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Effects.scala @@ -61,7 +61,7 @@ object Effects { extension (eff: Effect) def toEffs: Effects = Effects.empty + eff def asSeenFrom(eff: Effect, thisValue: Potential)(implicit env: Env): Effect = - trace(eff.show + " asSeenFrom " + thisValue.show + ", current = " + currentClass.show, init, eff => show(Effects.empty + eff.asInstanceOf[Effect])) { eff match { + trace(eff.show + " asSeenFrom " + thisValue.show + ", current = " + currentClass.show, init, _.asInstanceOf[Effect].show) { eff match { case Promote(pot) => val pot1 = Potentials.asSeenFrom(pot, thisValue) Promote(pot1)(eff.source) diff --git a/compiler/src/dotty/tools/dotc/transform/init/Potentials.scala b/compiler/src/dotty/tools/dotc/transform/init/Potentials.scala index b98811a15f65..6ed73bf43679 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Potentials.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Potentials.scala @@ -174,7 +174,7 @@ object Potentials { extension (ps: Potentials) def promote(source: Tree): Effects = ps.map(Promote(_)(source)) - def asSeenFrom(pot: Potential, thisValue: Potential)(implicit env: Env): Potential = trace(pot.show + " asSeenFrom " + thisValue.show, init, pot => pot.asInstanceOf[Potential].show) { + def asSeenFrom(pot: Potential, thisValue: Potential)(implicit env: Env): Potential = trace(pot.show + " asSeenFrom " + thisValue.show, init, _.asInstanceOf[Potential].show) { pot match { case MethodReturn(pot1, sym) => val pot = asSeenFrom(pot1, thisValue) From e3de5acb9e7b4e8802e4b45234c01b17af76d758 Mon Sep 17 00:00:00 2001 From: Liu Fengyun Date: Sat, 28 Nov 2020 22:21:09 +0100 Subject: [PATCH 04/16] Don't produce duplicate select effect in potential expansion --- .../tools/dotc/transform/init/Checking.scala | 4 ++-- .../dotc/transform/init/Potentials.scala | 19 +++++++++++-------- 2 files changed, 13 insertions(+), 10 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/transform/init/Checking.scala b/compiler/src/dotty/tools/dotc/transform/init/Checking.scala index 4f81a02f7ed7..fda36d2b74e6 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Checking.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Checking.scala @@ -345,7 +345,7 @@ object Checking { case _ => val Summary(pots, effs) = expand(pot1) - val Summary(pots2, effs2) = pots.select(sym, pot.source) + val Summary(pots2, effs2) = pots.select(sym, pot.source, selectEffect = false) Summary(pots2, effs ++ effs2) } @@ -374,7 +374,7 @@ object Checking { case _ => val Summary(pots, effs) = expand(pot1) - val Summary(pots2, effs2) = pots.select(sym, pot.source) + val Summary(pots2, effs2) = pots.select(sym, pot.source, selectEffect = false) Summary(pots2, effs ++ effs2) } diff --git a/compiler/src/dotty/tools/dotc/transform/init/Potentials.scala b/compiler/src/dotty/tools/dotc/transform/init/Potentials.scala index 6ed73bf43679..b61a2fa0cbb1 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Potentials.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Potentials.scala @@ -155,21 +155,24 @@ object Potentials { extension (pot: Potential) def toPots: Potentials = Potentials.empty + pot - extension (ps: Potentials) def select (symbol: Symbol, source: Tree)(using Context): Summary = - ps.foldLeft(Summary.empty) { case (Summary(pots, effs), pot) => + extension (ps: Potentials) def select (symbol: Symbol, source: Tree, selectEffect: Boolean = true)(using Context): Summary = + ps.foldLeft(Summary.empty) { case (summary, pot) => // max potential length // TODO: it can be specified on a project basis via compiler options if (pot.size > 2) summary + Promote(pot)(pot.source) else if (symbol.isConstructor) - Summary(pots + pot, effs + MethodCall(pot, symbol)(source)) + val res = summary + pot + if selectEffect then res + MethodCall(pot, symbol)(source) + else res else if (symbol.isOneOf(Flags.Method | Flags.Lazy)) - Summary( - pots + MethodReturn(pot, symbol)(source), - effs + MethodCall(pot, symbol)(source) - ) + val res = summary + MethodReturn(pot, symbol)(source) + if selectEffect then res + MethodCall(pot, symbol)(source) + else res else - Summary(pots + FieldReturn(pot, symbol)(source), effs + FieldAccess(pot, symbol)(source)) + val res = summary + FieldReturn(pot, symbol)(source) + if selectEffect then res + FieldAccess(pot, symbol)(source) + else res } extension (ps: Potentials) def promote(source: Tree): Effects = ps.map(Promote(_)(source)) From 7910e358d7498785c7edca00ffb953bf8f0c7022 Mon Sep 17 00:00:00 2001 From: Liu Fengyun Date: Sun, 29 Nov 2020 16:53:35 +0100 Subject: [PATCH 05/16] Revert #9673 Handle NoPrefix properly --- .../dotty/tools/dotc/transform/init/Summarization.scala | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/transform/init/Summarization.scala b/compiler/src/dotty/tools/dotc/transform/init/Summarization.scala index 1ae2822a0c18..fb2a0a9b367e 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Summarization.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Summarization.scala @@ -316,8 +316,11 @@ object Summarization { if env.canIgnoreClass(cls) then Effects.empty else { val ctor = cls.primaryConstructor - Summarization.analyze(New(ref.tpe))(env.withOwner(ctor.owner))._2 + - MethodCall(ThisRef()(ref), ctor)(ref) + val prefixEff = + if tref.prefix == NoPrefix then Effects.empty + else Summarization.analyze(New(ref.tpe))(env.withOwner(ctor.owner)).effs + + prefixEff + MethodCall(ThisRef()(ref), ctor)(ref) } }) } From 322f4d01990485af28702c9946339eec004acf3c Mon Sep 17 00:00:00 2001 From: Liu Fengyun Date: Thu, 3 Dec 2020 12:19:36 +0100 Subject: [PATCH 06/16] check initialization in community build --- community-build/src/scala/dotty/communitybuild/projects.scala | 1 + .../src/dotty/tools/dotc/transform/init/Summarization.scala | 2 +- compiler/test/dotty/tools/dotc/CompilationTests.scala | 4 ++-- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/community-build/src/scala/dotty/communitybuild/projects.scala b/community-build/src/scala/dotty/communitybuild/projects.scala index 89c5c7aa4a1a..01e70dd1849d 100644 --- a/community-build/src/scala/dotty/communitybuild/projects.scala +++ b/community-build/src/scala/dotty/communitybuild/projects.scala @@ -96,6 +96,7 @@ final case class SbtCommunityProject( private val baseCommand = "clean; set logLevel in Global := Level.Error; set updateOptions in Global ~= (_.withLatestSnapshots(false)); " + ++ """set scalacOptions in Global += "-Ycheck-init";""" ++ s"++$compilerVersion!; " override val testCommand = diff --git a/compiler/src/dotty/tools/dotc/transform/init/Summarization.scala b/compiler/src/dotty/tools/dotc/transform/init/Summarization.scala index fb2a0a9b367e..d1fbbbbeb158 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Summarization.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Summarization.scala @@ -318,7 +318,7 @@ object Summarization { val ctor = cls.primaryConstructor val prefixEff = if tref.prefix == NoPrefix then Effects.empty - else Summarization.analyze(New(ref.tpe))(env.withOwner(ctor.owner)).effs + else Summarization.analyze(tref.prefix, ref)(env.withOwner(ctor.owner)).effs prefixEff + MethodCall(ThisRef()(ref), ctor)(ref) } diff --git a/compiler/test/dotty/tools/dotc/CompilationTests.scala b/compiler/test/dotty/tools/dotc/CompilationTests.scala index 2ce99b0fe61b..53fd04c04136 100644 --- a/compiler/test/dotty/tools/dotc/CompilationTests.scala +++ b/compiler/test/dotty/tools/dotc/CompilationTests.scala @@ -40,7 +40,7 @@ class CompilationTests { compileFilesInDir("tests/new", defaultOptions), compileFilesInDir("tests/pos-scala2", scala2CompatMode), compileFilesInDir("tests/pos-custom-args/erased", defaultOptions.and("-Yerased-terms")), - compileFilesInDir("tests/pos", defaultOptions), + compileFilesInDir("tests/pos", defaultOptions.and("-Ycheck-init")), compileFilesInDir("tests/pos-deep-subtype", allowDeepSubtypes), compileFile( // succeeds despite -Xfatal-warnings because of -nowarn @@ -187,7 +187,7 @@ class CompilationTests { compileFile("tests/run-custom-args/defaults-serizaliable-no-forwarders.scala", defaultOptions and "-Xmixin-force-forwarders:false"), compileFilesInDir("tests/run-custom-args/erased", defaultOptions.and("-Yerased-terms")), compileFilesInDir("tests/run-deep-subtype", allowDeepSubtypes), - compileFilesInDir("tests/run", defaultOptions) + compileFilesInDir("tests/run", defaultOptions.and("-Ycheck-init")) ).checkRuns() } From 7f0312101fe95a0a4228729bd41814e9838ee3a7 Mon Sep 17 00:00:00 2001 From: Liu Fengyun Date: Fri, 4 Dec 2020 08:44:52 +0100 Subject: [PATCH 07/16] Use correct owner for summarizing effects in parent calls --- .../tools/dotc/transform/init/Checking.scala | 6 +- .../dotc/transform/init/Summarization.scala | 2 +- tests/init/crash/matthias4.scala | 84 +++++++++++++++++++ 3 files changed, 89 insertions(+), 3 deletions(-) create mode 100644 tests/init/crash/matthias4.scala diff --git a/compiler/src/dotty/tools/dotc/transform/init/Checking.scala b/compiler/src/dotty/tools/dotc/transform/init/Checking.scala index fda36d2b74e6..65c3d909f18c 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Checking.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Checking.scala @@ -89,6 +89,8 @@ object Checking { 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 @@ -97,7 +99,7 @@ object Checking { case vdef : ValDef => val summary = Summarization.analyze(vdef.rhs) theEnv.summaryOf(cls).cacheFor(vdef.symbol, summary) - if (!vdef.symbol.is(Flags.Lazy)) { + if (!vdef.symbol.isOneOf(Flags.Lazy | Flags.Deferred)) { checkEffects(summary.effs) traceIndented(vdef.symbol.show + " initialized", init) state.fieldsInited += vdef.symbol @@ -163,7 +165,7 @@ object Checking { case ref => val cls = ref.tpe.classSymbol.asClass - if (!state.parentsInited.contains(cls) && cls.primaryConstructor.exists) + if (cls.primaryConstructor.exists) checkConstructor(cls.primaryConstructor, ref.tpe, ref) } diff --git a/compiler/src/dotty/tools/dotc/transform/init/Summarization.scala b/compiler/src/dotty/tools/dotc/transform/init/Summarization.scala index d1fbbbbeb158..bde4e5e7b97c 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Summarization.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Summarization.scala @@ -318,7 +318,7 @@ object Summarization { val ctor = cls.primaryConstructor val prefixEff = if tref.prefix == NoPrefix then Effects.empty - else Summarization.analyze(tref.prefix, ref)(env.withOwner(ctor.owner)).effs + else Summarization.analyze(tref.prefix, ref).effs prefixEff + MethodCall(ThisRef()(ref), ctor)(ref) } diff --git a/tests/init/crash/matthias4.scala b/tests/init/crash/matthias4.scala new file mode 100644 index 000000000000..18599ae71418 --- /dev/null +++ b/tests/init/crash/matthias4.scala @@ -0,0 +1,84 @@ +/* +object A requires B { + B.X getX() { + return B.getX(); + } + void setX(B.X x) {} +} +object B { + class X {} + X getX() { + return new X(); + } + void setX(X x) {} +} +object C requires B { + object A; + void test() { + A.setX(B.getX()); + } +} +*/ + +trait _a extends AnyRef with _b { + val a: _a; + val A: A; + type A <: a.AObject; + trait AObject { + def getX(): B.X; + def setX(x: B.X): Unit; + } +} +trait a123 extends AnyRef with _a with _b { + val a: this.type = this; + val A: A = new A(); + class A() extends AObject { + def getX(): B.X = B.getX(); + def setX(x: B.X) = B.setX(x); + } +} + +trait _b { + val b: _b; + val B: B; + type B <: b.BObject; + trait BObject { + type X; + def getX(): X; + def setX(x: X): Unit; + } +} +abstract class b() extends AnyRef with _b { + val b: this.type = this; + val B: B = new B(); + class B() extends BObject { + class X() {} + def getX(): X = new X(); + def setX(x: X) = (); + } +} + +trait _m { + val m: _m; + val M: M; + type M <: m.MObject; + trait MObject {} +} +abstract class m() extends AnyRef with _m with _b { + val m: this.type = this; + val M: M = new M(); + class M() extends MObject with a123 with Linker { + def test() = { + val x: B.X = B.getX(); + A.setX(x); + } + } + trait Linker { + val b: m.this.b.type = m.this.b; + val B: m.this.B.type = m.this.B; + type B = m.this.B; + val m: m.this.m.type = m.this.m; + val M: m.this.M.type = m.this.M; + type M = m.this.M; + } +} From 1b20194f00f289e72cc72ee1d0096773f2256a04 Mon Sep 17 00:00:00 2001 From: Liu Fengyun Date: Fri, 4 Dec 2020 09:19:53 +0100 Subject: [PATCH 08/16] Fix crash: ensure class type is applied Otherwise, looking up members will crash the compiler --- .../init/crash/singleton-ops-test-issue-8287.scala | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 tests/init/crash/singleton-ops-test-issue-8287.scala diff --git a/tests/init/crash/singleton-ops-test-issue-8287.scala b/tests/init/crash/singleton-ops-test-issue-8287.scala new file mode 100644 index 000000000000..bc4595bf79a1 --- /dev/null +++ b/tests/init/crash/singleton-ops-test-issue-8287.scala @@ -0,0 +1,13 @@ +import scala.compiletime.ops.int._ + +object Test { + class Vec[S <: Int] { + infix def concat [RS <: Int](that : Vec[RS]) : Vec[S + RS] = new Vec[S + RS] + } + + val v1 = new Vec[1] + val v2 = new Vec[2] + val v3 : Vec[3] = v1 concat v2 + val v3a = v1 concat v2 + val v3b : Vec[3] = v3a +} \ No newline at end of file From 8ed8887cfc0fea84be495e1bd33fd77dd4a3945f Mon Sep 17 00:00:00 2001 From: Liu Fengyun Date: Fri, 4 Dec 2020 10:04:41 +0100 Subject: [PATCH 09/16] Fix stdlib CB: don't check language patches The owners are different for the patches, and they are always safe. --- compiler/src/dotty/tools/dotc/transform/init/Checker.scala | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/compiler/src/dotty/tools/dotc/transform/init/Checker.scala b/compiler/src/dotty/tools/dotc/transform/init/Checker.scala index 61d4dcfaa952..c7607827f639 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Checker.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Checker.scala @@ -9,6 +9,7 @@ import ast.tpd import dotty.tools.dotc.core._ import Contexts._ import Types._ +import Symbols._ import dotty.tools.dotc.transform._ import MegaPhase._ @@ -45,7 +46,7 @@ class Checker extends MiniPhase { } // A concrete class may not be instantiated if the self type is not satisfied - if (instantiable) { + if (instantiable && cls.enclosingPackageClass != defn.StdLibPatchesPackage.moduleClass) { implicit val state: Checking.State = Checking.State( visited = Set.empty, path = Vector.empty, From 500083c61d5ceee2f87650c16dbf7fa496ce37a9 Mon Sep 17 00:00:00 2001 From: Liu Fengyun Date: Fri, 4 Dec 2020 10:38:14 +0100 Subject: [PATCH 10/16] Suppress warning in shapeless Shapeless enabled `-Xfatal-warings`, the warnings make the CI fail. --- community-build/community-projects/shapeless | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/community-build/community-projects/shapeless b/community-build/community-projects/shapeless index bfa5dba99d64..52ca352fefa3 160000 --- a/community-build/community-projects/shapeless +++ b/community-build/community-projects/shapeless @@ -1 +1 @@ -Subproject commit bfa5dba99d6402336a1fbaf687f5cb29b3e78fe2 +Subproject commit 52ca352fefa3090e91cbcddba4e7145a23fa5c1a From 16c8bfa833629a0840fdc10ffda45e79e3959aa9 Mon Sep 17 00:00:00 2001 From: Liu Fengyun Date: Fri, 4 Dec 2020 10:54:27 +0100 Subject: [PATCH 11/16] Restore tests --- tests/init/{full => }/neg/hybrid2.scala | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename tests/init/{full => }/neg/hybrid2.scala (100%) diff --git a/tests/init/full/neg/hybrid2.scala b/tests/init/neg/hybrid2.scala similarity index 100% rename from tests/init/full/neg/hybrid2.scala rename to tests/init/neg/hybrid2.scala From 91c0f565b31a7c5492e06bcde7e45a0910f75ce3 Mon Sep 17 00:00:00 2001 From: Liu Fengyun Date: Fri, 4 Dec 2020 12:06:02 +0100 Subject: [PATCH 12/16] Fix CI: ignore export clause --- .../src/dotty/tools/dotc/transform/init/Summarization.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/compiler/src/dotty/tools/dotc/transform/init/Summarization.scala b/compiler/src/dotty/tools/dotc/transform/init/Summarization.scala index bde4e5e7b97c..8df4cb7f558d 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Summarization.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Summarization.scala @@ -202,7 +202,7 @@ object Summarization { case _: TypeDef => Summary.empty - case _: Import => + case _: Import | _: Export => Summary.empty case _ => From 0f034aa0b51ca7df6cdff54b5a3066b0df6465b8 Mon Sep 17 00:00:00 2001 From: Liu Fengyun Date: Fri, 12 Feb 2021 11:15:04 +0100 Subject: [PATCH 13/16] Set defTree properly In the following test: - tests/pos/i3130b.scala If we add `transparent`, then everything is OK. The reason is that we set `Symbol.defTree` systematically in PostTyper. Now the inlining happens after PostTyper, thus `defTree` is not properly set for inlined definitions. To compensate, we ensure that `defTree` is set in the ReTyper. The IninerTyper extends ReTyper, thus it fixes the problem. Doing the fix directly in InlinerTyper, however, does not pass the CI. The reason is that `-Ycheck:all` will run `TreeChecker` which will make tree bindings get lost. --- compiler/src/dotty/tools/dotc/typer/ReTyper.scala | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/compiler/src/dotty/tools/dotc/typer/ReTyper.scala b/compiler/src/dotty/tools/dotc/typer/ReTyper.scala index 8ff0dfc8c61b..42c71c1a7b72 100644 --- a/compiler/src/dotty/tools/dotc/typer/ReTyper.scala +++ b/compiler/src/dotty/tools/dotc/typer/ReTyper.scala @@ -115,7 +115,9 @@ class ReTyper extends Typer with ReChecking { } override def typedUnadapted(tree: untpd.Tree, pt: Type, locked: TypeVars)(using Context): Tree = - try super.typedUnadapted(tree, pt, locked) + try super.typedUnadapted(tree, pt, locked) match + case member: MemberDef => member.setDefTree + case tree => tree catch { case NonFatal(ex) => if ctx.phase != Phases.typerPhase && ctx.phase != Phases.inliningPhase then From e4efda8656fd580ae4b0ed9283a9bc29dc03f4ed Mon Sep 17 00:00:00 2001 From: Liu Fengyun Date: Fri, 12 Feb 2021 19:50:50 +0100 Subject: [PATCH 14/16] Disable -Ycheck-init for cats due to -Xfatal-warnings cats enables -Xfatal-warnings and we don't want to change the library to create conflicts with upstream. --- .../src/scala/dotty/communitybuild/projects.scala | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/community-build/src/scala/dotty/communitybuild/projects.scala b/community-build/src/scala/dotty/communitybuild/projects.scala index 01e70dd1849d..e35b616af626 100644 --- a/community-build/src/scala/dotty/communitybuild/projects.scala +++ b/community-build/src/scala/dotty/communitybuild/projects.scala @@ -90,13 +90,17 @@ final case class SbtCommunityProject( extraSbtArgs: List[String] = Nil, dependencies: List[CommunityProject] = Nil, sbtPublishCommand: String = null, - sbtDocCommand: String = null + sbtDocCommand: String = null, + scalacOptions: List[String] = List("-Ycheck-init") ) extends CommunityProject: override val binaryName: String = "sbt" + private def scalacOptionsString: String = + scalacOptions.map("\"" + _ + "\"").mkString("List(", ",", ")") + private val baseCommand = "clean; set logLevel in Global := Level.Error; set updateOptions in Global ~= (_.withLatestSnapshots(false)); " - ++ """set scalacOptions in Global += "-Ycheck-init";""" + ++ (if scalacOptions.isEmpty then "" else s"""set scalacOptions in Global ++= $scalacOptionsString;""") ++ s"++$compilerVersion!; " override val testCommand = @@ -467,7 +471,8 @@ object projects: project = "discipline-specs2", sbtTestCommand = "test", sbtPublishCommand = "coreJVM/publishLocal;coreJS/publishLocal", - dependencies = List(discipline) + dependencies = List(discipline), + scalacOptions = Nil // disabble -Ycheck-init ) lazy val simulacrumScalafixAnnotations = SbtCommunityProject( @@ -480,7 +485,8 @@ object projects: project = "cats", sbtTestCommand = "set scalaJSStage in Global := FastOptStage;buildJVM;validateAllJS", sbtPublishCommand = "catsJVM/publishLocal;catsJS/publishLocal", - dependencies = List(discipline, disciplineMunit, scalacheck, simulacrumScalafixAnnotations) + dependencies = List(discipline, disciplineMunit, scalacheck, simulacrumScalafixAnnotations), + scalacOptions = Nil // disable -Ycheck-init, due to -Xfatal-warning ) lazy val catsMtl = SbtCommunityProject( From 3a6b0c8c557264af83ceef7a31733ed28ce14a81 Mon Sep 17 00:00:00 2001 From: Liu Fengyun Date: Tue, 16 Feb 2021 11:30:04 +0100 Subject: [PATCH 15/16] Address review --- .../tools/dotc/transform/init/Checking.scala | 4 ++-- .../tools/dotc/transform/init/Potentials.scala | 16 ++++++++++++---- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/transform/init/Checking.scala b/compiler/src/dotty/tools/dotc/transform/init/Checking.scala index 65c3d909f18c..85217501d55f 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Checking.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Checking.scala @@ -347,7 +347,7 @@ object Checking { case _ => val Summary(pots, effs) = expand(pot1) - val Summary(pots2, effs2) = pots.select(sym, pot.source, selectEffect = false) + val Summary(pots2, effs2) = pots.select(sym, pot.source, ignoreSelectEffect = false) Summary(pots2, effs ++ effs2) } @@ -376,7 +376,7 @@ object Checking { case _ => val Summary(pots, effs) = expand(pot1) - val Summary(pots2, effs2) = pots.select(sym, pot.source, selectEffect = false) + val Summary(pots2, effs2) = pots.select(sym, pot.source, ignoreSelectEffect = false) Summary(pots2, effs ++ effs2) } diff --git a/compiler/src/dotty/tools/dotc/transform/init/Potentials.scala b/compiler/src/dotty/tools/dotc/transform/init/Potentials.scala index b61a2fa0cbb1..a7b409b56d5e 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Potentials.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Potentials.scala @@ -155,7 +155,15 @@ object Potentials { extension (pot: Potential) def toPots: Potentials = Potentials.empty + pot - extension (ps: Potentials) def select (symbol: Symbol, source: Tree, selectEffect: Boolean = true)(using Context): Summary = + /** 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 @@ -163,15 +171,15 @@ object Potentials { summary + Promote(pot)(pot.source) else if (symbol.isConstructor) val res = summary + pot - if selectEffect then res + MethodCall(pot, symbol)(source) + 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 selectEffect then res + MethodCall(pot, symbol)(source) + if ignoreSelectEffect then res + MethodCall(pot, symbol)(source) else res else val res = summary + FieldReturn(pot, symbol)(source) - if selectEffect then res + FieldAccess(pot, symbol)(source) + if ignoreSelectEffect then res + FieldAccess(pot, symbol)(source) else res } From db19f60d89060b1d826156e0265d4a8e78845164 Mon Sep 17 00:00:00 2001 From: Liu Fengyun Date: Tue, 16 Feb 2021 11:33:15 +0100 Subject: [PATCH 16/16] Revert changes to shapeless To avoid unnecessary conflicts, we disable -Ycheck-init on shapeless. --- community-build/community-projects/shapeless | 2 +- .../src/scala/dotty/communitybuild/projects.scala | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/community-build/community-projects/shapeless b/community-build/community-projects/shapeless index 52ca352fefa3..bfa5dba99d64 160000 --- a/community-build/community-projects/shapeless +++ b/community-build/community-projects/shapeless @@ -1 +1 @@ -Subproject commit 52ca352fefa3090e91cbcddba4e7145a23fa5c1a +Subproject commit bfa5dba99d6402336a1fbaf687f5cb29b3e78fe2 diff --git a/community-build/src/scala/dotty/communitybuild/projects.scala b/community-build/src/scala/dotty/communitybuild/projects.scala index e35b616af626..5a8f485516cd 100644 --- a/community-build/src/scala/dotty/communitybuild/projects.scala +++ b/community-build/src/scala/dotty/communitybuild/projects.scala @@ -312,7 +312,8 @@ object projects: lazy val shapeless = SbtCommunityProject( project = "shapeless", sbtTestCommand = "test", - sbtDocCommand = forceDoc("typeable", "deriving", "data") + sbtDocCommand = forceDoc("typeable", "deriving", "data"), + scalacOptions = Nil // disable -Ycheck-init, due to -Xfatal-warnings ) lazy val xmlInterpolator = SbtCommunityProject( @@ -472,7 +473,7 @@ object projects: sbtTestCommand = "test", sbtPublishCommand = "coreJVM/publishLocal;coreJS/publishLocal", dependencies = List(discipline), - scalacOptions = Nil // disabble -Ycheck-init + scalacOptions = Nil // disable -Ycheck-init ) lazy val simulacrumScalafixAnnotations = SbtCommunityProject(