From 7febb3acc52d90fa4d3fd60c6b6c8f5857ba9bee Mon Sep 17 00:00:00 2001 From: Liu Fengyun Date: Sat, 3 Apr 2021 09:47:43 +0200 Subject: [PATCH 01/47] WIP - Check global objects --- compiler/src/dotty/tools/dotc/Compiler.scala | 4 +- .../tools/dotc/transform/init/Checker.scala | 37 +++- .../tools/dotc/transform/init/Checking.scala | 157 ++++++++++++++-- .../dotc/transform/init/CycleChecker.scala | 168 ++++++++++++++++++ .../tools/dotc/transform/init/Effects.scala | 14 +- .../tools/dotc/transform/init/Errors.scala | 9 + .../dotc/transform/init/Potentials.scala | 27 ++- .../dotc/transform/init/Summarization.scala | 55 ++++-- tests/init/neg/features-trees2.scala | 6 + tests/init/neg/inner9.scala | 1 - 10 files changed, 437 insertions(+), 41 deletions(-) create mode 100644 compiler/src/dotty/tools/dotc/transform/init/CycleChecker.scala create mode 100644 tests/init/neg/features-trees2.scala diff --git a/compiler/src/dotty/tools/dotc/Compiler.scala b/compiler/src/dotty/tools/dotc/Compiler.scala index 8c22bf5b9293..425a23f56d70 100644 --- a/compiler/src/dotty/tools/dotc/Compiler.scala +++ b/compiler/src/dotty/tools/dotc/Compiler.scala @@ -64,8 +64,8 @@ class Compiler { new CheckStatic, // Check restrictions that apply to @static members new BetaReduce, // Reduce closure applications new InlineVals, // Check right hand-sides of an `inline val`s - new ExpandSAMs, // Expand single abstract method closures to anonymous classes - new init.Checker) :: // Check initialization of objects + new ExpandSAMs) :: // Expand single abstract method closures to anonymous classes + List(new init.Checker) :: // Check initialization of objects List(new ElimRepeated, // Rewrite vararg parameters and arguments new ProtectedAccessors, // Add accessors for protected members new ExtensionMethods, // Expand methods of value classes with extension methods diff --git a/compiler/src/dotty/tools/dotc/transform/init/Checker.scala b/compiler/src/dotty/tools/dotc/transform/init/Checker.scala index ec15d35096f6..424de3ba83ae 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Checker.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Checker.scala @@ -12,13 +12,13 @@ import Types._ import Symbols._ import dotty.tools.dotc.transform._ -import MegaPhase._ +import Phases._ import scala.collection.mutable -class Checker extends MiniPhase { +class Checker extends Phase { import tpd._ val phaseName = "initChecker" @@ -26,13 +26,38 @@ class Checker extends MiniPhase { // cache of class summary private val cache = new Cache + private val cycleChecker = new CycleChecker + override val runsAfter = Set(Pickler.name) override def isEnabled(using Context): Boolean = super.isEnabled && ctx.settings.YcheckInit.value - override def transformTypeDef(tree: TypeDef)(using Context): tpd.Tree = { - if (!tree.isClassDef) return tree + override def runOn(units: List[CompilationUnit])(using Context): List[CompilationUnit] = + cycleChecker.clean() + val newUnits = super.runOn(units) + cycleChecker.check() + newUnits + + val traverser = new TreeTraverser { + override def traverse(tree: Tree)(using Context): Unit = + tree match { + case tdef: TypeDef if tdef.isClassDef => + cycleChecker.classesInCurrentRun += tdef.symbol + checkClassDef(tdef) + traverseChildren(tree) + case _ => + traverseChildren(tree) + } + } + + override def run(using Context): Unit = { + val unit = ctx.compilationUnit + traverser.traverse(unit.tpdTree) + } + + def checkClassDef(tree: TypeDef)(using Context): tpd.Tree = { + assert(tree.isClassDef) val cls = tree.symbol.asClass val instantiable: Boolean = @@ -54,10 +79,14 @@ class Checker extends MiniPhase { fieldsInited = mutable.Set.empty, parentsInited = mutable.Set.empty, safePromoted = mutable.Set.empty, + dependencies = mutable.Set.empty, env = Env(ctx.withOwner(cls), cache) ) Checking.checkClassBody(tree) + + if cls.is(Flags.Module) && cls.isStatic then + cycleChecker.cacheObjectDependencies(cls.sourceModule, state.dependencies.toList) } tree diff --git a/compiler/src/dotty/tools/dotc/transform/init/Checking.scala b/compiler/src/dotty/tools/dotc/transform/init/Checking.scala index cc8f95b20e8c..95e41d8e130f 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Checking.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Checking.scala @@ -37,6 +37,7 @@ object Checking { fieldsInited: mutable.Set[Symbol], parentsInited: mutable.Set[ClassSymbol], safePromoted: mutable.Set[Potential], // Potentials that can be safely promoted + dependencies: mutable.Set[Dependency], // if the current class is a static object, its dependencies env: Env ) { def withOwner[T](sym: Symbol)(op: State ?=> T): T = @@ -45,6 +46,11 @@ object Checking { this.visited = state.visited res + def isThisStatic(using Context): Boolean = + thisClass.is(Flags.Module) && thisClass.sourceModule.isStatic + + def isInSameFile(that: Symbol)(using Context): Boolean = + that.source == that.source def visit[T](eff: Effect)(op: State ?=> T): T = val state: State = this.copy(path = path :+ eff.source, visited = this.visited + eff) @@ -53,10 +59,8 @@ object Checking { res def test(op: State ?=> Errors): Errors = { - val savedVisited = visited - val errors = op(using this) - visited = savedVisited - errors + val state = this.copy(dependencies = mutable.Set.empty) + op(using state) } } @@ -71,11 +75,11 @@ object Checking { } else state.visit(eff) { - eff match { - case eff: Promote => Checking.checkPromote(eff) - case eff: FieldAccess => Checking.checkFieldAccess(eff) - case eff: MethodCall => Checking.checkMethodCall(eff) - } + eff match + case eff: Promote => Checking.checkPromote(eff) + case eff: FieldAccess => Checking.checkFieldAccess(eff) + case eff: MethodCall => Checking.checkMethodCall(eff) + case eff: AccessGlobal => Checking.checkAccessGlobal(eff) } } } @@ -164,13 +168,25 @@ object Checking { } tpl.parents.foreach { - case tree @ Block(_, parent) => + case tree @ Block(stats, parent) => + if state.isThisStatic then + val argss = termArgss(parent) + checkStats((stats :: argss).flatten, cls) + checkConstructor(funPart(parent).symbol, parent.tpe, tree) - case tree @ Apply(Block(_, parent), _) => + case tree @ Apply(Block(stats, parent), args) => + if state.isThisStatic then + val argss = termArgss(parent) + checkStats((stats :: args :: argss).flatten, cls) + checkConstructor(funPart(parent).symbol, tree.tpe, tree) case parent : Apply => + if state.isThisStatic then + val argss = termArgss(parent) + checkStats(argss.flatten, cls) + checkConstructor(funPart(parent).symbol, parent.tpe, parent) case ref => @@ -198,7 +214,7 @@ object Checking { case SuperRef(thisRef: ThisRef, supercls) => val target = resolveSuper(state.thisClass, supercls, sym) - if (!target.is(Flags.Method)) + if (!target.isOneOf(Flags.Method | Flags.Lazy)) check(FieldAccess(pot, target)(eff.source)) else if (target.hasSource) { val effs = thisRef.effectsOf(target).toList @@ -218,6 +234,38 @@ object Checking { else Errors.empty + case pot @ Global(obj) => + if !state.isThisStatic then + Errors.empty + else if state.isInSameFile(obj) then + // more fine-grained check if in the same source + val target = resolve(pot.moduleClass, sym) + if (target.hasSource) { + val effs = pot.effectsOf(target).toList + effs.flatMap(check(_)) + } + else Errors.empty + else + // coarse-grained check if in different file + state.dependencies += Dependency(obj.moduleClass)(pot.source) + Errors.empty + + case hot: Hot => + if !state.isThisStatic then + Errors.empty + else if state.isInSameFile(hot.classSymbol) then + // more fine-grained check if in the same source + val target = resolve(hot.classSymbol, sym) + if (target.hasSource) { + val effs = hot.effectsOf(target).toList + effs.flatMap(check(_)) + } + else Errors.empty + else + // coarse-grained check if in different file + state.dependencies += Dependency(hot.classSymbol)(pot.source) + Errors.empty + case _: Cold => CallCold(sym, eff.source, state.path).toErrors @@ -254,6 +302,24 @@ object Checking { if (target.is(Flags.Lazy)) check(MethodCall(pot, target)(eff.source)) else Errors.empty + case hot: Hot => + if state.isThisStatic then + val target = resolve(hot.classSymbol, field) + if (target.is(Flags.Lazy)) check(MethodCall(hot, target)(eff.source)) + else Errors.empty + else + Errors.empty + + case obj: Global => + // for globals, either accessing the object is an error + // or all fields are already initialized + if state.isThisStatic then + val target = resolve(obj.moduleClass, field) + if (target.is(Flags.Lazy)) check(MethodCall(obj, target)(eff.source)) + else Errors.empty + else + Errors.empty + case _: Cold => AccessCold(field, eff.source, state.path).toErrors @@ -293,6 +359,16 @@ object Checking { if (errors.isEmpty) Errors.empty else PromoteWarm(pot, eff.source, state.path).toErrors + case obj: Global => + assert(state.isThisStatic, "encountered global object promotion while checking " + state.thisClass.show) + state.dependencies += Dependency(obj.symbol.moduleClass)(pot.source) + Errors.empty + + case hot: Hot => + if !state.isThisStatic then Errors.empty + state.dependencies += Dependency(hot.classSymbol)(pot.source) + Errors.empty + case Fun(pots, effs) => val errs1 = state.test { effs.toList.flatMap(check(_)) @@ -319,6 +395,17 @@ object Checking { errs } + private def checkAccessGlobal(eff: AccessGlobal)(using state: State): Errors = + val obj = eff.potential + if state.isThisStatic then + if obj.moduleClass != state.thisClass then + state.dependencies += Dependency(obj.symbol)(eff.source) + Errors.empty + else + CyclicObjectInit(obj.symbol, state.path).toErrors + else + Errors.empty + private def expand(pot: Potential)(using state: State): Summary = trace("expand " + pot.show, init, _.asInstanceOf[Summary].show) { pot match { case MethodReturn(pot1, sym) => @@ -352,6 +439,26 @@ object Checking { if (target.hasSource) Summary(warm.potentialsOf(target), Effects.empty) else Summary.empty // warning already issued in call effect + case hot: Hot => + if !state.isThisStatic then + Summary.empty + else if state.isInSameFile(hot.classSymbol) then + val target = resolve(hot.classSymbol, sym) + if (target.hasSource) Summary(hot.potentialsOf(target), Effects.empty) + else Summary.empty + else + Summary.empty // already recorded with effect checking + + case pot @ Global(obj) => + if !state.isThisStatic then + Summary.empty + else if state.isInSameFile(obj) then + val target = resolve(pot.moduleClass, sym) + if (target.hasSource) Summary(pot.potentialsOf(target), Effects.empty) + else Summary.empty + else + Summary.empty // already recorded with effect checking + case _: Cold => Summary.empty // error already reported, ignore @@ -381,6 +488,26 @@ object Checking { if (target.hasSource) Summary(warm.potentialsOf(target), Effects.empty) else Summary(Cold()(pot.source)) + case hot: Hot => + if !state.isThisStatic then + Summary.empty + else if state.isInSameFile(hot.classSymbol) then + val target = resolve(hot.classSymbol, sym) + if (target.hasSource) Summary(hot.potentialsOf(target), Effects.empty) + else Summary.empty + else + Summary.empty + + case pot @ Global(obj) => + if !state.isThisStatic then + Summary.empty + else if state.isInSameFile(obj) then + val target = resolve(pot.moduleClass, sym) + if (target.hasSource) Summary(pot.potentialsOf(target), Effects.empty) + else Summary.empty + else + Summary.empty + case _: Cold => Summary.empty // error already reported, ignore @@ -396,8 +523,8 @@ object Checking { // all outers for `this` are assumed to be hot Summary.empty - case _: Fun => - throw new Exception("Unexpected code reached") + case _: Fun | _: Hot | _: Global => + throw new Exception("Unexpected code reached " + pot.show) case warm: Warm => Summary(warm.resolveOuter(cls)) @@ -408,7 +535,7 @@ object Checking { Summary(pots2, effs) } - case _: ThisRef | _: Fun | _: Warm | _: Cold => + case _: ThisRef | _: Fun | _: Warm | _: Cold | _: Global | _: Hot => Summary(pot) case SuperRef(pot1, supercls) => diff --git a/compiler/src/dotty/tools/dotc/transform/init/CycleChecker.scala b/compiler/src/dotty/tools/dotc/transform/init/CycleChecker.scala new file mode 100644 index 000000000000..602f776986ef --- /dev/null +++ b/compiler/src/dotty/tools/dotc/transform/init/CycleChecker.scala @@ -0,0 +1,168 @@ +package dotty.tools.dotc +package transform +package init + +import core._ +import Flags._ +import Contexts._ +import Types._ +import Symbols._ +import Decorators._ +import printing.SyntaxHighlighting +import reporting.trace +import config.Printers.init + + +import ast.tpd._ + +import scala.collection.mutable + + + +/** The dependencies of a static object or a class + * + * This class is used in checking cyclic initialization of static objects. + * + * For the check to be fast, the algorithm uses a combination of + * coarse-grained approximation and fine-grained abstractions. + * + * Fine-grained abstractions are created from the initialization + * check for static objects. + * + * Coarse-grained abstractions are constructed as follows: + * + * - if a static object `O` is used in another class/static-object `B`, + * then B -> O + * - if `new C` appears in a another class/static-object `B`, + * then B -> C + * - if a static-object/class `A` extends another class `B`, + * then A -> B + * + * Given a dependency graph, we check if there exists cycles. + * + * This check does not need to care about objects in libraries, as separate + * compilation ensures that there cannot be cyles between two separately + * compiled projects. + * + */ +case class Dependency(sym: Symbol)(val source: Tree) { + def show(using Context): String = "Dep(" + sym.show + ")" +} + +class CycleChecker { + private val summaryCache = mutable.Map.empty[Symbol, List[Dependency]] + + val classesInCurrentRun = mutable.Set.empty[Symbol] + val objectsInCurrentRun = mutable.Set.empty[Symbol] + + /** Checking state */ + case class State(visited: mutable.Set[Symbol], path: Vector[Tree]) + + def cacheObjectDependencies(obj: Symbol, deps: List[Dependency]): Unit = + objectsInCurrentRun += obj + summaryCache(obj) = deps + + def dependenciesOf(sym: Symbol)(using Context): List[Dependency] = + if (summaryCache.contains(sym)) summaryCache(sym) + else if (!classesInCurrentRun.contains(sym)) Nil + else trace("summary for " + sym.show, init) { + assert(sym.isClass) + val cls = sym.asClass + val deps = analyze(cls) + summaryCache(cls) = deps + deps + } + + def check()(using Context): Unit = ??? + + def clean() = { + summaryCache.clear() + classesInCurrentRun.clear() + objectsInCurrentRun.clear() + } + + def analyze(cls: ClassSymbol)(using Context): List[Dependency] = { + def isStaticObjectRef(sym: Symbol) = + sym.isTerm && !sym.is(Flags.Package) && sym.is(Flags.Module) && sym.isStatic + + val isStaticObj = isStaticObjectRef(cls) + + if (cls.defTree.isEmpty) return Nil + + val cdef = cls.defTree.asInstanceOf[TypeDef] + val tpl = cdef.rhs.asInstanceOf[Template] + + var deps: List[Dependency] = Nil + + def analyzeType(tp: Type, source: Tree): Unit = tp match { + case (_: ConstantType) | NoPrefix => + + case tmref: TermRef if isStaticObjectRef(tmref.symbol) => + deps = Dependency(tmref.symbol)(source) :: deps + + case tmref: TermRef => + analyzeType(tmref.prefix, source) + + case ThisType(tref) => + if isStaticObjectRef(tref.symbol.sourceModule) && tref.symbol != cls + then + val obj = tref.symbol.sourceModule + deps = Dependency(obj)(source) :: deps + + case SuperType(thisTp, _) => + analyzeType(thisTp, source) + + case _: TypeRef | _: AppliedType => + // possible from parent list + + case _ => + throw new Exception("unexpected type: " + tp) + } + + + val traverser = new TreeTraverser { + override def traverse(tree: Tree)(using Context): Unit = + tree match { + case tree @ Template(_, parents, self, _) => + if !isStaticObj then + parents.foreach(traverse(_)) + tree.body.foreach { + case ddef: DefDef => + traverse(ddef) + case vdef: ValDef if vdef.symbol.is(Flags.Lazy) => + traverse(vdef) + case stat => + if !isStaticObj then traverse(stat) + } + + case tree: RefTree if tree.isTerm => + analyzeType(tree.tpe, tree) + + case tree: This => + analyzeType(tree.tpe, tree) + + case tree: ValOrDefDef => + traverseChildren(tree.rhs) + + case tdef: TypeDef => + // don't go into nested classes + + case tree: New => + deps = Dependency(tree.tpe.classSymbol)(tree) :: deps + + case _ => + traverseChildren(tree) + } + } + + // TODO: the traverser might create duplicate entries for parents + tpl.parents.foreach { tree => + val tp = tree.tpe + deps = Dependency(tp.classSymbol)(tree) :: deps + } + + traverser.traverse(tpl) + deps + } + +} diff --git a/compiler/src/dotty/tools/dotc/transform/init/Effects.scala b/compiler/src/dotty/tools/dotc/transform/init/Effects.scala index ce9d8d8aa497..29d3b0521f26 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Effects.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Effects.scala @@ -58,10 +58,18 @@ object Effects { def show(using Context): String = potential.show + "." + method.name.show + "!" } + /** Accessing a global object */ + case class AccessGlobal(potential: Global) extends Effect { + val source = potential.source + + def show(using Context): String = potential.symbol.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 { + 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) @@ -73,7 +81,9 @@ object Effects { case MethodCall(pot, sym) => val pot1 = Potentials.asSeenFrom(pot, thisValue) MethodCall(pot1, sym)(eff.source) - } } + + case _: AccessGlobal => eff + } def asSeenFrom(effs: Effects, thisValue: Potential)(implicit env: Env): Effects = effs.map(asSeenFrom(_, thisValue)) diff --git a/compiler/src/dotty/tools/dotc/transform/init/Errors.scala b/compiler/src/dotty/tools/dotc/transform/init/Errors.scala index 73b8cd123033..6cdd0138d669 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Errors.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Errors.scala @@ -69,6 +69,15 @@ object Errors { report.warning(show + stacktrace, field.srcPos) } + case class CyclicObjectInit(obj: Symbol, trace: Vector[Tree]) extends Error { + def source: Tree = trace.last + def show(using Context): String = + "Cyclic object initialization of " + obj.show + "." + + override def issue(using Context): Unit = + report.warning(show + stacktrace, obj.srcPos) + } + /** Promote `this` under initialization to fully-initialized */ case class PromoteThis(pot: ThisRef, source: Tree, trace: Vector[Tree]) extends Error { def show(using Context): String = "Promote the value under initialization to fully-initialized." diff --git a/compiler/src/dotty/tools/dotc/transform/init/Potentials.scala b/compiler/src/dotty/tools/dotc/transform/init/Potentials.scala index 39eac29a2741..bbb72e1417bd 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Potentials.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Potentials.scala @@ -32,6 +32,12 @@ object Potentials { def show(using Context): String def source: Tree + def isGlobal: Boolean = + this match + case _: Global => true + case FieldReturn(pot, _) => pot.isGlobal + case _ => false + def toPots: Potentials = Vector(this) } @@ -153,6 +159,21 @@ object Potentials { "Fun[pots = " + potentials.map(_.show).mkString(";") + ", effs = " + effects.map(_.show).mkString(";") + "]" } + /** Reference to a global object */ + case class Global(symbol: Symbol)(val source: Tree) extends Refinable { + def show(using Context): String = symbol.show + + def moduleClass(using Context): ClassSymbol = symbol.moduleClass.asClass + } + + /** The potential of a hot object + * + * A hot object may potentially reference a global object. + */ + case class Hot(classSymbol: ClassSymbol)(val source: Tree) extends Refinable { + def show(using Context): String = "Hot[" + classSymbol.name.show + "]" + } + // ------------------ operations on potentials ------------------ /** Selection on a set of potentials @@ -218,12 +239,12 @@ object Potentials { 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) + + case _: Global | _: Hot | _: Cold => + pot } } diff --git a/compiler/src/dotty/tools/dotc/transform/init/Summarization.scala b/compiler/src/dotty/tools/dotc/transform/init/Summarization.scala index 8da415bf82bb..f266b94ba7ee 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Summarization.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Summarization.scala @@ -40,11 +40,13 @@ object Summarization { analyze(supert.tpe, supert) case Select(qualifier, name) => + val sym = expr.symbol val Summary(pots, effs) = analyze(qualifier) - if (env.canIgnoreMethod(expr.symbol)) Summary(effs) - else if (!expr.symbol.exists) { // polymorphic function apply and structural types + if (env.canIgnoreMethod(sym)) Summary(effs) + else if (!sym.exists) // polymorphic function apply and structural types Summary(pots.promote(expr) ++ effs) - } + else if (sym.is(Flags.Module) && sym.isStatic) + Summary(effs) + Global(sym)(expr) else { val Summary(pots2, effs2) = pots.select(expr.symbol, expr) Summary(pots2, effs ++ effs2) @@ -90,7 +92,7 @@ object Summarization { val thisRef = ThisRef()(expr) val enclosing = cls.owner.lexicallyEnclosingClass.asClass val summary = resolveThis(enclosing, thisRef, cur, expr) - if summary.pots.isEmpty then summary + if summary.pots.isEmpty then summary + Hot(cls)(expr) else { assert(summary.pots.size == 1) summary.dropPotentials + Warm(cls, summary.pots.head)(expr) @@ -98,10 +100,14 @@ object Summarization { } else { val summary = analyze(tref.prefix, expr) - if summary.pots.isEmpty then summary + if summary.pots.isEmpty then summary + Hot(cls)(expr) else { assert(summary.pots.size == 1) - summary.dropPotentials + Warm(cls, summary.pots.head)(expr) + val outer = summary.pots.head + val pot = + if outer.isGlobal then Hot(cls)(expr) + else Warm(cls, outer)(expr) + summary.dropPotentials + pot } } @@ -236,6 +242,18 @@ object Summarization { case tmref: TermRef if tmref.prefix == NoPrefix => Summary.empty + case tmref: TermRef + if tmref.symbol.is(Flags.Module, butNot = Flags.Package) + && tmref.symbol.isStatic + => + val cls = tmref.symbol.moduleClass + if cls == env.ctx.owner.lexicallyEnclosingClass then + // self reference to an object inside the object + Summary(ThisRef()(source)) + else + val pot = Global(tmref.symbol)(source) + Summary(pot) + AccessGlobal(pot) + case tmref: TermRef => val Summary(pots, effs) = analyze(tmref.prefix, source) if (env.canIgnoreMethod(tmref.symbol)) Summary(effs) @@ -245,9 +263,18 @@ object Summarization { } case ThisType(tref) => - val enclosing = env.ctx.owner.lexicallyEnclosingClass.asClass - val cls = tref.symbol.asClass - resolveThis(cls, ThisRef()(source), enclosing, source) + if tref.symbol.is(Flags.Module, butNot = Flags.Package) + && tref.symbol.isStatic + && env.ctx.owner.isStatic + && tref.symbol != env.ctx.owner + then + val sym = tref.symbol.sourceModule + val pot = Global(sym)(source) + Summary(pot) + AccessGlobal(pot) + else + 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) @@ -285,12 +312,13 @@ object Summarization { if (cls.is(Flags.Package)) Summary.empty else if (cls == cur) Summary(pot) else if (pot.size > 2) Summary(Promote(pot)(source)) + else if (cls.is(Flags.Module) && !cur.ownersIterator.exists(_ == cls)) { + // Dotty uses O$.this outside of the object O + val pot = Global(cls.sourceModule)(source) + Summary(pot) + AccessGlobal(pot) + } 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) @@ -378,5 +406,4 @@ object Summarization { ClassSummary(cls, parentOuter.toMap) } } - } diff --git a/tests/init/neg/features-trees2.scala b/tests/init/neg/features-trees2.scala new file mode 100644 index 000000000000..c422d4738ca7 --- /dev/null +++ b/tests/init/neg/features-trees2.scala @@ -0,0 +1,6 @@ +object Trees { + class ValDef { counter += 1 } + class EmptyValDef extends ValDef + val theEmptyValDef = new EmptyValDef + private var counter = 0 // error +} diff --git a/tests/init/neg/inner9.scala b/tests/init/neg/inner9.scala index db5198ea0138..2faf638644fe 100644 --- a/tests/init/neg/inner9.scala +++ b/tests/init/neg/inner9.scala @@ -5,7 +5,6 @@ object Flags { new Flags.Inner - val a = this.b + 3 val b = 5 // error } From d51d4b222ec15780d9df5c8190e57a010b0f349b Mon Sep 17 00:00:00 2001 From: Liu Fengyun Date: Sun, 11 Apr 2021 11:15:10 +0200 Subject: [PATCH 02/47] Introduce hierarchy for dependencies --- .../tools/dotc/transform/init/Checking.scala | 10 ++-- .../dotc/transform/init/CycleChecker.scala | 50 +++++++++++++++++-- .../tools/dotc/transform/init/Errors.scala | 20 ++++---- 3 files changed, 62 insertions(+), 18 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/transform/init/Checking.scala b/compiler/src/dotty/tools/dotc/transform/init/Checking.scala index 95e41d8e130f..046bd14c1f04 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Checking.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Checking.scala @@ -247,7 +247,7 @@ object Checking { else Errors.empty else // coarse-grained check if in different file - state.dependencies += Dependency(obj.moduleClass)(pot.source) + state.dependencies += InstanceUsage(obj.moduleClass)(pot.source) Errors.empty case hot: Hot => @@ -263,7 +263,7 @@ object Checking { else Errors.empty else // coarse-grained check if in different file - state.dependencies += Dependency(hot.classSymbol)(pot.source) + state.dependencies += ClassUsage(hot.classSymbol)(pot.source) Errors.empty case _: Cold => @@ -361,12 +361,12 @@ object Checking { case obj: Global => assert(state.isThisStatic, "encountered global object promotion while checking " + state.thisClass.show) - state.dependencies += Dependency(obj.symbol.moduleClass)(pot.source) + state.dependencies += InstanceUsage(obj.symbol)(pot.source) Errors.empty case hot: Hot => if !state.isThisStatic then Errors.empty - state.dependencies += Dependency(hot.classSymbol)(pot.source) + state.dependencies += InstanceUsage(hot.classSymbol)(pot.source) Errors.empty case Fun(pots, effs) => @@ -399,7 +399,7 @@ object Checking { val obj = eff.potential if state.isThisStatic then if obj.moduleClass != state.thisClass then - state.dependencies += Dependency(obj.symbol)(eff.source) + state.dependencies += ObjectInit(obj.symbol)(eff.source) Errors.empty else CyclicObjectInit(obj.symbol, state.path).toErrors diff --git a/compiler/src/dotty/tools/dotc/transform/init/CycleChecker.scala b/compiler/src/dotty/tools/dotc/transform/init/CycleChecker.scala index 602f776986ef..1ddc69058075 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/CycleChecker.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/CycleChecker.scala @@ -45,8 +45,24 @@ import scala.collection.mutable * compiled projects. * */ -case class Dependency(sym: Symbol)(val source: Tree) { - def show(using Context): String = "Dep(" + sym.show + ")" +trait Dependency { + def symbol: Symbol + def source: Tree +} + +/** Depend on the initialization of another object */ +case class ObjectInit(symbol: Symbol)(val source: Tree) extends Dependency { + def show(using Context): String = "ObjectInit(" + symbol.show + ")" +} + +/** Depend on usage of an instance, which can be either a class instance or object */ +case class InstanceUsage(symbol: Symbol)(val source: Tree) extends Dependency { + def show(using Context): String = "InstanceUsage(" + symbol.show + ")" +} + +/** Depend on the class */ +case class ClassUsage(symbol: Symbol)(val source: Tree) extends Dependency { + def show(using Context): String = "ClassUsage(" + symbol.show + ")" } class CycleChecker { @@ -56,7 +72,7 @@ class CycleChecker { val objectsInCurrentRun = mutable.Set.empty[Symbol] /** Checking state */ - case class State(visited: mutable.Set[Symbol], path: Vector[Tree]) + case class State(visited: mutable.Set[Symbol], path: Set[Symbol], trace: Vector[Dependency]) def cacheObjectDependencies(obj: Symbol, deps: List[Dependency]): Unit = objectsInCurrentRun += obj @@ -75,6 +91,34 @@ class CycleChecker { def check()(using Context): Unit = ??? + private def visit(sym: Symbol, state: State, source: Tree)(using Context): List[Error] = trace("checking " + sym.show, init) { + if state.path.contains(sym) then + val cycle = state.trace.dropWhile(_.symbol != sym) + val objectNum = cycle.filter(dep => dep.symbol.is(Flags.Module) && dep.symbol.isStatic).size + val trace = cycle.map(_.source) :+ source + val error = CyclicObjectInit(sym, trace) + error :: Nil + else if state.visited.contains(sym) then + Nil + else + state.visited += sym + var path = state.path + + if sym.is(Flags.Module) && sym.isStatic then + path = state.path + dep.symbol + if sym.isTerm then + + val state2 = state.copy(path = path, trace = trace :+ dep.source) + val deps = dependenciesOf(cls) + Util.traceIndented("dependencies of " + sym.show + " = " + deps.map(_.sym.show).mkString(","), init) + var res: List[Error] = Nil + // TODO: stop early + deps.foreach { dep => + res = visit(dep.sym, state2, dep.source) + } + res + } + def clean() = { summaryCache.clear() classesInCurrentRun.clear() diff --git a/compiler/src/dotty/tools/dotc/transform/init/Errors.scala b/compiler/src/dotty/tools/dotc/transform/init/Errors.scala index 6cdd0138d669..f2a39470b339 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Errors.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Errors.scala @@ -20,7 +20,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 +60,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 + "." @@ -69,7 +69,7 @@ object Errors { report.warning(show + stacktrace, field.srcPos) } - case class CyclicObjectInit(obj: Symbol, trace: Vector[Tree]) extends Error { + case class CyclicObjectInit(obj: Symbol, trace: Seq[Tree]) extends Error { def source: Tree = trace.last def show(using Context): String = "Cyclic object initialization of " + obj.show + "." @@ -79,39 +79,39 @@ object Errors { } /** Promote `this` under initialization to fully-initialized */ - case class PromoteThis(pot: ThisRef, source: Tree, trace: Vector[Tree]) extends Error { + case class PromoteThis(pot: ThisRef, source: Tree, trace: Seq[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(pot: Warm, source: Tree, trace: Vector[Tree]) extends Error { + case class PromoteWarm(pot: Warm, source: Tree, trace: Seq[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 { + case class PromoteCold(source: Tree, trace: Seq[Tree]) extends Error { def show(using Context): String = "Promoting the value " + source.show + " to fully-initialized while it is under initialization" + "." } - 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 = "Calling the external method " + meth.show + " may cause initialization errors" + "." } /** Promote a value under initialization to fully-initialized */ - case class UnsafePromotion(pot: Potential, source: Tree, trace: Vector[Tree], errors: Errors) extends Error { + case class UnsafePromotion(pot: Potential, source: Tree, trace: Seq[Tree], errors: Errors) extends Error { assert(errors.nonEmpty) override def issue(using Context): Unit = From 9c418289b8beb2a6482efd8405d3787b9a414c19 Mon Sep 17 00:00:00 2001 From: Liu Fengyun Date: Thu, 15 Apr 2021 11:57:59 +0200 Subject: [PATCH 03/47] Add StaticCall dependencies --- .../tools/dotc/transform/init/Checker.scala | 6 +- .../tools/dotc/transform/init/Checking.scala | 110 ++------ .../dotc/transform/init/CycleChecker.scala | 249 +++++++++++------- 3 files changed, 181 insertions(+), 184 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/transform/init/Checker.scala b/compiler/src/dotty/tools/dotc/transform/init/Checker.scala index 424de3ba83ae..f8091f936fef 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Checker.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Checker.scala @@ -36,14 +36,13 @@ class Checker extends Phase { override def runOn(units: List[CompilationUnit])(using Context): List[CompilationUnit] = cycleChecker.clean() val newUnits = super.runOn(units) - cycleChecker.check() + cycleChecker.checkCyclic() newUnits val traverser = new TreeTraverser { override def traverse(tree: Tree)(using Context): Unit = tree match { case tdef: TypeDef if tdef.isClassDef => - cycleChecker.classesInCurrentRun += tdef.symbol checkClassDef(tdef) traverseChildren(tree) case _ => @@ -85,8 +84,7 @@ class Checker extends Phase { Checking.checkClassBody(tree) - if cls.is(Flags.Module) && cls.isStatic then - cycleChecker.cacheObjectDependencies(cls.sourceModule, state.dependencies.toList) + cycleChecker.cacheConstructorDependencies(cls.primaryConstructor, state.dependencies.toList) } tree diff --git a/compiler/src/dotty/tools/dotc/transform/init/Checking.scala b/compiler/src/dotty/tools/dotc/transform/init/Checking.scala index 046bd14c1f04..d5bd364a2aec 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Checking.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Checking.scala @@ -67,7 +67,7 @@ object Checking { 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 = { + 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) @@ -82,7 +82,6 @@ object Checking { case eff: AccessGlobal => Checking.checkAccessGlobal(eff) } } - } private def checkEffects(effs: Effects)(using state: State): Unit = traceOp("checking effects " + Effects.show(effs), init) { for { @@ -234,37 +233,10 @@ object Checking { else Errors.empty - case pot @ Global(obj) => - if !state.isThisStatic then - Errors.empty - else if state.isInSameFile(obj) then - // more fine-grained check if in the same source - val target = resolve(pot.moduleClass, sym) - if (target.hasSource) { - val effs = pot.effectsOf(target).toList - effs.flatMap(check(_)) - } - else Errors.empty - else - // coarse-grained check if in different file - state.dependencies += InstanceUsage(obj.moduleClass)(pot.source) - Errors.empty - - case hot: Hot => - if !state.isThisStatic then - Errors.empty - else if state.isInSameFile(hot.classSymbol) then - // more fine-grained check if in the same source - val target = resolve(hot.classSymbol, sym) - if (target.hasSource) { - val effs = hot.effectsOf(target).toList - effs.flatMap(check(_)) - } - else Errors.empty - else - // coarse-grained check if in different file - state.dependencies += ClassUsage(hot.classSymbol)(pot.source) - Errors.empty + case _: Hot | _: Global => + // coarse-grained check if in different file + state.dependencies += StaticCall(sym)(pot.source) + Errors.empty case _: Cold => CallCold(sym, eff.source, state.path).toErrors @@ -303,22 +275,16 @@ object Checking { else Errors.empty case hot: Hot => - if state.isThisStatic then - val target = resolve(hot.classSymbol, field) - if (target.is(Flags.Lazy)) check(MethodCall(hot, target)(eff.source)) - else Errors.empty - else - Errors.empty + val target = resolve(hot.classSymbol, field) + if (target.is(Flags.Lazy)) check(MethodCall(hot, target)(eff.source)) + else Errors.empty case obj: Global => // for globals, either accessing the object is an error // or all fields are already initialized - if state.isThisStatic then - val target = resolve(obj.moduleClass, field) - if (target.is(Flags.Lazy)) check(MethodCall(obj, target)(eff.source)) - else Errors.empty - else - Errors.empty + val target = resolve(obj.moduleClass, field) + if (target.is(Flags.Lazy)) check(MethodCall(obj, target)(eff.source)) + else Errors.empty case _: Cold => AccessCold(field, eff.source, state.path).toErrors @@ -361,7 +327,7 @@ object Checking { case obj: Global => assert(state.isThisStatic, "encountered global object promotion while checking " + state.thisClass.show) - state.dependencies += InstanceUsage(obj.symbol)(pot.source) + state.dependencies += InstanceUsage(obj.moduleClass)(pot.source) Errors.empty case hot: Hot => @@ -397,14 +363,8 @@ object Checking { private def checkAccessGlobal(eff: AccessGlobal)(using state: State): Errors = val obj = eff.potential - if state.isThisStatic then - if obj.moduleClass != state.thisClass then - state.dependencies += ObjectInit(obj.symbol)(eff.source) - Errors.empty - else - CyclicObjectInit(obj.symbol, state.path).toErrors - else - Errors.empty + state.dependencies += ObjectAccess(obj.symbol)(eff.source) + Errors.empty private def expand(pot: Potential)(using state: State): Summary = trace("expand " + pot.show, init, _.asInstanceOf[Summary].show) { pot match { @@ -439,25 +399,8 @@ object Checking { if (target.hasSource) Summary(warm.potentialsOf(target), Effects.empty) else Summary.empty // warning already issued in call effect - case hot: Hot => - if !state.isThisStatic then - Summary.empty - else if state.isInSameFile(hot.classSymbol) then - val target = resolve(hot.classSymbol, sym) - if (target.hasSource) Summary(hot.potentialsOf(target), Effects.empty) - else Summary.empty - else - Summary.empty // already recorded with effect checking - - case pot @ Global(obj) => - if !state.isThisStatic then - Summary.empty - else if state.isInSameFile(obj) then - val target = resolve(pot.moduleClass, sym) - if (target.hasSource) Summary(pot.potentialsOf(target), Effects.empty) - else Summary.empty - else - Summary.empty // already recorded with effect checking + case _: Hot | _: Global => + Summary(Promote(pot)(pot.source)) case _: Cold => Summary.empty // error already reported, ignore @@ -488,25 +431,8 @@ object Checking { if (target.hasSource) Summary(warm.potentialsOf(target), Effects.empty) else Summary(Cold()(pot.source)) - case hot: Hot => - if !state.isThisStatic then - Summary.empty - else if state.isInSameFile(hot.classSymbol) then - val target = resolve(hot.classSymbol, sym) - if (target.hasSource) Summary(hot.potentialsOf(target), Effects.empty) - else Summary.empty - else - Summary.empty - - case pot @ Global(obj) => - if !state.isThisStatic then - Summary.empty - else if state.isInSameFile(obj) then - val target = resolve(pot.moduleClass, sym) - if (target.hasSource) Summary(pot.potentialsOf(target), Effects.empty) - else Summary.empty - else - Summary.empty + case _: Hot | _: Global => + Summary(Promote(pot)(pot.source)) case _: Cold => Summary.empty // error already reported, ignore diff --git a/compiler/src/dotty/tools/dotc/transform/init/CycleChecker.scala b/compiler/src/dotty/tools/dotc/transform/init/CycleChecker.scala index 1ddc69058075..3b5b51d5cbcd 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/CycleChecker.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/CycleChecker.scala @@ -11,10 +11,10 @@ import Decorators._ import printing.SyntaxHighlighting import reporting.trace import config.Printers.init - - import ast.tpd._ +import Errors._ + import scala.collection.mutable @@ -48,11 +48,12 @@ import scala.collection.mutable trait Dependency { def symbol: Symbol def source: Tree + def show(using Context): String } /** Depend on the initialization of another object */ -case class ObjectInit(symbol: Symbol)(val source: Tree) extends Dependency { - def show(using Context): String = "ObjectInit(" + symbol.show + ")" +case class ObjectAccess(symbol: Symbol)(val source: Tree) extends Dependency { + def show(using Context): String = "ObjectAccess(" + symbol.show + ")" } /** Depend on usage of an instance, which can be either a class instance or object */ @@ -60,130 +61,163 @@ case class InstanceUsage(symbol: Symbol)(val source: Tree) extends Dependency { def show(using Context): String = "InstanceUsage(" + symbol.show + ")" } -/** Depend on the class */ +/** A static method call detected from fine-grained analysis + * + * The method can be either on a static object or on a hot object. + * The target of the call is determined statically. + */ +case class StaticCall(symbol: Symbol)(val source: Tree) extends Dependency { + def show(using Context): String = "StaticCall(" + symbol.show + ")" +} + +/** A class is used + * + * This is a coarse-grained abstraction + */ case class ClassUsage(symbol: Symbol)(val source: Tree) extends Dependency { def show(using Context): String = "ClassUsage(" + symbol.show + ")" } + class CycleChecker { private val summaryCache = mutable.Map.empty[Symbol, List[Dependency]] - val classesInCurrentRun = mutable.Set.empty[Symbol] - val objectsInCurrentRun = mutable.Set.empty[Symbol] + private val classesInCurrentRun = mutable.Set.empty[Symbol] + private val objectsInCurrentRun = mutable.Set.empty[Symbol] /** Checking state */ - case class State(visited: mutable.Set[Symbol], path: Set[Symbol], trace: Vector[Dependency]) - - def cacheObjectDependencies(obj: Symbol, deps: List[Dependency]): Unit = - objectsInCurrentRun += obj - summaryCache(obj) = deps - - def dependenciesOf(sym: Symbol)(using Context): List[Dependency] = - if (summaryCache.contains(sym)) summaryCache(sym) - else if (!classesInCurrentRun.contains(sym)) Nil - else trace("summary for " + sym.show, init) { - assert(sym.isClass) - val cls = sym.asClass - val deps = analyze(cls) - summaryCache(cls) = deps - deps - } + case class State(visited: mutable.Set[Dependency], path: Vector[Symbol], trace: Vector[Dependency]) { + def visit[T](dep: Dependency)(op: State ?=> T): T = + this.visited += dep + val state2: State = this.copy(trace = trace :+ dep) + val res = op(using state2) + res - def check()(using Context): Unit = ??? - - private def visit(sym: Symbol, state: State, source: Tree)(using Context): List[Error] = trace("checking " + sym.show, init) { - if state.path.contains(sym) then - val cycle = state.trace.dropWhile(_.symbol != sym) - val objectNum = cycle.filter(dep => dep.symbol.is(Flags.Module) && dep.symbol.isStatic).size - val trace = cycle.map(_.source) :+ source - val error = CyclicObjectInit(sym, trace) - error :: Nil - else if state.visited.contains(sym) then - Nil - else - state.visited += sym - var path = state.path - - if sym.is(Flags.Module) && sym.isStatic then - path = state.path + dep.symbol - if sym.isTerm then - - val state2 = state.copy(path = path, trace = trace :+ dep.source) - val deps = dependenciesOf(cls) - Util.traceIndented("dependencies of " + sym.show + " = " + deps.map(_.sym.show).mkString(","), init) - var res: List[Error] = Nil - // TODO: stop early - deps.foreach { dep => - res = visit(dep.sym, state2, dep.source) - } + def withPath[T](obj: Symbol)(op: State ?=> T): T = + val state2 = this.copy(path = path :+ obj) + val res = op(using state2) res - } - def clean() = { - summaryCache.clear() - classesInCurrentRun.clear() - objectsInCurrentRun.clear() } - def analyze(cls: ClassSymbol)(using Context): List[Dependency] = { - def isStaticObjectRef(sym: Symbol) = - sym.isTerm && !sym.is(Flags.Package) && sym.is(Flags.Module) && sym.isStatic + def state(using ev: State) = ev + +// ----- checking ------------------------------- + def checkCyclic(): Unit = ??? + + private def check(dep: Dependency)(using Context, State): List[Error] = + trace("checking dependency " + dep.show, init, errs => Errors.show(errs.asInstanceOf[Errors])) { + if state.visited.contains(dep) then + Nil + else + state.visit(dep) { + dep match + case dep: ObjectAccess => checkObjectAccess(dep) + case dep: InstanceUsage => checkInstanceUsage(dep) + case dep: StaticCall => checkStaticCall(dep) + case dep: ClassUsage => checkClassUsage(dep) + } + } - val isStaticObj = isStaticObjectRef(cls) + private def checkObjectAccess(dep: ObjectAccess)(using Context, State): List[Error] = + val obj = dep.symbol + if state.path.contains(obj) then + val cycle = state.path.dropWhile(_ != obj) + if cycle.size > 1 then + val trace = state.trace.map(_.source) :+ dep.source + val error = CyclicObjectInit(obj, trace) + error :: Nil + else + // TODO: issue a warning for access an object outside its scope during its initialization + Nil + else + val constr = obj.primaryConstructor + state.withPath(obj) { + check(StaticCall(constr)(dep.source)) + } - if (cls.defTree.isEmpty) return Nil - val cdef = cls.defTree.asInstanceOf[TypeDef] - val tpl = cdef.rhs.asInstanceOf[Template] + private def checkInstanceUsage(dep: InstanceUsage)(using Context, State): List[Error] = + if !classesInCurrentRun.contains(dep.symbol) then Nil + else { + val cls = dep.symbol + val deps = classDependencies(cls, excludeInit = true) + deps.flatMap(check(_)) + } - var deps: List[Dependency] = Nil + private def checkStaticCall(dep: StaticCall)(using Context, State): List[Error] = + val cls = dep.symbol.owner + if !classesInCurrentRun.contains(dep.symbol) then Nil + else { + val sym = dep.symbol + val deps = methodDependencies(sym) + deps.flatMap(check(_)) + } - def analyzeType(tp: Type, source: Tree): Unit = tp match { - case (_: ConstantType) | NoPrefix => + private def checkClassUsage(dep: ClassUsage)(using Context, State): List[Error] = + if !classesInCurrentRun.contains(dep.symbol) then Nil + else { + val cls = dep.symbol + val deps = classDependencies(cls, excludeInit = false) + deps.flatMap(check(_)) + } - case tmref: TermRef if isStaticObjectRef(tmref.symbol) => - deps = Dependency(tmref.symbol)(source) :: deps +// ----- analysis of dependencies ------------------------------- - case tmref: TermRef => - analyzeType(tmref.prefix, source) + def cacheConstructorDependencies(constr: Symbol, deps: List[Dependency])(using Context): Unit = + summaryCache(constr) = deps + val cls = constr.owner + if cls.is(Flags.Module) && cls.isStatic then + objectsInCurrentRun += cls.sourceModule + else + classesInCurrentRun += cls - case ThisType(tref) => - if isStaticObjectRef(tref.symbol.sourceModule) && tref.symbol != cls - then - val obj = tref.symbol.sourceModule - deps = Dependency(obj)(source) :: deps + private def classDependencies(sym: Symbol, excludeInit: Boolean)(using Context): List[Dependency] = + if (summaryCache.contains(sym)) summaryCache(sym) + else trace("summary for " + sym.show, init) { + val cls = sym.asClass + val deps = analyzeClass(cls, excludeInit) + summaryCache(cls) = deps + deps + } - case SuperType(thisTp, _) => - analyzeType(thisTp, source) + private def methodDependencies(sym: Symbol)(using Context): List[Dependency] = + if (summaryCache.contains(sym)) summaryCache(sym) + else trace("summary for " + sym.show) { + val deps = analyzeMethod(sym) + summaryCache(sym) = deps + deps + } - case _: TypeRef | _: AppliedType => - // possible from parent list + def isStaticObjectRef(sym: Symbol)(using Context) = + sym.isTerm && !sym.is(Flags.Package) && sym.is(Flags.Module) && sym.isStatic - case _ => - throw new Exception("unexpected type: " + tp) - } + private def analyzeClass(cls: ClassSymbol, excludeInit: Boolean)(using Context): List[Dependency] = { + val cdef = cls.defTree.asInstanceOf[TypeDef] + val tpl = cdef.rhs.asInstanceOf[Template] + var deps: List[Dependency] = Nil val traverser = new TreeTraverser { override def traverse(tree: Tree)(using Context): Unit = tree match { case tree @ Template(_, parents, self, _) => - if !isStaticObj then + if !excludeInit then parents.foreach(traverse(_)) tree.body.foreach { - case ddef: DefDef => + case ddef: DefDef if ddef.symbol.isConstructor => traverse(ddef) case vdef: ValDef if vdef.symbol.is(Flags.Lazy) => traverse(vdef) case stat => - if !isStaticObj then traverse(stat) + if !excludeInit then traverse(stat) } case tree: RefTree if tree.isTerm => - analyzeType(tree.tpe, tree) + analyzeType(tree.tpe, tree, exclude = cls) case tree: This => - analyzeType(tree.tpe, tree) + analyzeType(tree.tpe, tree, exclude = cls) case tree: ValOrDefDef => traverseChildren(tree.rhs) @@ -192,7 +226,7 @@ class CycleChecker { // don't go into nested classes case tree: New => - deps = Dependency(tree.tpe.classSymbol)(tree) :: deps + deps = ClassUsage(tree.tpe.classSymbol)(tree) :: deps case _ => traverseChildren(tree) @@ -202,11 +236,50 @@ class CycleChecker { // TODO: the traverser might create duplicate entries for parents tpl.parents.foreach { tree => val tp = tree.tpe - deps = Dependency(tp.classSymbol)(tree) :: deps + val dep = + if excludeInit then InstanceUsage(tp.classSymbol)(tree) + else ClassUsage(tp.classSymbol)(tree) + + deps = dep :: deps } traverser.traverse(tpl) deps } + private def analyzeType(tp: Type, source: Tree, exclude: Symbol)(using Context): Unit = tp match { + case (_: ConstantType) | NoPrefix => + + case tmref: TermRef if isStaticObjectRef(tmref.symbol) => + ObjectAccess(tmref.symbol)(source) :: Nil + + case tmref: TermRef => + analyzeType(tmref.prefix, source, exclude) + + case ThisType(tref) => + if isStaticObjectRef(tref.symbol.sourceModule) && tref.symbol != exclude + then + val obj = tref.symbol.sourceModule + ObjectAccess(obj)(source) :: Nil + + case SuperType(thisTp, _) => + analyzeType(thisTp, source, exclude) + + case _: TypeRef | _: AppliedType => + // possible from parent list + + case _ => + throw new Exception("unexpected type: " + tp) + } + + + private def analyzeMethod(meth: Symbol)(using Context): List[Dependency] = ??? + +// ----- cleanup ------------------------ + + def clean() = { + summaryCache.clear() + classesInCurrentRun.clear() + objectsInCurrentRun.clear() + } } From ac985ba2e0080f1c3d353d8bf7ef0a73488ddacd Mon Sep 17 00:00:00 2001 From: Liu Fengyun Date: Thu, 15 Apr 2021 12:35:56 +0200 Subject: [PATCH 04/47] Add instance class to StaticCall --- .../src/dotty/tools/dotc/transform/init/Checking.scala | 8 +++++--- .../dotty/tools/dotc/transform/init/CycleChecker.scala | 6 +++--- 2 files changed, 8 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 d5bd364a2aec..24a4e06247da 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Checking.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Checking.scala @@ -233,9 +233,11 @@ object Checking { else Errors.empty - case _: Hot | _: Global => - // coarse-grained check if in different file - state.dependencies += StaticCall(sym)(pot.source) + case pot @ (_: Hot | _: Global) => + val cls = pot match + case hot: Hot => hot.classSymbol + case obj: Global => obj.moduleClass + state.dependencies += StaticCall(cls, sym)(pot.source) Errors.empty case _: Cold => diff --git a/compiler/src/dotty/tools/dotc/transform/init/CycleChecker.scala b/compiler/src/dotty/tools/dotc/transform/init/CycleChecker.scala index 3b5b51d5cbcd..6d357ec29aa4 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/CycleChecker.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/CycleChecker.scala @@ -66,8 +66,8 @@ case class InstanceUsage(symbol: Symbol)(val source: Tree) extends Dependency { * The method can be either on a static object or on a hot object. * The target of the call is determined statically. */ -case class StaticCall(symbol: Symbol)(val source: Tree) extends Dependency { - def show(using Context): String = "StaticCall(" + symbol.show + ")" +case class StaticCall(cls: Symbol, symbol: Symbol)(val source: Tree) extends Dependency { + def show(using Context): String = "StaticCall(" + cls.show + ", " + symbol.show + ")" } /** A class is used @@ -133,7 +133,7 @@ class CycleChecker { else val constr = obj.primaryConstructor state.withPath(obj) { - check(StaticCall(constr)(dep.source)) + check(StaticCall(constr.owner, constr)(dep.source)) } From e12a8c26b7a74d0e031529973894c045216fcf17 Mon Sep 17 00:00:00 2001 From: Liu Fengyun Date: Thu, 15 Apr 2021 15:55:39 +0200 Subject: [PATCH 05/47] Add missing instance usage dependency --- .../dotc/transform/init/CycleChecker.scala | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/transform/init/CycleChecker.scala b/compiler/src/dotty/tools/dotc/transform/init/CycleChecker.scala index 6d357ec29aa4..c8a5d7c7bee4 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/CycleChecker.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/CycleChecker.scala @@ -23,13 +23,13 @@ import scala.collection.mutable * * This class is used in checking cyclic initialization of static objects. * - * For the check to be fast, the algorithm uses a combination of - * coarse-grained approximation and fine-grained abstractions. + * For the check to be simple and fast, the algorithm uses a combination of + * coarse-grained analysis and fine-grained analysis. * * Fine-grained abstractions are created from the initialization * check for static objects. * - * Coarse-grained abstractions are constructed as follows: + * Coarse-grained abstractions are created as follows: * * - if a static object `O` is used in another class/static-object `B`, * then B -> O @@ -146,11 +146,9 @@ class CycleChecker { } private def checkStaticCall(dep: StaticCall)(using Context, State): List[Error] = - val cls = dep.symbol.owner - if !classesInCurrentRun.contains(dep.symbol) then Nil + if !classesInCurrentRun.contains(dep.cls) then Nil else { - val sym = dep.symbol - val deps = methodDependencies(sym) + val deps = methodDependencies(dep.cls, dep.symbol) deps.flatMap(check(_)) } @@ -181,10 +179,10 @@ class CycleChecker { deps } - private def methodDependencies(sym: Symbol)(using Context): List[Dependency] = + private def methodDependencies(cls: Symbol, sym: Symbol)(using Context): List[Dependency] = if (summaryCache.contains(sym)) summaryCache(sym) else trace("summary for " + sym.show) { - val deps = analyzeMethod(sym) + val deps = analyzeMethod(cls, sym) summaryCache(sym) = deps deps } @@ -251,7 +249,8 @@ class CycleChecker { case (_: ConstantType) | NoPrefix => case tmref: TermRef if isStaticObjectRef(tmref.symbol) => - ObjectAccess(tmref.symbol)(source) :: Nil + val obj = tmref.symbol + ObjectAccess(obj)(source) :: InstanceUsage(obj.moduleClass)(source) :: Nil case tmref: TermRef => analyzeType(tmref.prefix, source, exclude) @@ -259,8 +258,9 @@ class CycleChecker { case ThisType(tref) => if isStaticObjectRef(tref.symbol.sourceModule) && tref.symbol != exclude then - val obj = tref.symbol.sourceModule - ObjectAccess(obj)(source) :: Nil + val cls = tref.symbol + val obj = cls.sourceModule + ObjectAccess(obj)(source) :: InstanceUsage(cls)(source) :: Nil case SuperType(thisTp, _) => analyzeType(thisTp, source, exclude) From 252bf69c334b939a8feb093fee7ee7eaf33387a6 Mon Sep 17 00:00:00 2001 From: Liu Fengyun Date: Thu, 15 Apr 2021 16:50:54 +0200 Subject: [PATCH 06/47] Fine-grained analysis of method dependencies --- .../tools/dotc/transform/init/Checker.scala | 2 +- .../tools/dotc/transform/init/Checking.scala | 36 ++++++--------- .../dotc/transform/init/CycleChecker.scala | 44 ++++++++++++++----- .../dotc/transform/init/Potentials.scala | 4 +- 4 files changed, 50 insertions(+), 36 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/transform/init/Checker.scala b/compiler/src/dotty/tools/dotc/transform/init/Checker.scala index f8091f936fef..184c7136c325 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Checker.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Checker.scala @@ -26,7 +26,7 @@ class Checker extends Phase { // cache of class summary private val cache = new Cache - private val cycleChecker = new CycleChecker + private val cycleChecker = new CycleChecker(cache) override val runsAfter = Set(Pickler.name) diff --git a/compiler/src/dotty/tools/dotc/transform/init/Checking.scala b/compiler/src/dotty/tools/dotc/transform/init/Checking.scala index 24a4e06247da..718d276613b3 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Checking.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Checking.scala @@ -46,11 +46,9 @@ object Checking { this.visited = state.visited res - def isThisStatic(using Context): Boolean = - thisClass.is(Flags.Module) && thisClass.sourceModule.isStatic - def isInSameFile(that: Symbol)(using Context): Boolean = - that.source == that.source + def isFieldInitialized(field: Symbol): Boolean = + fieldsInited.contains(field) def visit[T](eff: Effect)(op: State ?=> T): T = val state: State = this.copy(path = path :+ eff.source, visited = this.visited + eff) @@ -67,7 +65,7 @@ object Checking { 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 = + private[init] 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) @@ -168,23 +166,19 @@ object Checking { tpl.parents.foreach { case tree @ Block(stats, parent) => - if state.isThisStatic then - val argss = termArgss(parent) - checkStats((stats :: argss).flatten, cls) - + val argss = termArgss(parent) + checkStats((stats :: argss).flatten, cls) checkConstructor(funPart(parent).symbol, parent.tpe, tree) case tree @ Apply(Block(stats, parent), args) => - if state.isThisStatic then - val argss = termArgss(parent) - checkStats((stats :: args :: argss).flatten, cls) + val argss = termArgss(parent) + checkStats((stats :: args :: argss).flatten, cls) checkConstructor(funPart(parent).symbol, tree.tpe, tree) case parent : Apply => - if state.isThisStatic then - val argss = termArgss(parent) - checkStats(argss.flatten, cls) + val argss = termArgss(parent) + checkStats(argss.flatten, cls) checkConstructor(funPart(parent).symbol, parent.tpe, parent) @@ -261,13 +255,13 @@ object Checking { 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 if (!state.isFieldInitialized(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 if (!state.isFieldInitialized(target)) AccessNonInit(target, state.path).toErrors else Errors.empty case Warm(cls, outer) => @@ -312,7 +306,7 @@ object Checking { val classRef = state.thisClass.info.asInstanceOf[ClassInfo].appliedRef val allFieldsInited = classRef.fields.forall { denot => val sym = denot.symbol - sym.isOneOf(Flags.Lazy | Flags.Deferred) || state.fieldsInited.contains(sym) + sym.isOneOf(Flags.Lazy | Flags.Deferred) || state.isFieldInitialized(sym) } if (allFieldsInited) Errors.empty @@ -328,12 +322,10 @@ object Checking { else PromoteWarm(pot, eff.source, state.path).toErrors case obj: Global => - assert(state.isThisStatic, "encountered global object promotion while checking " + state.thisClass.show) state.dependencies += InstanceUsage(obj.moduleClass)(pot.source) Errors.empty case hot: Hot => - if !state.isThisStatic then Errors.empty state.dependencies += InstanceUsage(hot.classSymbol)(pot.source) Errors.empty @@ -447,11 +439,11 @@ object Checking { case Outer(pot1, cls) => pot1 match { - case _: ThisRef => + case _: ThisRef | _: Hot | _: Global => // all outers for `this` are assumed to be hot Summary.empty - case _: Fun | _: Hot | _: Global => + case _: Fun => throw new Exception("Unexpected code reached " + pot.show) case warm: Warm => diff --git a/compiler/src/dotty/tools/dotc/transform/init/CycleChecker.scala b/compiler/src/dotty/tools/dotc/transform/init/CycleChecker.scala index c8a5d7c7bee4..4180ad24c8d1 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/CycleChecker.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/CycleChecker.scala @@ -13,7 +13,7 @@ import reporting.trace import config.Printers.init import ast.tpd._ -import Errors._ +import Errors._, Potentials._, Effects._ import scala.collection.mutable @@ -66,7 +66,7 @@ case class InstanceUsage(symbol: Symbol)(val source: Tree) extends Dependency { * The method can be either on a static object or on a hot object. * The target of the call is determined statically. */ -case class StaticCall(cls: Symbol, symbol: Symbol)(val source: Tree) extends Dependency { +case class StaticCall(cls: ClassSymbol, symbol: Symbol)(val source: Tree) extends Dependency { def show(using Context): String = "StaticCall(" + cls.show + ", " + symbol.show + ")" } @@ -79,7 +79,7 @@ case class ClassUsage(symbol: Symbol)(val source: Tree) extends Dependency { } -class CycleChecker { +class CycleChecker(cache: Cache) { private val summaryCache = mutable.Map.empty[Symbol, List[Dependency]] private val classesInCurrentRun = mutable.Set.empty[Symbol] @@ -133,7 +133,7 @@ class CycleChecker { else val constr = obj.primaryConstructor state.withPath(obj) { - check(StaticCall(constr.owner, constr)(dep.source)) + check(StaticCall(constr.owner.asClass, constr)(dep.source)) } @@ -148,7 +148,7 @@ class CycleChecker { private def checkStaticCall(dep: StaticCall)(using Context, State): List[Error] = if !classesInCurrentRun.contains(dep.cls) then Nil else { - val deps = methodDependencies(dep.cls, dep.symbol) + val deps = methodDependencies(dep) deps.flatMap(check(_)) } @@ -179,11 +179,11 @@ class CycleChecker { deps } - private def methodDependencies(cls: Symbol, sym: Symbol)(using Context): List[Dependency] = - if (summaryCache.contains(sym)) summaryCache(sym) - else trace("summary for " + sym.show) { - val deps = analyzeMethod(cls, sym) - summaryCache(sym) = deps + private def methodDependencies(call: StaticCall)(using Context): List[Dependency] = + if (summaryCache.contains(call.symbol)) summaryCache(call.symbol) + else trace("summary for " + call.symbol.show) { + val deps = analyzeMethod(call) + summaryCache(call.symbol) = deps deps } @@ -272,8 +272,30 @@ class CycleChecker { throw new Exception("unexpected type: " + tp) } + private def analyzeMethod(dep: StaticCall)(using Context): List[Dependency] = { + val env = Env(ctx.withOwner(dep.cls), cache) + val state = new Checking.State( + visited = Set.empty, + path = Vector.empty, + thisClass = dep.cls, + fieldsInited = mutable.Set.empty, + parentsInited = mutable.Set.empty, + safePromoted = mutable.Set(ThisRef()(dep.cls.defTree)), + dependencies = mutable.Set.empty, + env = env + ) { + override def isFieldInitialized(field: Symbol): Boolean = true + } + + val pot = Hot(dep.cls)(dep.source) + val effs = pot.effectsOf(dep.symbol)(using env) + + val errs = effs.flatMap(Checking.check(_)(using state)) + assert(errs.isEmpty, "unexpected errors: " + Errors.show(errs.toList)) + + state.dependencies.toList + } - private def analyzeMethod(meth: Symbol)(using Context): List[Dependency] = ??? // ----- cleanup ------------------------ diff --git a/compiler/src/dotty/tools/dotc/transform/init/Potentials.scala b/compiler/src/dotty/tools/dotc/transform/init/Potentials.scala index bbb72e1417bd..81357700b12e 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Potentials.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Potentials.scala @@ -46,7 +46,7 @@ object Potentials { * * The method performs prefix substitution */ - def effectsOf(sym: Symbol)(implicit env: Env): Effects = trace("effects of " + sym.show, init, r => Effects.show(r.asInstanceOf)) { + def effectsOf(sym: Symbol)(using 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 @@ -58,7 +58,7 @@ object Potentials { * * The method performs prefix substitution */ - def potentialsOf(sym: Symbol)(implicit env: Env): Potentials = trace("potentials of " + sym.show, init, r => Potentials.show(r.asInstanceOf)) { + def potentialsOf(sym: Symbol)(using 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 From 05f7a25f37cc1d359fee8a8399f835b1dfc47859 Mon Sep 17 00:00:00 2001 From: Liu Fengyun Date: Thu, 15 Apr 2021 17:16:58 +0200 Subject: [PATCH 07/47] Implement checkCyclic --- .../dotty/tools/dotc/transform/init/CycleChecker.scala | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/compiler/src/dotty/tools/dotc/transform/init/CycleChecker.scala b/compiler/src/dotty/tools/dotc/transform/init/CycleChecker.scala index 4180ad24c8d1..ee95fa6c0713 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/CycleChecker.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/CycleChecker.scala @@ -103,7 +103,14 @@ class CycleChecker(cache: Cache) { def state(using ev: State) = ev // ----- checking ------------------------------- - def checkCyclic(): Unit = ??? + def checkCyclic()(using Context): Unit = { + val state = State(visited = mutable.Set.empty, path = Vector.empty, trace = Vector.empty) + objectsInCurrentRun.foreach { obj => + val dep = ObjectAccess(obj)(obj.defTree) + val errors = check(dep)(using ctx, state) + errors.foreach(_.issue) + } + } private def check(dep: Dependency)(using Context, State): List[Error] = trace("checking dependency " + dep.show, init, errs => Errors.show(errs.asInstanceOf[Errors])) { From 6d2cd2977f7f458b74daba9e9f85c5e60b5134a2 Mon Sep 17 00:00:00 2001 From: Liu Fengyun Date: Thu, 15 Apr 2021 17:24:01 +0200 Subject: [PATCH 08/47] Check cycles before adding to visited set --- .../dotc/transform/init/CycleChecker.scala | 35 ++++++++++++------- 1 file changed, 22 insertions(+), 13 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/transform/init/CycleChecker.scala b/compiler/src/dotty/tools/dotc/transform/init/CycleChecker.scala index ee95fa6c0713..25f925adf1a5 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/CycleChecker.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/CycleChecker.scala @@ -9,7 +9,7 @@ import Types._ import Symbols._ import Decorators._ import printing.SyntaxHighlighting -import reporting.trace +import reporting._ import config.Printers.init import ast.tpd._ @@ -114,12 +114,12 @@ class CycleChecker(cache: Cache) { private def check(dep: Dependency)(using Context, State): List[Error] = trace("checking dependency " + dep.show, init, errs => Errors.show(errs.asInstanceOf[Errors])) { - if state.visited.contains(dep) then - Nil - else - state.visit(dep) { + dep match + case dep: ObjectAccess => checkObjectAccess(dep) + case _ => + if state.visited.contains(dep) then Nil + else state.visit(dep) { dep match - case dep: ObjectAccess => checkObjectAccess(dep) case dep: InstanceUsage => checkInstanceUsage(dep) case dep: StaticCall => checkStaticCall(dep) case dep: ClassUsage => checkClassUsage(dep) @@ -127,6 +127,7 @@ class CycleChecker(cache: Cache) { } private def checkObjectAccess(dep: ObjectAccess)(using Context, State): List[Error] = + Util.traceIndented("state.path = " + state.path.map(_.show), init) val obj = dep.symbol if state.path.contains(obj) then val cycle = state.path.dropWhile(_ != obj) @@ -138,14 +139,16 @@ class CycleChecker(cache: Cache) { // TODO: issue a warning for access an object outside its scope during its initialization Nil else - val constr = obj.primaryConstructor + val constr = obj.moduleClass.primaryConstructor state.withPath(obj) { check(StaticCall(constr.owner.asClass, constr)(dep.source)) } private def checkInstanceUsage(dep: InstanceUsage)(using Context, State): List[Error] = - if !classesInCurrentRun.contains(dep.symbol) then Nil + if !classesInCurrentRun.contains(dep.symbol) then + Util.traceIndented("skip " + dep.symbol.show + " which is not in current run ", init) + Nil else { val cls = dep.symbol val deps = classDependencies(cls, excludeInit = true) @@ -153,14 +156,18 @@ class CycleChecker(cache: Cache) { } private def checkStaticCall(dep: StaticCall)(using Context, State): List[Error] = - if !classesInCurrentRun.contains(dep.cls) then Nil + if !classesInCurrentRun.contains(dep.cls) then + Util.traceIndented("skip " + dep.cls.show + " which is not in current run ", init) + Nil else { val deps = methodDependencies(dep) deps.flatMap(check(_)) } private def checkClassUsage(dep: ClassUsage)(using Context, State): List[Error] = - if !classesInCurrentRun.contains(dep.symbol) then Nil + if !classesInCurrentRun.contains(dep.symbol) then + Util.traceIndented("skip " + dep.symbol.show + " which is not in current run ", init) + Nil else { val cls = dep.symbol val deps = classDependencies(cls, excludeInit = false) @@ -170,12 +177,13 @@ class CycleChecker(cache: Cache) { // ----- analysis of dependencies ------------------------------- def cacheConstructorDependencies(constr: Symbol, deps: List[Dependency])(using Context): Unit = + Util.traceIndented("deps for " + constr.show + " = " + deps.map(_.show), init) summaryCache(constr) = deps val cls = constr.owner + + classesInCurrentRun += cls if cls.is(Flags.Module) && cls.isStatic then objectsInCurrentRun += cls.sourceModule - else - classesInCurrentRun += cls private def classDependencies(sym: Symbol, excludeInit: Boolean)(using Context): List[Dependency] = if (summaryCache.contains(sym)) summaryCache(sym) @@ -186,13 +194,14 @@ class CycleChecker(cache: Cache) { deps } - private def methodDependencies(call: StaticCall)(using Context): List[Dependency] = + private def methodDependencies(call: StaticCall)(using Context): List[Dependency] = trace("dependencies of " + call.symbol.show, init, _.asInstanceOf[List[Dependency]].map(_.show).toString) { if (summaryCache.contains(call.symbol)) summaryCache(call.symbol) else trace("summary for " + call.symbol.show) { val deps = analyzeMethod(call) summaryCache(call.symbol) = deps deps } + } def isStaticObjectRef(sym: Symbol)(using Context) = sym.isTerm && !sym.is(Flags.Package) && sym.is(Flags.Module) && sym.isStatic From eedc8cd2e8b15246fd44dcc5d5f60fb47683a335 Mon Sep 17 00:00:00 2001 From: Liu Fengyun Date: Thu, 15 Apr 2021 22:20:06 +0200 Subject: [PATCH 09/47] Ignore static object access for prefix in new expressions --- .../dotc/transform/init/CycleChecker.scala | 8 +++--- .../tools/dotc/transform/init/Errors.scala | 9 +++++++ .../dotc/transform/init/Summarization.scala | 6 ++--- tests/init/neg/global-cycle6.scala | 25 +++++++++++++++++++ 4 files changed, 39 insertions(+), 9 deletions(-) create mode 100644 tests/init/neg/global-cycle6.scala diff --git a/compiler/src/dotty/tools/dotc/transform/init/CycleChecker.scala b/compiler/src/dotty/tools/dotc/transform/init/CycleChecker.scala index 25f925adf1a5..545bfc841216 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/CycleChecker.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/CycleChecker.scala @@ -131,13 +131,11 @@ class CycleChecker(cache: Cache) { val obj = dep.symbol if state.path.contains(obj) then val cycle = state.path.dropWhile(_ != obj) + val trace = state.trace.map(_.source) :+ dep.source if cycle.size > 1 then - val trace = state.trace.map(_.source) :+ dep.source - val error = CyclicObjectInit(obj, trace) - error :: Nil + CyclicObjectInit(obj, trace) :: Nil else - // TODO: issue a warning for access an object outside its scope during its initialization - Nil + ObjectLeakDuringInit(obj, trace) :: Nil else val constr = obj.moduleClass.primaryConstructor state.withPath(obj) { diff --git a/compiler/src/dotty/tools/dotc/transform/init/Errors.scala b/compiler/src/dotty/tools/dotc/transform/init/Errors.scala index f2a39470b339..2460defec5bf 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Errors.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Errors.scala @@ -78,6 +78,15 @@ object Errors { report.warning(show + stacktrace, obj.srcPos) } + case class ObjectLeakDuringInit(obj: Symbol, trace: Seq[Tree]) extends Error { + def source: Tree = trace.last + def show(using Context): String = + obj.show + " leaked during its initialization " + "." + + override def issue(using Context): Unit = + report.warning(show + stacktrace, obj.srcPos) + } + /** Promote `this` under initialization to fully-initialized */ case class PromoteThis(pot: ThisRef, source: Tree, trace: Seq[Tree]) extends Error { def show(using Context): String = "Promote the value under initialization to fully-initialized." diff --git a/compiler/src/dotty/tools/dotc/transform/init/Summarization.scala b/compiler/src/dotty/tools/dotc/transform/init/Summarization.scala index f266b94ba7ee..513f2edbb1b2 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Summarization.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Summarization.scala @@ -104,10 +104,8 @@ object Summarization { else { assert(summary.pots.size == 1) val outer = summary.pots.head - val pot = - if outer.isGlobal then Hot(cls)(expr) - else Warm(cls, outer)(expr) - summary.dropPotentials + pot + if outer.isGlobal then Summary(Hot(cls)(expr)) + else summary.dropPotentials + Warm(cls, outer)(expr) } } diff --git a/tests/init/neg/global-cycle6.scala b/tests/init/neg/global-cycle6.scala new file mode 100644 index 000000000000..2d4f23c25187 --- /dev/null +++ b/tests/init/neg/global-cycle6.scala @@ -0,0 +1,25 @@ +object A { // error + val n: Int = B.m + class Inner { + println(n) + } +} + +object B { + val a = new A.Inner + val m: Int = 10 +} + +object O { + object A { + val n: Int = B.m + class Inner { + val x: Int = 4 + } + } + + object B { + val a = new A.Inner + val m: Int = 10 + } +} \ No newline at end of file From 8c2d073d132813f6e20d248804ce634bf5009dac Mon Sep 17 00:00:00 2001 From: Liu Fengyun Date: Thu, 15 Apr 2021 22:28:42 +0200 Subject: [PATCH 10/47] Skip object access in libraries --- .../dotc/transform/init/CycleChecker.scala | 31 +++++++++++-------- tests/init/neg/t9312.scala | 23 ++++++++++++++ 2 files changed, 41 insertions(+), 13 deletions(-) create mode 100644 tests/init/neg/t9312.scala diff --git a/compiler/src/dotty/tools/dotc/transform/init/CycleChecker.scala b/compiler/src/dotty/tools/dotc/transform/init/CycleChecker.scala index 545bfc841216..55be1b15c98a 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/CycleChecker.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/CycleChecker.scala @@ -127,20 +127,25 @@ class CycleChecker(cache: Cache) { } private def checkObjectAccess(dep: ObjectAccess)(using Context, State): List[Error] = - Util.traceIndented("state.path = " + state.path.map(_.show), init) - val obj = dep.symbol - if state.path.contains(obj) then - val cycle = state.path.dropWhile(_ != obj) - val trace = state.trace.map(_.source) :+ dep.source - if cycle.size > 1 then - CyclicObjectInit(obj, trace) :: Nil + if !objectsInCurrentRun.contains(dep.symbol) then + Util.traceIndented("skip " + dep.symbol.show + " which is not in current run ", init) + Nil + else { + Util.traceIndented("state.path = " + state.path.map(_.show), init) + val obj = dep.symbol + if state.path.contains(obj) then + val cycle = state.path.dropWhile(_ != obj) + val trace = state.trace.map(_.source) :+ dep.source + if cycle.size > 1 then + CyclicObjectInit(obj, trace) :: Nil + else + ObjectLeakDuringInit(obj, trace) :: Nil else - ObjectLeakDuringInit(obj, trace) :: Nil - else - val constr = obj.moduleClass.primaryConstructor - state.withPath(obj) { - check(StaticCall(constr.owner.asClass, constr)(dep.source)) - } + val constr = obj.moduleClass.primaryConstructor + state.withPath(obj) { + check(StaticCall(constr.owner.asClass, constr)(dep.source)) + } + } private def checkInstanceUsage(dep: InstanceUsage)(using Context, State): List[Error] = diff --git a/tests/init/neg/t9312.scala b/tests/init/neg/t9312.scala new file mode 100644 index 000000000000..31bb3e5547c0 --- /dev/null +++ b/tests/init/neg/t9312.scala @@ -0,0 +1,23 @@ +object DeadLockTest { + def main(args: Array[String]): Unit = { + def run(block: => Unit): Unit = + new Thread(new Runnable {def run(): Unit = block}).start() + + run {println(Parent.Child1)} + run {println(Parent.Child2)} + + } + + object Parent { // error + trait Child { + Thread.sleep(2000) // ensure concurrent behavior + val parent = Parent + def siblings = parent.children - this + } + + object Child1 extends Child // error + object Child2 extends Child + + final val children = Set(Child1, Child2) + } +} From dcc0e64488bfca917eff3250a300fea77fa85e93 Mon Sep 17 00:00:00 2001 From: Liu Fengyun Date: Sat, 10 Apr 2021 11:14:13 +0200 Subject: [PATCH 11/47] Add tests --- tests/init/full/neg/global-cycle1.scala | 10 -------- tests/init/full/neg/global-cycle2.scala | 7 ------ tests/init/neg/global-cycle1.scala | 10 ++++++++ tests/init/neg/global-cycle2.scala | 7 ++++++ tests/init/{full => }/neg/global-cycle3.scala | 4 +-- tests/init/{full => }/neg/global-cycle4.scala | 4 +-- tests/init/neg/global-cycle5.scala | 15 +++++++++++ tests/init/{full => }/neg/i9176.scala | 2 +- tests/init/neg/t5366.scala | 15 +++++++++++ tests/init/neg/t9115.scala | 8 ++++++ tests/init/neg/t9261.scala | 3 +++ tests/init/neg/t9360.scala | 25 +++++++++++++++++++ 12 files changed, 88 insertions(+), 22 deletions(-) delete mode 100644 tests/init/full/neg/global-cycle1.scala delete mode 100644 tests/init/full/neg/global-cycle2.scala create mode 100644 tests/init/neg/global-cycle1.scala create mode 100644 tests/init/neg/global-cycle2.scala rename tests/init/{full => }/neg/global-cycle3.scala (51%) rename tests/init/{full => }/neg/global-cycle4.scala (72%) create mode 100644 tests/init/neg/global-cycle5.scala rename tests/init/{full => }/neg/i9176.scala (82%) create mode 100644 tests/init/neg/t5366.scala create mode 100644 tests/init/neg/t9115.scala create mode 100644 tests/init/neg/t9261.scala create mode 100644 tests/init/neg/t9360.scala diff --git a/tests/init/full/neg/global-cycle1.scala b/tests/init/full/neg/global-cycle1.scala deleted file mode 100644 index ebd667c51ba0..000000000000 --- a/tests/init/full/neg/global-cycle1.scala +++ /dev/null @@ -1,10 +0,0 @@ -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 deleted file mode 100644 index 30792e58af6b..000000000000 --- a/tests/init/full/neg/global-cycle2.scala +++ /dev/null @@ -1,7 +0,0 @@ -object A { - val a: Int = B.foo() // error -} - -object B { - def foo(): Int = A.a * 2 -} diff --git a/tests/init/neg/global-cycle1.scala b/tests/init/neg/global-cycle1.scala new file mode 100644 index 000000000000..ae6eb2a0fa0b --- /dev/null +++ b/tests/init/neg/global-cycle1.scala @@ -0,0 +1,10 @@ +object A { // error + val a: Int = B.b +} + +object B { // error + val b: Int = A.a +} + +@main +def Test = print(A.a) \ No newline at end of file diff --git a/tests/init/neg/global-cycle2.scala b/tests/init/neg/global-cycle2.scala new file mode 100644 index 000000000000..8694e3ccaf62 --- /dev/null +++ b/tests/init/neg/global-cycle2.scala @@ -0,0 +1,7 @@ + object A { // error + val a: Int = B.foo() +} + +object B { + def foo(): Int = A.a * 2 +} diff --git a/tests/init/full/neg/global-cycle3.scala b/tests/init/neg/global-cycle3.scala similarity index 51% rename from tests/init/full/neg/global-cycle3.scala rename to tests/init/neg/global-cycle3.scala index 7fae20dbe894..7c7d37bb639b 100644 --- a/tests/init/full/neg/global-cycle3.scala +++ b/tests/init/neg/global-cycle3.scala @@ -2,6 +2,6 @@ class A(x: Int) { def foo(): Int = B.a + 10 } -object B { - val a: Int = A(4).foo() // error +object B { // error + val a: Int = A(4).foo() } diff --git a/tests/init/full/neg/global-cycle4.scala b/tests/init/neg/global-cycle4.scala similarity index 72% rename from tests/init/full/neg/global-cycle4.scala rename to tests/init/neg/global-cycle4.scala index 3de0533cb521..30011fa75ba1 100644 --- a/tests/init/full/neg/global-cycle4.scala +++ b/tests/init/neg/global-cycle4.scala @@ -14,6 +14,6 @@ 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 +object O { // error + val a: Int = D(5).bar().foo() } diff --git a/tests/init/neg/global-cycle5.scala b/tests/init/neg/global-cycle5.scala new file mode 100644 index 000000000000..7ee1075ce5d5 --- /dev/null +++ b/tests/init/neg/global-cycle5.scala @@ -0,0 +1,15 @@ +class C(b: B) + +object O extends C(B()) { // error + val a: Int = new B().n + + object A { + val b: Int = 5 + } + + val c: Int = A.b + 4 +} + +class B { + val n: Int = O.A.b +} \ No newline at end of file diff --git a/tests/init/full/neg/i9176.scala b/tests/init/neg/i9176.scala similarity index 82% rename from tests/init/full/neg/i9176.scala rename to tests/init/neg/i9176.scala index abb8a6394dd2..74c762dcb32f 100644 --- a/tests/init/full/neg/i9176.scala +++ b/tests/init/neg/i9176.scala @@ -1,6 +1,6 @@ class Foo(val opposite: Foo) case object A extends Foo(B) // error -case object B extends Foo(A) // error +case object B extends Foo(A) object Test { def main(args: Array[String]): Unit = { println(A.opposite) diff --git a/tests/init/neg/t5366.scala b/tests/init/neg/t5366.scala new file mode 100644 index 000000000000..0d2fe6e9c384 --- /dev/null +++ b/tests/init/neg/t5366.scala @@ -0,0 +1,15 @@ +class IdAndMsg(val id: Int, val msg: String = "") + +case object ObjA extends IdAndMsg(1) // error +case object ObjB extends IdAndMsg(2) // error + +object IdAndMsg { // error + val values = List(ObjA , ObjB) +} + +object Test { + def main(args: Array[String]): Unit = { + ObjA + println(IdAndMsg.values) + } +} \ No newline at end of file diff --git a/tests/init/neg/t9115.scala b/tests/init/neg/t9115.scala new file mode 100644 index 000000000000..fe204e64c998 --- /dev/null +++ b/tests/init/neg/t9115.scala @@ -0,0 +1,8 @@ +object D { // error + def aaa = 1 //that’s the reason + class Z (depends: Any) + case object D1 extends Z(aaa) // 'null' when calling D.D1 first time // error + case object D2 extends Z(aaa) // 'null' when calling D.D2 first time // error + println(D1) + println(D2) +} \ No newline at end of file diff --git a/tests/init/neg/t9261.scala b/tests/init/neg/t9261.scala new file mode 100644 index 000000000000..378dad51b3a6 --- /dev/null +++ b/tests/init/neg/t9261.scala @@ -0,0 +1,3 @@ +sealed abstract class OrderType(val reverse: OrderType) +case object Buy extends OrderType(Sell) // error +case object Sell extends OrderType(Buy) // error diff --git a/tests/init/neg/t9360.scala b/tests/init/neg/t9360.scala new file mode 100644 index 000000000000..726082a3fe79 --- /dev/null +++ b/tests/init/neg/t9360.scala @@ -0,0 +1,25 @@ +class BaseClass(s: String) { + def print: Unit = () +} + +object Obj { // error + val s: String = "hello" + + object AObj extends BaseClass(s) // error + + object BObj extends BaseClass(s) // error + + val list = List(AObj, BObj) + + def print = { + println(list) + } +} + +object ObjectInit { + def main(args: Array[String]) = { + Obj.AObj.print + Obj.BObj.print + Obj.print + } +} From 60829842d17e125b946804152a52929fc230cba8 Mon Sep 17 00:00:00 2001 From: Liu Fengyun Date: Thu, 15 Apr 2021 23:36:48 +0200 Subject: [PATCH 12/47] Handle usage of proxy dependencies --- .../tools/dotc/transform/init/Checker.scala | 1 + .../tools/dotc/transform/init/Checking.scala | 16 ++++ .../dotc/transform/init/CycleChecker.scala | 75 +++++++++++++++---- 3 files changed, 79 insertions(+), 13 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/transform/init/Checker.scala b/compiler/src/dotty/tools/dotc/transform/init/Checker.scala index 184c7136c325..34375dc0baf0 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Checker.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Checker.scala @@ -44,6 +44,7 @@ class Checker extends Phase { tree match { case tdef: TypeDef if tdef.isClassDef => checkClassDef(tdef) + cycleChecker.classesInCurrentRun += tdef.symbol traverseChildren(tree) case _ => traverseChildren(tree) diff --git a/compiler/src/dotty/tools/dotc/transform/init/Checking.scala b/compiler/src/dotty/tools/dotc/transform/init/Checking.scala index 718d276613b3..fe993cc41a66 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Checking.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Checking.scala @@ -329,6 +329,22 @@ object Checking { state.dependencies += InstanceUsage(hot.classSymbol)(pot.source) Errors.empty + case MethodReturn(hot: Hot, sym) => + state.dependencies += ProxyUsage(hot.classSymbol, sym)(pot.source) + Errors.empty + + case MethodReturn(obj: Global, sym) => + state.dependencies += ProxyUsage(obj.moduleClass, sym)(pot.source) + Errors.empty + + case FieldReturn(hot: Hot, sym) => + state.dependencies += ProxyUsage(hot.classSymbol, sym)(pot.source) + Errors.empty + + case FieldReturn(obj: Global, sym) => + state.dependencies += ProxyUsage(obj.moduleClass, sym)(pot.source) + Errors.empty + case Fun(pots, effs) => val errs1 = state.test { effs.toList.flatMap(check(_)) diff --git a/compiler/src/dotty/tools/dotc/transform/init/CycleChecker.scala b/compiler/src/dotty/tools/dotc/transform/init/CycleChecker.scala index 55be1b15c98a..ba9e528fb30b 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/CycleChecker.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/CycleChecker.scala @@ -70,6 +70,11 @@ case class StaticCall(cls: ClassSymbol, symbol: Symbol)(val source: Tree) extend def show(using Context): String = "StaticCall(" + cls.show + ", " + symbol.show + ")" } +/** A static method call result is used */ + case class ProxyUsage(cls: ClassSymbol, symbol: Symbol)(val source: Tree) extends Dependency { + def show(using Context): String = "ProxyUsage(" + cls.show + ", " + symbol.show + ")" +} + /** A class is used * * This is a coarse-grained abstraction @@ -81,9 +86,10 @@ case class ClassUsage(symbol: Symbol)(val source: Tree) extends Dependency { class CycleChecker(cache: Cache) { private val summaryCache = mutable.Map.empty[Symbol, List[Dependency]] + private val proxyCache = mutable.Map.empty[Symbol, List[Dependency]] - private val classesInCurrentRun = mutable.Set.empty[Symbol] - private val objectsInCurrentRun = mutable.Set.empty[Symbol] + val classesInCurrentRun = mutable.Set.empty[Symbol] + val objectsInCurrentRun = mutable.Set.empty[Symbol] /** Checking state */ case class State(visited: mutable.Set[Dependency], path: Vector[Symbol], trace: Vector[Dependency]) { @@ -122,6 +128,7 @@ class CycleChecker(cache: Cache) { dep match case dep: InstanceUsage => checkInstanceUsage(dep) case dep: StaticCall => checkStaticCall(dep) + case dep: ProxyUsage => checkProxyUsage(dep) case dep: ClassUsage => checkClassUsage(dep) } } @@ -167,6 +174,15 @@ class CycleChecker(cache: Cache) { deps.flatMap(check(_)) } + private def checkProxyUsage(dep: ProxyUsage)(using Context, State): List[Error] = + if !classesInCurrentRun.contains(dep.cls) then + Util.traceIndented("skip " + dep.cls.show + " which is not in current run ", init) + Nil + else { + val deps = proxyDependencies(dep) + deps.flatMap(check(_)) + } + private def checkClassUsage(dep: ClassUsage)(using Context, State): List[Error] = if !classesInCurrentRun.contains(dep.symbol) then Util.traceIndented("skip " + dep.symbol.show + " which is not in current run ", init) @@ -184,7 +200,6 @@ class CycleChecker(cache: Cache) { summaryCache(constr) = deps val cls = constr.owner - classesInCurrentRun += cls if cls.is(Flags.Module) && cls.isStatic then objectsInCurrentRun += cls.sourceModule @@ -206,6 +221,36 @@ class CycleChecker(cache: Cache) { } } + private def proxyDependencies(dep: ProxyUsage)(using Context): List[Dependency] = trace("dependencies of " + dep.symbol.show, init, _.asInstanceOf[List[Dependency]].map(_.show).toString) { + if (proxyCache.contains(dep.symbol)) summaryCache(dep.symbol) + else trace("summary for " + dep.symbol.show) { + val env = Env(ctx.withOwner(dep.cls), cache) + val state = new Checking.State( + visited = Set.empty, + path = Vector.empty, + thisClass = dep.cls, + fieldsInited = mutable.Set.empty, + parentsInited = mutable.Set.empty, + safePromoted = mutable.Set(ThisRef()(dep.cls.defTree)), + dependencies = mutable.Set.empty, + env = env + ) { + override def isFieldInitialized(field: Symbol): Boolean = true + } + + val pot = Hot(dep.cls)(dep.source) + val target = Util.resolve(dep.cls, dep.symbol) + val effs = pot.potentialsOf(target)(using env).promote(dep.source) + + val errs = effs.flatMap(Checking.check(_)(using state)) + assert(errs.isEmpty, "unexpected errors: " + Errors.show(errs.toList)) + + val deps = state.dependencies.toList + proxyCache(dep.symbol) = deps + deps + } + } + def isStaticObjectRef(sym: Symbol)(using Context) = sym.isTerm && !sym.is(Flags.Package) && sym.is(Flags.Module) && sym.isStatic @@ -213,7 +258,7 @@ class CycleChecker(cache: Cache) { val cdef = cls.defTree.asInstanceOf[TypeDef] val tpl = cdef.rhs.asInstanceOf[Template] - var deps: List[Dependency] = Nil + var deps = new mutable.ListBuffer[Dependency] val traverser = new TreeTraverser { override def traverse(tree: Tree)(using Context): Unit = @@ -222,7 +267,7 @@ class CycleChecker(cache: Cache) { if !excludeInit then parents.foreach(traverse(_)) tree.body.foreach { - case ddef: DefDef if ddef.symbol.isConstructor => + case ddef: DefDef if !ddef.symbol.isConstructor => traverse(ddef) case vdef: ValDef if vdef.symbol.is(Flags.Lazy) => traverse(vdef) @@ -231,10 +276,10 @@ class CycleChecker(cache: Cache) { } case tree: RefTree if tree.isTerm => - analyzeType(tree.tpe, tree, exclude = cls) + deps ++= analyzeType(tree.tpe, tree, exclude = cls) case tree: This => - analyzeType(tree.tpe, tree, exclude = cls) + deps ++= analyzeType(tree.tpe, tree, exclude = cls) case tree: ValOrDefDef => traverseChildren(tree.rhs) @@ -243,7 +288,7 @@ class CycleChecker(cache: Cache) { // don't go into nested classes case tree: New => - deps = ClassUsage(tree.tpe.classSymbol)(tree) :: deps + deps += ClassUsage(tree.tpe.classSymbol)(tree) case _ => traverseChildren(tree) @@ -257,15 +302,15 @@ class CycleChecker(cache: Cache) { if excludeInit then InstanceUsage(tp.classSymbol)(tree) else ClassUsage(tp.classSymbol)(tree) - deps = dep :: deps + deps += dep } traverser.traverse(tpl) - deps + deps.toList } - private def analyzeType(tp: Type, source: Tree, exclude: Symbol)(using Context): Unit = tp match { - case (_: ConstantType) | NoPrefix => + private def analyzeType(tp: Type, source: Tree, exclude: Symbol)(using Context): List[Dependency] = tp match { + case (_: ConstantType) | NoPrefix => Nil case tmref: TermRef if isStaticObjectRef(tmref.symbol) => val obj = tmref.symbol @@ -280,12 +325,15 @@ class CycleChecker(cache: Cache) { val cls = tref.symbol val obj = cls.sourceModule ObjectAccess(obj)(source) :: InstanceUsage(cls)(source) :: Nil + else + Nil case SuperType(thisTp, _) => analyzeType(thisTp, source, exclude) case _: TypeRef | _: AppliedType => // possible from parent list + Nil case _ => throw new Exception("unexpected type: " + tp) @@ -307,7 +355,8 @@ class CycleChecker(cache: Cache) { } val pot = Hot(dep.cls)(dep.source) - val effs = pot.effectsOf(dep.symbol)(using env) + val target = Util.resolve(dep.cls, dep.symbol) + val effs = pot.effectsOf(target)(using env) val errs = effs.flatMap(Checking.check(_)(using state)) assert(errs.isEmpty, "unexpected errors: " + Errors.show(errs.toList)) From d045ea6ade94e77a6d86d6f098dff537ddc6c81b Mon Sep 17 00:00:00 2001 From: Liu Fengyun Date: Thu, 15 Apr 2021 23:50:51 +0200 Subject: [PATCH 13/47] Resolve overriding early --- .../tools/dotc/transform/init/Checking.scala | 15 ++++++++----- .../dotc/transform/init/CycleChecker.scala | 22 +++++++++++-------- 2 files changed, 23 insertions(+), 14 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/transform/init/Checking.scala b/compiler/src/dotty/tools/dotc/transform/init/Checking.scala index fe993cc41a66..843812f1d34b 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Checking.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Checking.scala @@ -231,7 +231,8 @@ object Checking { val cls = pot match case hot: Hot => hot.classSymbol case obj: Global => obj.moduleClass - state.dependencies += StaticCall(cls, sym)(pot.source) + val target = resolve(cls, sym) + state.dependencies += StaticCall(cls, target)(pot.source) Errors.empty case _: Cold => @@ -330,19 +331,23 @@ object Checking { Errors.empty case MethodReturn(hot: Hot, sym) => - state.dependencies += ProxyUsage(hot.classSymbol, sym)(pot.source) + val target = resolve(hot.classSymbol, sym) + state.dependencies += ProxyUsage(hot.classSymbol, target)(pot.source) Errors.empty case MethodReturn(obj: Global, sym) => - state.dependencies += ProxyUsage(obj.moduleClass, sym)(pot.source) + val target = resolve(obj.moduleClass, sym) + state.dependencies += ProxyUsage(obj.moduleClass, target)(pot.source) Errors.empty case FieldReturn(hot: Hot, sym) => - state.dependencies += ProxyUsage(hot.classSymbol, sym)(pot.source) + val target = resolve(hot.classSymbol, sym) + state.dependencies += ProxyUsage(hot.classSymbol, target)(pot.source) Errors.empty case FieldReturn(obj: Global, sym) => - state.dependencies += ProxyUsage(obj.moduleClass, sym)(pot.source) + val target = resolve(obj.moduleClass, sym) + state.dependencies += ProxyUsage(obj.moduleClass, target)(pot.source) Errors.empty case Fun(pots, effs) => diff --git a/compiler/src/dotty/tools/dotc/transform/init/CycleChecker.scala b/compiler/src/dotty/tools/dotc/transform/init/CycleChecker.scala index ba9e528fb30b..a11768292fd9 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/CycleChecker.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/CycleChecker.scala @@ -65,12 +65,18 @@ case class InstanceUsage(symbol: Symbol)(val source: Tree) extends Dependency { * * The method can be either on a static object or on a hot object. * The target of the call is determined statically. + * + * Note: Virtual method resolution should have been performed for the target. + * */ case class StaticCall(cls: ClassSymbol, symbol: Symbol)(val source: Tree) extends Dependency { def show(using Context): String = "StaticCall(" + cls.show + ", " + symbol.show + ")" } -/** A static method call result is used */ +/** A static method call result is used + * + * Note: Virtual method resolution should have been performed for the target. + */ case class ProxyUsage(cls: ClassSymbol, symbol: Symbol)(val source: Tree) extends Dependency { def show(using Context): String = "ProxyUsage(" + cls.show + ", " + symbol.show + ")" } @@ -166,8 +172,8 @@ class CycleChecker(cache: Cache) { } private def checkStaticCall(dep: StaticCall)(using Context, State): List[Error] = - if !classesInCurrentRun.contains(dep.cls) then - Util.traceIndented("skip " + dep.cls.show + " which is not in current run ", init) + if !classesInCurrentRun.contains(dep.cls) || !classesInCurrentRun.contains(dep.symbol.owner) then + Util.traceIndented("skip " + dep.show + " which is not in current run ", init) Nil else { val deps = methodDependencies(dep) @@ -175,8 +181,8 @@ class CycleChecker(cache: Cache) { } private def checkProxyUsage(dep: ProxyUsage)(using Context, State): List[Error] = - if !classesInCurrentRun.contains(dep.cls) then - Util.traceIndented("skip " + dep.cls.show + " which is not in current run ", init) + if !classesInCurrentRun.contains(dep.cls) || !classesInCurrentRun.contains(dep.symbol.owner) then + Util.traceIndented("skip " + dep.show + " which is not in current run ", init) Nil else { val deps = proxyDependencies(dep) @@ -239,8 +245,7 @@ class CycleChecker(cache: Cache) { } val pot = Hot(dep.cls)(dep.source) - val target = Util.resolve(dep.cls, dep.symbol) - val effs = pot.potentialsOf(target)(using env).promote(dep.source) + val effs = pot.potentialsOf(dep.symbol)(using env).promote(dep.source) val errs = effs.flatMap(Checking.check(_)(using state)) assert(errs.isEmpty, "unexpected errors: " + Errors.show(errs.toList)) @@ -355,8 +360,7 @@ class CycleChecker(cache: Cache) { } val pot = Hot(dep.cls)(dep.source) - val target = Util.resolve(dep.cls, dep.symbol) - val effs = pot.effectsOf(target)(using env) + val effs = pot.effectsOf(dep.symbol)(using env) val errs = effs.flatMap(Checking.check(_)(using state)) assert(errs.isEmpty, "unexpected errors: " + Errors.show(errs.toList)) From 14b9530490d2ce913541cd32a0186c585bc2f48d Mon Sep 17 00:00:00 2001 From: Liu Fengyun Date: Thu, 15 Apr 2021 23:53:47 +0200 Subject: [PATCH 14/47] All tests green --- tests/init/neg/global-cycle1.scala | 2 +- tests/init/neg/t5366.scala | 2 +- tests/init/neg/t9115.scala | 4 ++-- tests/init/neg/t9261.scala | 2 +- tests/init/neg/t9360.scala | 4 ++-- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/tests/init/neg/global-cycle1.scala b/tests/init/neg/global-cycle1.scala index ae6eb2a0fa0b..35368191d9d0 100644 --- a/tests/init/neg/global-cycle1.scala +++ b/tests/init/neg/global-cycle1.scala @@ -2,7 +2,7 @@ object A { // error val a: Int = B.b } -object B { // error +object B { val b: Int = A.a } diff --git a/tests/init/neg/t5366.scala b/tests/init/neg/t5366.scala index 0d2fe6e9c384..854bdfe0544b 100644 --- a/tests/init/neg/t5366.scala +++ b/tests/init/neg/t5366.scala @@ -1,7 +1,7 @@ class IdAndMsg(val id: Int, val msg: String = "") case object ObjA extends IdAndMsg(1) // error -case object ObjB extends IdAndMsg(2) // error +case object ObjB extends IdAndMsg(2) object IdAndMsg { // error val values = List(ObjA , ObjB) diff --git a/tests/init/neg/t9115.scala b/tests/init/neg/t9115.scala index fe204e64c998..620652b540e0 100644 --- a/tests/init/neg/t9115.scala +++ b/tests/init/neg/t9115.scala @@ -1,8 +1,8 @@ object D { // error def aaa = 1 //that’s the reason class Z (depends: Any) - case object D1 extends Z(aaa) // 'null' when calling D.D1 first time // error - case object D2 extends Z(aaa) // 'null' when calling D.D2 first time // error + case object D1 extends Z(aaa) // 'null' when calling D.D1 first time + case object D2 extends Z(aaa) // 'null' when calling D.D2 first time println(D1) println(D2) } \ No newline at end of file diff --git a/tests/init/neg/t9261.scala b/tests/init/neg/t9261.scala index 378dad51b3a6..1e23bedb9b6a 100644 --- a/tests/init/neg/t9261.scala +++ b/tests/init/neg/t9261.scala @@ -1,3 +1,3 @@ sealed abstract class OrderType(val reverse: OrderType) case object Buy extends OrderType(Sell) // error -case object Sell extends OrderType(Buy) // error +case object Sell extends OrderType(Buy) diff --git a/tests/init/neg/t9360.scala b/tests/init/neg/t9360.scala index 726082a3fe79..71f0af4c0fa1 100644 --- a/tests/init/neg/t9360.scala +++ b/tests/init/neg/t9360.scala @@ -5,9 +5,9 @@ class BaseClass(s: String) { object Obj { // error val s: String = "hello" - object AObj extends BaseClass(s) // error + object AObj extends BaseClass(s) - object BObj extends BaseClass(s) // error + object BObj extends BaseClass(s) val list = List(AObj, BObj) From 9c8850fe6c5514bef3b87661f8571a7c746edc35 Mon Sep 17 00:00:00 2001 From: Liu Fengyun Date: Fri, 16 Apr 2021 00:27:23 +0200 Subject: [PATCH 15/47] Fix crash compiling Dotty --- .../src/dotty/tools/dotc/transform/init/CycleChecker.scala | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/compiler/src/dotty/tools/dotc/transform/init/CycleChecker.scala b/compiler/src/dotty/tools/dotc/transform/init/CycleChecker.scala index a11768292fd9..5a1dab8caa89 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/CycleChecker.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/CycleChecker.scala @@ -340,6 +340,12 @@ class CycleChecker(cache: Cache) { // possible from parent list Nil + case AnnotatedType(tp, _) => + analyzeType(tp, source, exclude) + + case _: AndOrType | _: RefinedOrRecType => + Nil + case _ => throw new Exception("unexpected type: " + tp) } From 8ebaded37741776b9258cc20c8dc7b0e7ef86454 Mon Sep 17 00:00:00 2001 From: Liu Fengyun Date: Fri, 16 Apr 2021 01:03:05 +0200 Subject: [PATCH 16/47] Add missing object access trace --- .../dotty/tools/dotc/transform/init/CycleChecker.scala | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/transform/init/CycleChecker.scala b/compiler/src/dotty/tools/dotc/transform/init/CycleChecker.scala index 5a1dab8caa89..b6f1310145b5 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/CycleChecker.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/CycleChecker.scala @@ -105,8 +105,8 @@ class CycleChecker(cache: Cache) { val res = op(using state2) res - def withPath[T](obj: Symbol)(op: State ?=> T): T = - val state2 = this.copy(path = path :+ obj) + def visitObject[T](dep: ObjectAccess)(op: State ?=> T): T = + val state2 = this.copy(path = path :+ dep.symbol, trace = trace :+ dep) val res = op(using state2) res @@ -155,8 +155,8 @@ class CycleChecker(cache: Cache) { ObjectLeakDuringInit(obj, trace) :: Nil else val constr = obj.moduleClass.primaryConstructor - state.withPath(obj) { - check(StaticCall(constr.owner.asClass, constr)(dep.source)) + state.visitObject(dep) { + check(StaticCall(constr.owner.asClass, constr)(constr.defTree)) } } From 2e459463aa3ea002a013e59a17d47a70324cd7fb Mon Sep 17 00:00:00 2001 From: Liu Fengyun Date: Fri, 16 Apr 2021 02:12:08 +0200 Subject: [PATCH 17/47] Handle object access semantics depending on locations --- .../tools/dotc/transform/init/Checking.scala | 22 +++++++++++++------ .../dotc/transform/init/CycleChecker.scala | 2 -- .../dotc/transform/init/Potentials.scala | 7 ++++-- .../dotc/transform/init/Summarization.scala | 15 +++++++------ tests/init/neg/features-trees2.scala | 4 ++-- tests/init/neg/global-cycle7.scala | 18 +++++++++++++++ tests/init/neg/inner9.scala | 4 ++-- 7 files changed, 50 insertions(+), 22 deletions(-) create mode 100644 tests/init/neg/global-cycle7.scala diff --git a/compiler/src/dotty/tools/dotc/transform/init/Checking.scala b/compiler/src/dotty/tools/dotc/transform/init/Checking.scala index 843812f1d34b..daca3c0aff1b 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Checking.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Checking.scala @@ -227,14 +227,19 @@ object Checking { else Errors.empty - case pot @ (_: Hot | _: Global) => - val cls = pot match - case hot: Hot => hot.classSymbol - case obj: Global => obj.moduleClass - val target = resolve(cls, sym) - state.dependencies += StaticCall(cls, target)(pot.source) + case hot: Hot => + val target = resolve(hot.classSymbol, sym) + state.dependencies += StaticCall(hot.classSymbol, target)(pot.source) Errors.empty + case obj: Global => + val target = resolve(obj.moduleClass, sym) + if obj.enclosingClass == state.thisClass && obj.moduleClass == state.thisClass then + check(MethodCall(ThisRef()(obj.source), target)(eff.source)) + else + state.dependencies += StaticCall(obj.moduleClass, target)(pot.source) + Errors.empty + case _: Cold => CallCold(sym, eff.source, state.path).toErrors @@ -281,6 +286,8 @@ object Checking { // or all fields are already initialized val target = resolve(obj.moduleClass, field) if (target.is(Flags.Lazy)) check(MethodCall(obj, target)(eff.source)) + else if obj.enclosingClass == state.thisClass && obj.moduleClass == state.thisClass then + check(FieldAccess(ThisRef()(obj.source), target)(eff.source)) else Errors.empty case _: Cold => @@ -378,7 +385,8 @@ object Checking { private def checkAccessGlobal(eff: AccessGlobal)(using state: State): Errors = val obj = eff.potential - state.dependencies += ObjectAccess(obj.symbol)(eff.source) + if obj.enclosingClass != obj.moduleClass then + state.dependencies += ObjectAccess(obj.symbol)(eff.source) Errors.empty private def expand(pot: Potential)(using state: State): Summary = trace("expand " + pot.show, init, _.asInstanceOf[Summary].show) { diff --git a/compiler/src/dotty/tools/dotc/transform/init/CycleChecker.scala b/compiler/src/dotty/tools/dotc/transform/init/CycleChecker.scala index b6f1310145b5..bd50cc5e55fd 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/CycleChecker.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/CycleChecker.scala @@ -17,8 +17,6 @@ import Errors._, Potentials._, Effects._ import scala.collection.mutable - - /** The dependencies of a static object or a class * * This class is used in checking cyclic initialization of static objects. diff --git a/compiler/src/dotty/tools/dotc/transform/init/Potentials.scala b/compiler/src/dotty/tools/dotc/transform/init/Potentials.scala index 81357700b12e..cb01df7d40b8 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Potentials.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Potentials.scala @@ -159,8 +159,11 @@ object Potentials { "Fun[pots = " + potentials.map(_.show).mkString(";") + ", effs = " + effects.map(_.show).mkString(";") + "]" } - /** Reference to a global object */ - case class Global(symbol: Symbol)(val source: Tree) extends Refinable { + /** Reference to a global object + * + * @param enclosingClass The class where the reference appears in + */ + case class Global(symbol: Symbol, enclosingClass: ClassSymbol)(val source: Tree) extends Refinable { def show(using Context): String = symbol.show def moduleClass(using Context): ClassSymbol = symbol.moduleClass.asClass diff --git a/compiler/src/dotty/tools/dotc/transform/init/Summarization.scala b/compiler/src/dotty/tools/dotc/transform/init/Summarization.scala index 513f2edbb1b2..ffac57b52287 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Summarization.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Summarization.scala @@ -46,7 +46,8 @@ object Summarization { else if (!sym.exists) // polymorphic function apply and structural types Summary(pots.promote(expr) ++ effs) else if (sym.is(Flags.Module) && sym.isStatic) - Summary(effs) + Global(sym)(expr) + val enclosing = env.ctx.owner.lexicallyEnclosingClass.asClass + Summary(effs) + Global(sym, enclosing)(expr) else { val Summary(pots2, effs2) = pots.select(expr.symbol, expr) Summary(pots2, effs ++ effs2) @@ -249,7 +250,8 @@ object Summarization { // self reference to an object inside the object Summary(ThisRef()(source)) else - val pot = Global(tmref.symbol)(source) + val enclosing = env.ctx.owner.lexicallyEnclosingClass.asClass + val pot = Global(tmref.symbol, enclosing)(source) Summary(pot) + AccessGlobal(pot) case tmref: TermRef => @@ -261,16 +263,14 @@ object Summarization { } case ThisType(tref) => + val enclosing = env.ctx.owner.lexicallyEnclosingClass.asClass if tref.symbol.is(Flags.Module, butNot = Flags.Package) && tref.symbol.isStatic - && env.ctx.owner.isStatic - && tref.symbol != env.ctx.owner then val sym = tref.symbol.sourceModule - val pot = Global(sym)(source) + val pot = Global(sym, enclosing)(source) Summary(pot) + AccessGlobal(pot) else - val enclosing = env.ctx.owner.lexicallyEnclosingClass.asClass val cls = tref.symbol.asClass resolveThis(cls, ThisRef()(source), enclosing, source) @@ -312,7 +312,8 @@ object Summarization { else if (pot.size > 2) Summary(Promote(pot)(source)) else if (cls.is(Flags.Module) && !cur.ownersIterator.exists(_ == cls)) { // Dotty uses O$.this outside of the object O - val pot = Global(cls.sourceModule)(source) + val enclosing = env.ctx.owner.lexicallyEnclosingClass.asClass + val pot = Global(cls.sourceModule, enclosing)(source) Summary(pot) + AccessGlobal(pot) } else { diff --git a/tests/init/neg/features-trees2.scala b/tests/init/neg/features-trees2.scala index c422d4738ca7..62b456ac5530 100644 --- a/tests/init/neg/features-trees2.scala +++ b/tests/init/neg/features-trees2.scala @@ -1,6 +1,6 @@ -object Trees { +object Trees { // error class ValDef { counter += 1 } class EmptyValDef extends ValDef val theEmptyValDef = new EmptyValDef - private var counter = 0 // error + private var counter = 0 } diff --git a/tests/init/neg/global-cycle7.scala b/tests/init/neg/global-cycle7.scala new file mode 100644 index 000000000000..76a29455c9eb --- /dev/null +++ b/tests/init/neg/global-cycle7.scala @@ -0,0 +1,18 @@ +object A { // error + val n: Int = B.m +} + +object B { + val m: Int = A.n +} + +abstract class TokensCommon { + def maxToken: Int + + val tokenString, debugString: Array[String] = new Array[String](maxToken + 1) +} + +object JavaTokens extends TokensCommon { + final def maxToken: Int = DOUBLE + final val DOUBLE = 188 +} \ No newline at end of file diff --git a/tests/init/neg/inner9.scala b/tests/init/neg/inner9.scala index 2faf638644fe..6188932dcdbb 100644 --- a/tests/init/neg/inner9.scala +++ b/tests/init/neg/inner9.scala @@ -1,11 +1,11 @@ -object Flags { +object Flags { // error class Inner { println(b) } new Flags.Inner - val b = 5 // error + val b = 5 } object Flags2 { From ec5c7bad97078a67f5b102aa88c5746ff7aec71b Mon Sep 17 00:00:00 2001 From: Liu Fengyun Date: Fri, 16 Apr 2021 02:35:48 +0200 Subject: [PATCH 18/47] Fix crash in compiling Dotty --- .../dotty/tools/dotc/transform/init/Checking.scala | 5 +++-- .../tools/dotc/transform/init/CycleChecker.scala | 13 ++++++------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/transform/init/Checking.scala b/compiler/src/dotty/tools/dotc/transform/init/Checking.scala index daca3c0aff1b..c59c839b5579 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Checking.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Checking.scala @@ -38,7 +38,8 @@ object Checking { parentsInited: mutable.Set[ClassSymbol], safePromoted: mutable.Set[Potential], // Potentials that can be safely promoted dependencies: mutable.Set[Dependency], // if the current class is a static object, its dependencies - env: Env + env: Env, + init: Boolean = false // whether object is initialized, used in CycleChecker ) { def withOwner[T](sym: Symbol)(op: State ?=> T): T = val state = this.copy(env = env.withOwner(sym)) @@ -48,7 +49,7 @@ object Checking { def isFieldInitialized(field: Symbol): Boolean = - fieldsInited.contains(field) + init || fieldsInited.contains(field) def visit[T](eff: Effect)(op: State ?=> T): T = val state: State = this.copy(path = path :+ eff.source, visited = this.visited + eff) diff --git a/compiler/src/dotty/tools/dotc/transform/init/CycleChecker.scala b/compiler/src/dotty/tools/dotc/transform/init/CycleChecker.scala index bd50cc5e55fd..be7114ee3f7e 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/CycleChecker.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/CycleChecker.scala @@ -226,7 +226,7 @@ class CycleChecker(cache: Cache) { } private def proxyDependencies(dep: ProxyUsage)(using Context): List[Dependency] = trace("dependencies of " + dep.symbol.show, init, _.asInstanceOf[List[Dependency]].map(_.show).toString) { - if (proxyCache.contains(dep.symbol)) summaryCache(dep.symbol) + if (proxyCache.contains(dep.symbol)) proxyCache(dep.symbol) else trace("summary for " + dep.symbol.show) { val env = Env(ctx.withOwner(dep.cls), cache) val state = new Checking.State( @@ -350,7 +350,7 @@ class CycleChecker(cache: Cache) { private def analyzeMethod(dep: StaticCall)(using Context): List[Dependency] = { val env = Env(ctx.withOwner(dep.cls), cache) - val state = new Checking.State( + val state = Checking.State( visited = Set.empty, path = Vector.empty, thisClass = dep.cls, @@ -358,16 +358,15 @@ class CycleChecker(cache: Cache) { parentsInited = mutable.Set.empty, safePromoted = mutable.Set(ThisRef()(dep.cls.defTree)), dependencies = mutable.Set.empty, - env = env - ) { - override def isFieldInitialized(field: Symbol): Boolean = true - } + env = env, + init = true + ) val pot = Hot(dep.cls)(dep.source) val effs = pot.effectsOf(dep.symbol)(using env) val errs = effs.flatMap(Checking.check(_)(using state)) - assert(errs.isEmpty, "unexpected errors: " + Errors.show(errs.toList)) + assert(errs.isEmpty, "unexpected errors: " + Errors.show(errs.toList) + " while analyzing " + dep.show) state.dependencies.toList } From 8bdd645301e2b5bcc2633ea0eecab8d268a32875 Mon Sep 17 00:00:00 2001 From: Liu Fengyun Date: Fri, 16 Apr 2021 16:38:13 +0200 Subject: [PATCH 19/47] Refine trace and error message for cycles --- .../src/dotty/tools/dotc/transform/init/CycleChecker.scala | 6 +++--- compiler/src/dotty/tools/dotc/transform/init/Errors.scala | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/transform/init/CycleChecker.scala b/compiler/src/dotty/tools/dotc/transform/init/CycleChecker.scala index be7114ee3f7e..dd07f0a403e1 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/CycleChecker.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/CycleChecker.scala @@ -146,15 +146,15 @@ class CycleChecker(cache: Cache) { val obj = dep.symbol if state.path.contains(obj) then val cycle = state.path.dropWhile(_ != obj) - val trace = state.trace.map(_.source) :+ dep.source + val trace = state.trace.dropWhile(_.symbol != obj).map(_.source) :+ dep.source if cycle.size > 1 then - CyclicObjectInit(obj, trace) :: Nil + CyclicObjectInit(cycle, trace) :: Nil else ObjectLeakDuringInit(obj, trace) :: Nil else val constr = obj.moduleClass.primaryConstructor state.visitObject(dep) { - check(StaticCall(constr.owner.asClass, constr)(constr.defTree)) + check(StaticCall(constr.owner.asClass, constr)(obj.moduleClass.defTree)) } } diff --git a/compiler/src/dotty/tools/dotc/transform/init/Errors.scala b/compiler/src/dotty/tools/dotc/transform/init/Errors.scala index 2460defec5bf..34fcfd20691e 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Errors.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Errors.scala @@ -69,13 +69,13 @@ object Errors { report.warning(show + stacktrace, field.srcPos) } - case class CyclicObjectInit(obj: Symbol, trace: Seq[Tree]) extends Error { + case class CyclicObjectInit(objs: Seq[Symbol], trace: Seq[Tree]) extends Error { def source: Tree = trace.last def show(using Context): String = - "Cyclic object initialization of " + obj.show + "." + "Cyclic object initialization for " + objs.map(_.show).mkString(", ") + "." override def issue(using Context): Unit = - report.warning(show + stacktrace, obj.srcPos) + report.warning(show + stacktrace, objs.head.srcPos) } case class ObjectLeakDuringInit(obj: Symbol, trace: Seq[Tree]) extends Error { From 3508d33f1ed337008e8a874497e0463196abaaaf Mon Sep 17 00:00:00 2001 From: Liu Fengyun Date: Fri, 16 Apr 2021 17:12:28 +0200 Subject: [PATCH 20/47] Simplify summarization logic Now we handle the semantics of object access during checking. --- .../tools/dotc/transform/init/Summarization.scala | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/transform/init/Summarization.scala b/compiler/src/dotty/tools/dotc/transform/init/Summarization.scala index ffac57b52287..175ec48dd19a 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Summarization.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Summarization.scala @@ -245,14 +245,9 @@ object Summarization { if tmref.symbol.is(Flags.Module, butNot = Flags.Package) && tmref.symbol.isStatic => - val cls = tmref.symbol.moduleClass - if cls == env.ctx.owner.lexicallyEnclosingClass then - // self reference to an object inside the object - Summary(ThisRef()(source)) - else - val enclosing = env.ctx.owner.lexicallyEnclosingClass.asClass - val pot = Global(tmref.symbol, enclosing)(source) - Summary(pot) + AccessGlobal(pot) + val enclosing = env.ctx.owner.lexicallyEnclosingClass.asClass + val pot = Global(tmref.symbol, enclosing)(source) + Summary(pot) + AccessGlobal(pot) case tmref: TermRef => val Summary(pots, effs) = analyze(tmref.prefix, source) From 28016646ecc43d7cb5a942935008c8dd284e28d2 Mon Sep 17 00:00:00 2001 From: Liu Fengyun Date: Fri, 16 Apr 2021 22:49:44 +0200 Subject: [PATCH 21/47] Fix community build --- .../dotty/tools/dotc/transform/init/CycleChecker.scala | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/transform/init/CycleChecker.scala b/compiler/src/dotty/tools/dotc/transform/init/CycleChecker.scala index dd07f0a403e1..02b65a18a513 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/CycleChecker.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/CycleChecker.scala @@ -237,7 +237,8 @@ class CycleChecker(cache: Cache) { parentsInited = mutable.Set.empty, safePromoted = mutable.Set(ThisRef()(dep.cls.defTree)), dependencies = mutable.Set.empty, - env = env + env = env, + init = true ) { override def isFieldInitialized(field: Symbol): Boolean = true } @@ -341,11 +342,8 @@ class CycleChecker(cache: Cache) { case AnnotatedType(tp, _) => analyzeType(tp, source, exclude) - case _: AndOrType | _: RefinedOrRecType => - Nil - case _ => - throw new Exception("unexpected type: " + tp) + Nil } private def analyzeMethod(dep: StaticCall)(using Context): List[Dependency] = { From ea48d0b7e94f8025706c833e02edd539a5a36d68 Mon Sep 17 00:00:00 2001 From: Liu Fengyun Date: Fri, 16 Apr 2021 23:45:28 +0200 Subject: [PATCH 22/47] Fix tests Move checker before ExpandSAMs: - inner classes are more complex to deal with - the trees are not set for the synthesized class --- compiler/src/dotty/tools/dotc/Compiler.scala | 2 +- compiler/src/dotty/tools/dotc/transform/init/Checker.scala | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/compiler/src/dotty/tools/dotc/Compiler.scala b/compiler/src/dotty/tools/dotc/Compiler.scala index 425a23f56d70..69c4a721e97a 100644 --- a/compiler/src/dotty/tools/dotc/Compiler.scala +++ b/compiler/src/dotty/tools/dotc/Compiler.scala @@ -57,6 +57,7 @@ class Compiler { /** Phases dealing with the transformation from pickled trees to backend trees */ protected def transformPhases: List[List[Phase]] = + List(new init.Checker) :: // Check initialization of objects List(new FirstTransform, // Some transformations to put trees into a canonical form new CheckReentrant, // Internal use only: Check that compiled program has no data races involving global vars new ElimPackagePrefixes, // Eliminate references to package prefixes in Select nodes @@ -65,7 +66,6 @@ class Compiler { new BetaReduce, // Reduce closure applications new InlineVals, // Check right hand-sides of an `inline val`s new ExpandSAMs) :: // Expand single abstract method closures to anonymous classes - List(new init.Checker) :: // Check initialization of objects List(new ElimRepeated, // Rewrite vararg parameters and arguments new ProtectedAccessors, // Add accessors for protected members new ExtensionMethods, // Expand methods of value classes with extension methods diff --git a/compiler/src/dotty/tools/dotc/transform/init/Checker.scala b/compiler/src/dotty/tools/dotc/transform/init/Checker.scala index 34375dc0baf0..4e2152f5497a 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Checker.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Checker.scala @@ -29,6 +29,7 @@ class Checker extends Phase { private val cycleChecker = new CycleChecker(cache) override val runsAfter = Set(Pickler.name) + val runsBefore = Set(ExpandSAMs.name) override def isEnabled(using Context): Boolean = super.isEnabled && ctx.settings.YcheckInit.value From da7231752a39de50547232767751751906352aee Mon Sep 17 00:00:00 2001 From: Liu Fengyun Date: Sat, 17 Apr 2021 00:30:10 +0200 Subject: [PATCH 23/47] Refine code documentation --- compiler/src/dotty/tools/dotc/transform/init/Checking.scala | 4 ++-- .../src/dotty/tools/dotc/transform/init/CycleChecker.scala | 5 +++-- tests/init/{pos => neg}/constant.scala | 0 3 files changed, 5 insertions(+), 4 deletions(-) rename tests/init/{pos => neg}/constant.scala (100%) diff --git a/compiler/src/dotty/tools/dotc/transform/init/Checking.scala b/compiler/src/dotty/tools/dotc/transform/init/Checking.scala index c59c839b5579..1e33d2f6aa12 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Checking.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Checking.scala @@ -37,9 +37,9 @@ object Checking { fieldsInited: mutable.Set[Symbol], parentsInited: mutable.Set[ClassSymbol], safePromoted: mutable.Set[Potential], // Potentials that can be safely promoted - dependencies: mutable.Set[Dependency], // if the current class is a static object, its dependencies + dependencies: mutable.Set[Dependency], // dependencies collected for checking global objects env: Env, - init: Boolean = false // whether object is initialized, used in CycleChecker + init: Boolean = false // whether the object is initialized, used in CycleChecker ) { def withOwner[T](sym: Symbol)(op: State ?=> T): T = val state = this.copy(env = env.withOwner(sym)) diff --git a/compiler/src/dotty/tools/dotc/transform/init/CycleChecker.scala b/compiler/src/dotty/tools/dotc/transform/init/CycleChecker.scala index 02b65a18a513..ba6ea9c3a1e9 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/CycleChecker.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/CycleChecker.scala @@ -24,10 +24,10 @@ import scala.collection.mutable * For the check to be simple and fast, the algorithm uses a combination of * coarse-grained analysis and fine-grained analysis. * - * Fine-grained abstractions are created from the initialization + * Fine-grained dependencies are collected from the initialization * check for static objects. * - * Coarse-grained abstractions are created as follows: + * Coarse-grained dependencies are created as follows: * * - if a static object `O` is used in another class/static-object `B`, * then B -> O @@ -374,6 +374,7 @@ class CycleChecker(cache: Cache) { def clean() = { summaryCache.clear() + proxyCache.clear() classesInCurrentRun.clear() objectsInCurrentRun.clear() } diff --git a/tests/init/pos/constant.scala b/tests/init/neg/constant.scala similarity index 100% rename from tests/init/pos/constant.scala rename to tests/init/neg/constant.scala From f3bf03ba06b8cfb35fd14b0d30dd46efbad4858b Mon Sep 17 00:00:00 2001 From: Liu Fengyun Date: Sat, 17 Apr 2021 00:38:50 +0200 Subject: [PATCH 24/47] Report errors instead of crash --- .../src/dotty/tools/dotc/transform/init/CycleChecker.scala | 4 ++-- compiler/src/dotty/tools/dotc/transform/init/Errors.scala | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/transform/init/CycleChecker.scala b/compiler/src/dotty/tools/dotc/transform/init/CycleChecker.scala index ba6ea9c3a1e9..e28995655fd2 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/CycleChecker.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/CycleChecker.scala @@ -247,7 +247,7 @@ class CycleChecker(cache: Cache) { val effs = pot.potentialsOf(dep.symbol)(using env).promote(dep.source) val errs = effs.flatMap(Checking.check(_)(using state)) - assert(errs.isEmpty, "unexpected errors: " + Errors.show(errs.toList)) + errs.foreach(_.issue) val deps = state.dependencies.toList proxyCache(dep.symbol) = deps @@ -364,7 +364,7 @@ class CycleChecker(cache: Cache) { val effs = pot.effectsOf(dep.symbol)(using env) val errs = effs.flatMap(Checking.check(_)(using state)) - assert(errs.isEmpty, "unexpected errors: " + Errors.show(errs.toList) + " while analyzing " + dep.show) + errs.foreach(_.issue) state.dependencies.toList } diff --git a/compiler/src/dotty/tools/dotc/transform/init/Errors.scala b/compiler/src/dotty/tools/dotc/transform/init/Errors.scala index 34fcfd20691e..e159b8551974 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Errors.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Errors.scala @@ -116,7 +116,7 @@ object Errors { case class CallUnknown(meth: Symbol, source: Tree, trace: Seq[Tree]) extends Error { def show(using Context): String = - "Calling the external method " + meth.show + " may cause initialization errors" + "." + "Unable to analyze external " + meth.show + "." } /** Promote a value under initialization to fully-initialized */ From b2ae9473bd29709a42d4be450a61af9738847aff Mon Sep 17 00:00:00 2001 From: Liu Fengyun Date: Sat, 17 Apr 2021 00:39:30 +0200 Subject: [PATCH 25/47] Fix tests after phase move: constant folding happens later --- tests/init/neg/constant.scala | 2 +- tests/init/neg/final-fields.scala | 2 +- tests/init/neg/global-cycle7.scala | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/init/neg/constant.scala b/tests/init/neg/constant.scala index 042c264171ba..01e17e1c9e62 100644 --- a/tests/init/neg/constant.scala +++ b/tests/init/neg/constant.scala @@ -1,4 +1,4 @@ class A { final val a = b - final val b = 4 + final val b = 4 // error } \ No newline at end of file diff --git a/tests/init/neg/final-fields.scala b/tests/init/neg/final-fields.scala index 174ee9eeb79d..94205c38d763 100644 --- a/tests/init/neg/final-fields.scala +++ b/tests/init/neg/final-fields.scala @@ -22,7 +22,7 @@ object Test0 extends U { object Test1 extends U { final val f1 = 1 final val f3 = f1 + f2 - final val f2 = 2 + final val f2 = 2 // error: but constant folding avoided the issue val f4: 3 = f3 diff --git a/tests/init/neg/global-cycle7.scala b/tests/init/neg/global-cycle7.scala index 76a29455c9eb..32fdc4eb7b10 100644 --- a/tests/init/neg/global-cycle7.scala +++ b/tests/init/neg/global-cycle7.scala @@ -14,5 +14,5 @@ abstract class TokensCommon { object JavaTokens extends TokensCommon { final def maxToken: Int = DOUBLE - final val DOUBLE = 188 + final val DOUBLE = 188 // error: but constant folding avoided the issue } \ No newline at end of file From a97b30955438ffc20a0bfc6c269f14dd4ac15d60 Mon Sep 17 00:00:00 2001 From: Liu Fengyun Date: Tue, 20 Apr 2021 10:51:44 +0200 Subject: [PATCH 26/47] Add instanceClass to InstanceUsage --- .../tools/dotc/transform/init/Checking.scala | 4 +- .../dotc/transform/init/CycleChecker.scala | 54 ++++++++++--------- 2 files changed, 30 insertions(+), 28 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/transform/init/Checking.scala b/compiler/src/dotty/tools/dotc/transform/init/Checking.scala index 1e33d2f6aa12..16cadb192a40 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Checking.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Checking.scala @@ -331,11 +331,11 @@ object Checking { else PromoteWarm(pot, eff.source, state.path).toErrors case obj: Global => - state.dependencies += InstanceUsage(obj.moduleClass)(pot.source) + state.dependencies += InstanceUsage(obj.moduleClass, obj.moduleClass)(pot.source) Errors.empty case hot: Hot => - state.dependencies += InstanceUsage(hot.classSymbol)(pot.source) + state.dependencies += InstanceUsage(hot.classSymbol, hot.classSymbol)(pot.source) Errors.empty case MethodReturn(hot: Hot, sym) => diff --git a/compiler/src/dotty/tools/dotc/transform/init/CycleChecker.scala b/compiler/src/dotty/tools/dotc/transform/init/CycleChecker.scala index e28995655fd2..b738d8c073eb 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/CycleChecker.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/CycleChecker.scala @@ -55,7 +55,7 @@ case class ObjectAccess(symbol: Symbol)(val source: Tree) extends Dependency { } /** Depend on usage of an instance, which can be either a class instance or object */ -case class InstanceUsage(symbol: Symbol)(val source: Tree) extends Dependency { +case class InstanceUsage(symbol: ClassSymbol, instanceClass: ClassSymbol)(val source: Tree) extends Dependency { def show(using Context): String = "InstanceUsage(" + symbol.show + ")" } @@ -83,7 +83,7 @@ case class StaticCall(cls: ClassSymbol, symbol: Symbol)(val source: Tree) extend * * This is a coarse-grained abstraction */ -case class ClassUsage(symbol: Symbol)(val source: Tree) extends Dependency { +case class ClassUsage(symbol: ClassSymbol)(val source: Tree) extends Dependency { def show(using Context): String = "ClassUsage(" + symbol.show + ")" } @@ -158,16 +158,13 @@ class CycleChecker(cache: Cache) { } } - private def checkInstanceUsage(dep: InstanceUsage)(using Context, State): List[Error] = if !classesInCurrentRun.contains(dep.symbol) then Util.traceIndented("skip " + dep.symbol.show + " which is not in current run ", init) Nil - else { - val cls = dep.symbol - val deps = classDependencies(cls, excludeInit = true) + else + val deps = instanceDependencies(dep.symbol, dep.instanceClass) deps.flatMap(check(_)) - } private def checkStaticCall(dep: StaticCall)(using Context, State): List[Error] = if !classesInCurrentRun.contains(dep.cls) || !classesInCurrentRun.contains(dep.symbol.owner) then @@ -193,7 +190,7 @@ class CycleChecker(cache: Cache) { Nil else { val cls = dep.symbol - val deps = classDependencies(cls, excludeInit = false) + val deps = classDependencies(cls) deps.flatMap(check(_)) } @@ -207,11 +204,20 @@ class CycleChecker(cache: Cache) { if cls.is(Flags.Module) && cls.isStatic then objectsInCurrentRun += cls.sourceModule - private def classDependencies(sym: Symbol, excludeInit: Boolean)(using Context): List[Dependency] = + private def classDependencies(sym: Symbol)(using Context): List[Dependency] = + if (summaryCache.contains(sym)) summaryCache(sym) + else trace("summary for " + sym.show, init) { + val cls = sym.asClass + val deps = analyzeClass(cls, cls) + summaryCache(cls) = deps + deps + } + + private def instanceDependencies(sym: Symbol, instanceClass: ClassSymbol)(using Context): List[Dependency] = if (summaryCache.contains(sym)) summaryCache(sym) else trace("summary for " + sym.show, init) { val cls = sym.asClass - val deps = analyzeClass(cls, excludeInit) + val deps = analyzeClass(cls, instanceClass) summaryCache(cls) = deps deps } @@ -258,7 +264,7 @@ class CycleChecker(cache: Cache) { def isStaticObjectRef(sym: Symbol)(using Context) = sym.isTerm && !sym.is(Flags.Package) && sym.is(Flags.Module) && sym.isStatic - private def analyzeClass(cls: ClassSymbol, excludeInit: Boolean)(using Context): List[Dependency] = { + private def analyzeClass(cls: ClassSymbol, instanceClass: ClassSymbol)(using Context): List[Dependency] = { val cdef = cls.defTree.asInstanceOf[TypeDef] val tpl = cdef.rhs.asInstanceOf[Template] @@ -268,17 +274,19 @@ class CycleChecker(cache: Cache) { override def traverse(tree: Tree)(using Context): Unit = tree match { case tree @ Template(_, parents, self, _) => - if !excludeInit then - parents.foreach(traverse(_)) tree.body.foreach { case ddef: DefDef if !ddef.symbol.isConstructor => traverse(ddef) case vdef: ValDef if vdef.symbol.is(Flags.Lazy) => traverse(vdef) - case stat => - if !excludeInit then traverse(stat) + case stat => // TODO: promote potential of fields } + case tree @ Select(inst: New, _) if tree.symbol.isConstructor => + val cls = inst.tpe.classSymbol.asClass + deps += InstanceUsage(cls, cls)(tree) + deps += StaticCall(cls, tree.symbol)(tree) + case tree: RefTree if tree.isTerm => deps ++= analyzeType(tree.tpe, tree, exclude = cls) @@ -291,9 +299,6 @@ class CycleChecker(cache: Cache) { case tdef: TypeDef => // don't go into nested classes - case tree: New => - deps += ClassUsage(tree.tpe.classSymbol)(tree) - case _ => traverseChildren(tree) } @@ -302,11 +307,7 @@ class CycleChecker(cache: Cache) { // TODO: the traverser might create duplicate entries for parents tpl.parents.foreach { tree => val tp = tree.tpe - val dep = - if excludeInit then InstanceUsage(tp.classSymbol)(tree) - else ClassUsage(tp.classSymbol)(tree) - - deps += dep + deps += InstanceUsage(tp.classSymbol.asClass, instanceClass)(tree) } traverser.traverse(tpl) @@ -318,7 +319,8 @@ class CycleChecker(cache: Cache) { case tmref: TermRef if isStaticObjectRef(tmref.symbol) => val obj = tmref.symbol - ObjectAccess(obj)(source) :: InstanceUsage(obj.moduleClass)(source) :: Nil + val cls = obj.moduleClass.asClass + ObjectAccess(obj)(source) :: InstanceUsage(cls, cls)(source) :: Nil case tmref: TermRef => analyzeType(tmref.prefix, source, exclude) @@ -326,9 +328,9 @@ class CycleChecker(cache: Cache) { case ThisType(tref) => if isStaticObjectRef(tref.symbol.sourceModule) && tref.symbol != exclude then - val cls = tref.symbol + val cls = tref.symbol.asClass val obj = cls.sourceModule - ObjectAccess(obj)(source) :: InstanceUsage(cls)(source) :: Nil + ObjectAccess(obj)(source) :: InstanceUsage(cls, cls)(source) :: Nil else Nil From 8675dc77e46e63b646567d59e7a54e473bed68f1 Mon Sep 17 00:00:00 2001 From: Liu Fengyun Date: Tue, 20 Apr 2021 10:52:56 +0200 Subject: [PATCH 27/47] Remove class usage --- .../dotc/transform/init/CycleChecker.scala | 31 +------------------ 1 file changed, 1 insertion(+), 30 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/transform/init/CycleChecker.scala b/compiler/src/dotty/tools/dotc/transform/init/CycleChecker.scala index b738d8c073eb..d75714818a7a 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/CycleChecker.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/CycleChecker.scala @@ -75,19 +75,10 @@ case class StaticCall(cls: ClassSymbol, symbol: Symbol)(val source: Tree) extend * * Note: Virtual method resolution should have been performed for the target. */ - case class ProxyUsage(cls: ClassSymbol, symbol: Symbol)(val source: Tree) extends Dependency { +case class ProxyUsage(cls: ClassSymbol, symbol: Symbol)(val source: Tree) extends Dependency { def show(using Context): String = "ProxyUsage(" + cls.show + ", " + symbol.show + ")" } -/** A class is used - * - * This is a coarse-grained abstraction - */ -case class ClassUsage(symbol: ClassSymbol)(val source: Tree) extends Dependency { - def show(using Context): String = "ClassUsage(" + symbol.show + ")" -} - - class CycleChecker(cache: Cache) { private val summaryCache = mutable.Map.empty[Symbol, List[Dependency]] private val proxyCache = mutable.Map.empty[Symbol, List[Dependency]] @@ -133,7 +124,6 @@ class CycleChecker(cache: Cache) { case dep: InstanceUsage => checkInstanceUsage(dep) case dep: StaticCall => checkStaticCall(dep) case dep: ProxyUsage => checkProxyUsage(dep) - case dep: ClassUsage => checkClassUsage(dep) } } @@ -184,16 +174,6 @@ class CycleChecker(cache: Cache) { deps.flatMap(check(_)) } - private def checkClassUsage(dep: ClassUsage)(using Context, State): List[Error] = - if !classesInCurrentRun.contains(dep.symbol) then - Util.traceIndented("skip " + dep.symbol.show + " which is not in current run ", init) - Nil - else { - val cls = dep.symbol - val deps = classDependencies(cls) - deps.flatMap(check(_)) - } - // ----- analysis of dependencies ------------------------------- def cacheConstructorDependencies(constr: Symbol, deps: List[Dependency])(using Context): Unit = @@ -204,15 +184,6 @@ class CycleChecker(cache: Cache) { if cls.is(Flags.Module) && cls.isStatic then objectsInCurrentRun += cls.sourceModule - private def classDependencies(sym: Symbol)(using Context): List[Dependency] = - if (summaryCache.contains(sym)) summaryCache(sym) - else trace("summary for " + sym.show, init) { - val cls = sym.asClass - val deps = analyzeClass(cls, cls) - summaryCache(cls) = deps - deps - } - private def instanceDependencies(sym: Symbol, instanceClass: ClassSymbol)(using Context): List[Dependency] = if (summaryCache.contains(sym)) summaryCache(sym) else trace("summary for " + sym.show, init) { From d8e887f44200ca070f38a1e938a9a1eca10151e2 Mon Sep 17 00:00:00 2001 From: Liu Fengyun Date: Tue, 20 Apr 2021 11:06:00 +0200 Subject: [PATCH 28/47] Check fields of InstanceUsage --- .../dotty/tools/dotc/transform/init/CycleChecker.scala | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/transform/init/CycleChecker.scala b/compiler/src/dotty/tools/dotc/transform/init/CycleChecker.scala index d75714818a7a..5470989cad57 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/CycleChecker.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/CycleChecker.scala @@ -248,9 +248,13 @@ class CycleChecker(cache: Cache) { tree.body.foreach { case ddef: DefDef if !ddef.symbol.isConstructor => traverse(ddef) - case vdef: ValDef if vdef.symbol.is(Flags.Lazy) => - traverse(vdef) - case stat => // TODO: promote potential of fields + case vdef: ValDef => + if vdef.symbol.is(Flags.Lazy) then + traverse(vdef) + else + deps += ProxyUsage(instanceClass, vdef.symbol)(vdef) + case stat => + } case tree @ Select(inst: New, _) if tree.symbol.isConstructor => From 96fe9b5dac3d652a9f55d08b88f80c9d3008c2d1 Mon Sep 17 00:00:00 2001 From: Liu Fengyun Date: Tue, 20 Apr 2021 14:07:17 +0200 Subject: [PATCH 29/47] Make ordering of check deterministic --- compiler/src/dotty/tools/dotc/transform/init/CycleChecker.scala | 2 +- tests/init/neg/t9312.scala | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/transform/init/CycleChecker.scala b/compiler/src/dotty/tools/dotc/transform/init/CycleChecker.scala index 5470989cad57..bd6368dc5a9a 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/CycleChecker.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/CycleChecker.scala @@ -84,7 +84,7 @@ class CycleChecker(cache: Cache) { private val proxyCache = mutable.Map.empty[Symbol, List[Dependency]] val classesInCurrentRun = mutable.Set.empty[Symbol] - val objectsInCurrentRun = mutable.Set.empty[Symbol] + val objectsInCurrentRun = new mutable.ListBuffer[Symbol] /** Checking state */ case class State(visited: mutable.Set[Dependency], path: Vector[Symbol], trace: Vector[Dependency]) { diff --git a/tests/init/neg/t9312.scala b/tests/init/neg/t9312.scala index 31bb3e5547c0..d4eed8d9e137 100644 --- a/tests/init/neg/t9312.scala +++ b/tests/init/neg/t9312.scala @@ -15,7 +15,7 @@ object DeadLockTest { def siblings = parent.children - this } - object Child1 extends Child // error + object Child1 extends Child object Child2 extends Child final val children = Set(Child1, Child2) From 5787e5cda5fedd93eeaf75cfdc8a1805d00aaed9 Mon Sep 17 00:00:00 2001 From: Liu Fengyun Date: Tue, 20 Apr 2021 16:11:53 +0200 Subject: [PATCH 30/47] Make checking state final Overriding methods of State is erroneous and should be forbidden. --- compiler/src/dotty/tools/dotc/transform/init/Checking.scala | 3 +-- .../src/dotty/tools/dotc/transform/init/CycleChecker.scala | 4 +--- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/transform/init/Checking.scala b/compiler/src/dotty/tools/dotc/transform/init/Checking.scala index 16cadb192a40..6f01d633ddc8 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Checking.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Checking.scala @@ -30,7 +30,7 @@ object Checking { * */ - case class State( + final 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` @@ -47,7 +47,6 @@ object Checking { this.visited = state.visited res - def isFieldInitialized(field: Symbol): Boolean = init || fieldsInited.contains(field) diff --git a/compiler/src/dotty/tools/dotc/transform/init/CycleChecker.scala b/compiler/src/dotty/tools/dotc/transform/init/CycleChecker.scala index bd6368dc5a9a..23a3ed514b62 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/CycleChecker.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/CycleChecker.scala @@ -216,9 +216,7 @@ class CycleChecker(cache: Cache) { dependencies = mutable.Set.empty, env = env, init = true - ) { - override def isFieldInitialized(field: Symbol): Boolean = true - } + ) val pot = Hot(dep.cls)(dep.source) val effs = pot.potentialsOf(dep.symbol)(using env).promote(dep.source) From 1a5b1372f0bbcf8d74e1b6d4747e8eb0f8525c53 Mon Sep 17 00:00:00 2001 From: Liu Fengyun Date: Tue, 20 Apr 2021 16:40:43 +0200 Subject: [PATCH 31/47] More friendly trace for collected transitive dependencies --- .../tools/dotc/transform/init/Checking.scala | 18 +++++----- .../dotc/transform/init/CycleChecker.scala | 34 +++++++++---------- 2 files changed, 26 insertions(+), 26 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/transform/init/Checking.scala b/compiler/src/dotty/tools/dotc/transform/init/Checking.scala index 6f01d633ddc8..1bf7ac149a46 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Checking.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Checking.scala @@ -229,7 +229,7 @@ object Checking { case hot: Hot => val target = resolve(hot.classSymbol, sym) - state.dependencies += StaticCall(hot.classSymbol, target)(pot.source) + state.dependencies += StaticCall(hot.classSymbol, target)(state.path) Errors.empty case obj: Global => @@ -237,7 +237,7 @@ object Checking { if obj.enclosingClass == state.thisClass && obj.moduleClass == state.thisClass then check(MethodCall(ThisRef()(obj.source), target)(eff.source)) else - state.dependencies += StaticCall(obj.moduleClass, target)(pot.source) + state.dependencies += StaticCall(obj.moduleClass, target)(state.path) Errors.empty case _: Cold => @@ -330,31 +330,31 @@ object Checking { else PromoteWarm(pot, eff.source, state.path).toErrors case obj: Global => - state.dependencies += InstanceUsage(obj.moduleClass, obj.moduleClass)(pot.source) + state.dependencies += InstanceUsage(obj.moduleClass, obj.moduleClass)(state.path) Errors.empty case hot: Hot => - state.dependencies += InstanceUsage(hot.classSymbol, hot.classSymbol)(pot.source) + state.dependencies += InstanceUsage(hot.classSymbol, hot.classSymbol)(state.path) Errors.empty case MethodReturn(hot: Hot, sym) => val target = resolve(hot.classSymbol, sym) - state.dependencies += ProxyUsage(hot.classSymbol, target)(pot.source) + state.dependencies += ProxyUsage(hot.classSymbol, target)(state.path) Errors.empty case MethodReturn(obj: Global, sym) => val target = resolve(obj.moduleClass, sym) - state.dependencies += ProxyUsage(obj.moduleClass, target)(pot.source) + state.dependencies += ProxyUsage(obj.moduleClass, target)(state.path) Errors.empty case FieldReturn(hot: Hot, sym) => val target = resolve(hot.classSymbol, sym) - state.dependencies += ProxyUsage(hot.classSymbol, target)(pot.source) + state.dependencies += ProxyUsage(hot.classSymbol, target)(state.path) Errors.empty case FieldReturn(obj: Global, sym) => val target = resolve(obj.moduleClass, sym) - state.dependencies += ProxyUsage(obj.moduleClass, target)(pot.source) + state.dependencies += ProxyUsage(obj.moduleClass, target)(state.path) Errors.empty case Fun(pots, effs) => @@ -386,7 +386,7 @@ object Checking { private def checkAccessGlobal(eff: AccessGlobal)(using state: State): Errors = val obj = eff.potential if obj.enclosingClass != obj.moduleClass then - state.dependencies += ObjectAccess(obj.symbol)(eff.source) + state.dependencies += ObjectAccess(obj.symbol)(state.path) Errors.empty private def expand(pot: Potential)(using state: State): Summary = trace("expand " + pot.show, init, _.asInstanceOf[Summary].show) { diff --git a/compiler/src/dotty/tools/dotc/transform/init/CycleChecker.scala b/compiler/src/dotty/tools/dotc/transform/init/CycleChecker.scala index 23a3ed514b62..3ecbf71abc7d 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/CycleChecker.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/CycleChecker.scala @@ -45,17 +45,17 @@ import scala.collection.mutable */ trait Dependency { def symbol: Symbol - def source: Tree + def source: Vector[Tree] def show(using Context): String } /** Depend on the initialization of another object */ -case class ObjectAccess(symbol: Symbol)(val source: Tree) extends Dependency { +case class ObjectAccess(symbol: Symbol)(val source: Vector[Tree]) extends Dependency { def show(using Context): String = "ObjectAccess(" + symbol.show + ")" } /** Depend on usage of an instance, which can be either a class instance or object */ -case class InstanceUsage(symbol: ClassSymbol, instanceClass: ClassSymbol)(val source: Tree) extends Dependency { +case class InstanceUsage(symbol: ClassSymbol, instanceClass: ClassSymbol)(val source: Vector[Tree]) extends Dependency { def show(using Context): String = "InstanceUsage(" + symbol.show + ")" } @@ -67,7 +67,7 @@ case class InstanceUsage(symbol: ClassSymbol, instanceClass: ClassSymbol)(val so * Note: Virtual method resolution should have been performed for the target. * */ -case class StaticCall(cls: ClassSymbol, symbol: Symbol)(val source: Tree) extends Dependency { +case class StaticCall(cls: ClassSymbol, symbol: Symbol)(val source: Vector[Tree]) extends Dependency { def show(using Context): String = "StaticCall(" + cls.show + ", " + symbol.show + ")" } @@ -75,7 +75,7 @@ case class StaticCall(cls: ClassSymbol, symbol: Symbol)(val source: Tree) extend * * Note: Virtual method resolution should have been performed for the target. */ -case class ProxyUsage(cls: ClassSymbol, symbol: Symbol)(val source: Tree) extends Dependency { +case class ProxyUsage(cls: ClassSymbol, symbol: Symbol)(val source: Vector[Tree]) extends Dependency { def show(using Context): String = "ProxyUsage(" + cls.show + ", " + symbol.show + ")" } @@ -107,7 +107,7 @@ class CycleChecker(cache: Cache) { def checkCyclic()(using Context): Unit = { val state = State(visited = mutable.Set.empty, path = Vector.empty, trace = Vector.empty) objectsInCurrentRun.foreach { obj => - val dep = ObjectAccess(obj)(obj.defTree) + val dep = ObjectAccess(obj)(Vector(obj.defTree)) val errors = check(dep)(using ctx, state) errors.foreach(_.issue) } @@ -136,7 +136,7 @@ class CycleChecker(cache: Cache) { val obj = dep.symbol if state.path.contains(obj) then val cycle = state.path.dropWhile(_ != obj) - val trace = state.trace.dropWhile(_.symbol != obj).map(_.source) :+ dep.source + val trace = state.trace.dropWhile(_.symbol != obj).flatMap(_.source) ++ dep.source if cycle.size > 1 then CyclicObjectInit(cycle, trace) :: Nil else @@ -144,7 +144,7 @@ class CycleChecker(cache: Cache) { else val constr = obj.moduleClass.primaryConstructor state.visitObject(dep) { - check(StaticCall(constr.owner.asClass, constr)(obj.moduleClass.defTree)) + check(StaticCall(constr.owner.asClass, constr)(Vector(obj.moduleClass.defTree))) } } @@ -218,8 +218,8 @@ class CycleChecker(cache: Cache) { init = true ) - val pot = Hot(dep.cls)(dep.source) - val effs = pot.potentialsOf(dep.symbol)(using env).promote(dep.source) + val pot = Hot(dep.cls)(dep.source.last) + val effs = pot.potentialsOf(dep.symbol)(using env).map(pot => Promote(pot)(pot.source)) val errs = effs.flatMap(Checking.check(_)(using state)) errs.foreach(_.issue) @@ -250,15 +250,15 @@ class CycleChecker(cache: Cache) { if vdef.symbol.is(Flags.Lazy) then traverse(vdef) else - deps += ProxyUsage(instanceClass, vdef.symbol)(vdef) + deps += ProxyUsage(instanceClass, vdef.symbol)(Vector(vdef)) case stat => } case tree @ Select(inst: New, _) if tree.symbol.isConstructor => val cls = inst.tpe.classSymbol.asClass - deps += InstanceUsage(cls, cls)(tree) - deps += StaticCall(cls, tree.symbol)(tree) + deps += InstanceUsage(cls, cls)(Vector(tree)) + deps += StaticCall(cls, tree.symbol)(Vector(tree)) case tree: RefTree if tree.isTerm => deps ++= analyzeType(tree.tpe, tree, exclude = cls) @@ -280,7 +280,7 @@ class CycleChecker(cache: Cache) { // TODO: the traverser might create duplicate entries for parents tpl.parents.foreach { tree => val tp = tree.tpe - deps += InstanceUsage(tp.classSymbol.asClass, instanceClass)(tree) + deps += InstanceUsage(tp.classSymbol.asClass, instanceClass)(Vector(tree)) } traverser.traverse(tpl) @@ -293,7 +293,7 @@ class CycleChecker(cache: Cache) { case tmref: TermRef if isStaticObjectRef(tmref.symbol) => val obj = tmref.symbol val cls = obj.moduleClass.asClass - ObjectAccess(obj)(source) :: InstanceUsage(cls, cls)(source) :: Nil + ObjectAccess(obj)(Vector(source)) :: InstanceUsage(cls, cls)(Vector(source)) :: Nil case tmref: TermRef => analyzeType(tmref.prefix, source, exclude) @@ -303,7 +303,7 @@ class CycleChecker(cache: Cache) { then val cls = tref.symbol.asClass val obj = cls.sourceModule - ObjectAccess(obj)(source) :: InstanceUsage(cls, cls)(source) :: Nil + ObjectAccess(obj)(Vector(source)) :: InstanceUsage(cls, cls)(Vector(source)) :: Nil else Nil @@ -335,7 +335,7 @@ class CycleChecker(cache: Cache) { init = true ) - val pot = Hot(dep.cls)(dep.source) + val pot = Hot(dep.cls)(dep.source.last) val effs = pot.effectsOf(dep.symbol)(using env) val errs = effs.flatMap(Checking.check(_)(using state)) From 6546e2df0191cd8a4854d0ab3d27cd2d999f3fad Mon Sep 17 00:00:00 2001 From: Liu Fengyun Date: Tue, 20 Apr 2021 17:06:28 +0200 Subject: [PATCH 32/47] Add more tests --- tests/init/neg/global-cycle9.scala | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 tests/init/neg/global-cycle9.scala diff --git a/tests/init/neg/global-cycle9.scala b/tests/init/neg/global-cycle9.scala new file mode 100644 index 000000000000..ed706807521f --- /dev/null +++ b/tests/init/neg/global-cycle9.scala @@ -0,0 +1,20 @@ +object Names { // error + abstract class Name + + abstract class TermName extends Name: + def toTypeName: TypeName = ??? + + final class SimpleName(val start: Int, val length: Int) extends TermName + final class TypeName(val toTermName: TermName) extends Name + + class NameTable: + def add(index: Int, name: Name): Unit = () + add(0, EmptyTermName) + + var chrs: Array[Char] = new Array[Char](1024) + + val EmptyTermName: SimpleName = SimpleName(-1, 0) + val EmptyTypeName: TypeName = EmptyTermName.toTypeName + + val nameTable = NameTable() +} From 25f07820ccc6bb0622460598a32a103dbac0fc6b Mon Sep 17 00:00:00 2001 From: Liu Fengyun Date: Tue, 20 Apr 2021 18:34:34 +0200 Subject: [PATCH 33/47] More refined analysis for calls on objects --- .../dotc/transform/init/CycleChecker.scala | 22 ++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/transform/init/CycleChecker.scala b/compiler/src/dotty/tools/dotc/transform/init/CycleChecker.scala index 3ecbf71abc7d..684e493e830e 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/CycleChecker.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/CycleChecker.scala @@ -196,9 +196,12 @@ class CycleChecker(cache: Cache) { private def methodDependencies(call: StaticCall)(using Context): List[Dependency] = trace("dependencies of " + call.symbol.show, init, _.asInstanceOf[List[Dependency]].map(_.show).toString) { if (summaryCache.contains(call.symbol)) summaryCache(call.symbol) else trace("summary for " + call.symbol.show) { - val deps = analyzeMethod(call) - summaryCache(call.symbol) = deps - deps + if call.symbol.isOneOf(Flags.Method | Flags.Lazy) then + val deps = analyzeMethod(call) + summaryCache(call.symbol) = deps + deps + else + Nil } } @@ -260,6 +263,12 @@ class CycleChecker(cache: Cache) { deps += InstanceUsage(cls, cls)(Vector(tree)) deps += StaticCall(cls, tree.symbol)(Vector(tree)) + case tree @ Select(qual, name) if name.isTermName && isStaticObjectRef(qual.symbol) => + val cls = qual.symbol.moduleClass.asClass + deps += ObjectAccess(qual.symbol)(Vector(tree)) + deps += StaticCall(cls, tree.symbol)(Vector(tree)) + deps += ProxyUsage(cls, tree.symbol)(Vector(tree)) + case tree: RefTree if tree.isTerm => deps ++= analyzeType(tree.tpe, tree, exclude = cls) @@ -295,6 +304,13 @@ class CycleChecker(cache: Cache) { val cls = obj.moduleClass.asClass ObjectAccess(obj)(Vector(source)) :: InstanceUsage(cls, cls)(Vector(source)) :: Nil + case tmref @ TermRef(prefix: TermRef, _) if isStaticObjectRef(prefix.symbol) => + val obj = prefix.symbol + val cls = obj.moduleClass.asClass + ObjectAccess(obj)(Vector(source)) :: + StaticCall(cls, tmref.symbol)(Vector(source)) :: + ProxyUsage(cls, tmref.symbol)(Vector(source)) :: Nil + case tmref: TermRef => analyzeType(tmref.prefix, source, exclude) From afd304a4b3bc973d4ecb7917fa5a64586036f003 Mon Sep 17 00:00:00 2001 From: Liu Fengyun Date: Wed, 21 Apr 2021 00:38:50 +0200 Subject: [PATCH 34/47] Handle O.this.m() in type analysis --- .../dotc/transform/init/CycleChecker.scala | 32 +++++++++++-------- 1 file changed, 19 insertions(+), 13 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/transform/init/CycleChecker.scala b/compiler/src/dotty/tools/dotc/transform/init/CycleChecker.scala index 684e493e830e..978b1f5fca90 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/CycleChecker.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/CycleChecker.scala @@ -179,9 +179,9 @@ class CycleChecker(cache: Cache) { def cacheConstructorDependencies(constr: Symbol, deps: List[Dependency])(using Context): Unit = Util.traceIndented("deps for " + constr.show + " = " + deps.map(_.show), init) summaryCache(constr) = deps - val cls = constr.owner + val cls = constr.owner.asClass - if cls.is(Flags.Module) && cls.isStatic then + if isStaticObjectClass(cls) then objectsInCurrentRun += cls.sourceModule private def instanceDependencies(sym: Symbol, instanceClass: ClassSymbol)(using Context): List[Dependency] = @@ -236,6 +236,9 @@ class CycleChecker(cache: Cache) { def isStaticObjectRef(sym: Symbol)(using Context) = sym.isTerm && !sym.is(Flags.Package) && sym.is(Flags.Module) && sym.isStatic + def isStaticObjectClass(cls: ClassSymbol)(using Context) = + cls.is(Flags.Module) && cls.isStatic + private def analyzeClass(cls: ClassSymbol, instanceClass: ClassSymbol)(using Context): List[Dependency] = { val cdef = cls.defTree.asInstanceOf[TypeDef] val tpl = cdef.rhs.asInstanceOf[Template] @@ -263,13 +266,10 @@ class CycleChecker(cache: Cache) { deps += InstanceUsage(cls, cls)(Vector(tree)) deps += StaticCall(cls, tree.symbol)(Vector(tree)) - case tree @ Select(qual, name) if name.isTermName && isStaticObjectRef(qual.symbol) => - val cls = qual.symbol.moduleClass.asClass - deps += ObjectAccess(qual.symbol)(Vector(tree)) - deps += StaticCall(cls, tree.symbol)(Vector(tree)) - deps += ProxyUsage(cls, tree.symbol)(Vector(tree)) + case tree @ Select(qual, name) if name.isTermName && qual.tpe.isStable => + deps ++= analyzeType(tree.tpe, tree, exclude = cls) - case tree: RefTree if tree.isTerm => + case tree: Ident if tree.isTerm => deps ++= analyzeType(tree.tpe, tree, exclude = cls) case tree: This => @@ -296,6 +296,12 @@ class CycleChecker(cache: Cache) { deps.toList } + private def useObjectMember(obj: Symbol, member: Symbol, source: Tree)(using Context): List[Dependency] = + val cls = obj.moduleClass.asClass + ObjectAccess(obj)(Vector(source)) :: + StaticCall(cls, member)(Vector(source)) :: + ProxyUsage(cls, member)(Vector(source)) :: Nil + private def analyzeType(tp: Type, source: Tree, exclude: Symbol)(using Context): List[Dependency] = tp match { case (_: ConstantType) | NoPrefix => Nil @@ -305,11 +311,11 @@ class CycleChecker(cache: Cache) { ObjectAccess(obj)(Vector(source)) :: InstanceUsage(cls, cls)(Vector(source)) :: Nil case tmref @ TermRef(prefix: TermRef, _) if isStaticObjectRef(prefix.symbol) => - val obj = prefix.symbol - val cls = obj.moduleClass.asClass - ObjectAccess(obj)(Vector(source)) :: - StaticCall(cls, tmref.symbol)(Vector(source)) :: - ProxyUsage(cls, tmref.symbol)(Vector(source)) :: Nil + useObjectMember(prefix.symbol, tmref.symbol, source) + + case tmref @ TermRef(prefix: ThisType, _) if isStaticObjectClass(prefix.cls) && prefix.cls != exclude => + val obj = prefix.cls.sourceModule + useObjectMember(obj, tmref.symbol, source) case tmref: TermRef => analyzeType(tmref.prefix, source, exclude) From 06e577fbadf7f0169dea3df4cdf0740f0d865b02 Mon Sep 17 00:00:00 2001 From: Liu Fengyun Date: Wed, 21 Apr 2021 01:04:29 +0200 Subject: [PATCH 35/47] Add tests --- tests/init/neg/global-cycle10.scala | 17 +++++++++++++++++ tests/init/neg/global-cycle8.scala | 21 +++++++++++++++++++++ 2 files changed, 38 insertions(+) create mode 100644 tests/init/neg/global-cycle10.scala create mode 100644 tests/init/neg/global-cycle8.scala diff --git a/tests/init/neg/global-cycle10.scala b/tests/init/neg/global-cycle10.scala new file mode 100644 index 000000000000..5ca76d1e70fd --- /dev/null +++ b/tests/init/neg/global-cycle10.scala @@ -0,0 +1,17 @@ +abstract class Base { + def foo(): Unit + foo() +} + +object O extends Base { // error + val msg: String = "hello" + + class Inner { + println(msg) + } + + def foo() = new Inner +} + +@main +def Test = O \ No newline at end of file diff --git a/tests/init/neg/global-cycle8.scala b/tests/init/neg/global-cycle8.scala new file mode 100644 index 000000000000..f1b258e7c3ce --- /dev/null +++ b/tests/init/neg/global-cycle8.scala @@ -0,0 +1,21 @@ +class A { + def foo() = println(O.n) +} + +class B { + val a = new A +} + +object O { // error + val n: Int = 10 + println(P.m) +} + +object P { + val m = Q.bar(new B) +} + +object Q { + def bar(b: B) = b.a.foo() +} + From 8af6f2156c790ec6a297337f738103ebd7ccffc3 Mon Sep 17 00:00:00 2001 From: Liu Fengyun Date: Wed, 21 Apr 2021 16:14:54 +0200 Subject: [PATCH 36/47] Don't trigger object access effect after super constructor call --- .../src/dotty/tools/dotc/transform/init/Checker.scala | 1 + .../src/dotty/tools/dotc/transform/init/Checking.scala | 9 ++++++++- .../dotty/tools/dotc/transform/init/CycleChecker.scala | 2 ++ 3 files changed, 11 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 4e2152f5497a..b463d651d546 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Checker.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Checker.scala @@ -81,6 +81,7 @@ class Checker extends Phase { parentsInited = mutable.Set.empty, safePromoted = mutable.Set.empty, dependencies = mutable.Set.empty, + superConstrCalled = false, env = Env(ctx.withOwner(cls), cache) ) diff --git a/compiler/src/dotty/tools/dotc/transform/init/Checking.scala b/compiler/src/dotty/tools/dotc/transform/init/Checking.scala index 1bf7ac149a46..be5a512052db 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Checking.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Checking.scala @@ -38,6 +38,7 @@ object Checking { parentsInited: mutable.Set[ClassSymbol], safePromoted: mutable.Set[Potential], // Potentials that can be safely promoted dependencies: mutable.Set[Dependency], // dependencies collected for checking global objects + var superConstrCalled: Boolean, // Wether super constructor has been called for the current object env: Env, init: Boolean = false // whether the object is initialized, used in CycleChecker ) { @@ -188,6 +189,10 @@ object Checking { checkConstructor(cls.primaryConstructor, ref.tpe, ref) } + // Global objects can be safely accessed after super constructor is called + if cls == state.thisClass then + state.superConstrCalled = true + // check class body tpl.body.foreach { checkClassBodyStat(_) } } @@ -385,7 +390,9 @@ object Checking { private def checkAccessGlobal(eff: AccessGlobal)(using state: State): Errors = val obj = eff.potential - if obj.enclosingClass != obj.moduleClass then + if obj.moduleClass != state.thisClass + || obj.enclosingClass != state.thisClass && !state.superConstrCalled + then state.dependencies += ObjectAccess(obj.symbol)(state.path) Errors.empty diff --git a/compiler/src/dotty/tools/dotc/transform/init/CycleChecker.scala b/compiler/src/dotty/tools/dotc/transform/init/CycleChecker.scala index 978b1f5fca90..81ad248244ce 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/CycleChecker.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/CycleChecker.scala @@ -217,6 +217,7 @@ class CycleChecker(cache: Cache) { parentsInited = mutable.Set.empty, safePromoted = mutable.Set(ThisRef()(dep.cls.defTree)), dependencies = mutable.Set.empty, + superConstrCalled = true, env = env, init = true ) @@ -353,6 +354,7 @@ class CycleChecker(cache: Cache) { parentsInited = mutable.Set.empty, safePromoted = mutable.Set(ThisRef()(dep.cls.defTree)), dependencies = mutable.Set.empty, + superConstrCalled = true, env = env, init = true ) From 662da71401c29b823bf4a1fb992cc5a0e6157baa Mon Sep 17 00:00:00 2001 From: Liu Fengyun Date: Wed, 21 Apr 2021 17:41:23 +0200 Subject: [PATCH 37/47] More fine-grained analysis for inner classes of static objects --- .../tools/dotc/transform/init/Checking.scala | 30 ++++++++++++++----- .../dotc/transform/init/CycleChecker.scala | 8 ++++- .../dotc/transform/init/Potentials.scala | 3 ++ tests/init/neg/features-trees2.scala | 4 +-- tests/init/neg/global-cycle11.scala | 22 ++++++++++++++ tests/init/neg/global-cycle9.scala | 10 ++++++- tests/init/neg/inner9.scala | 4 +-- 7 files changed, 68 insertions(+), 13 deletions(-) create mode 100644 tests/init/neg/global-cycle11.scala diff --git a/compiler/src/dotty/tools/dotc/transform/init/Checking.scala b/compiler/src/dotty/tools/dotc/transform/init/Checking.scala index be5a512052db..8bcea1e909d7 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Checking.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Checking.scala @@ -89,6 +89,14 @@ object Checking { } error.issue } + /** Whether it's known to be safe to access an object */ + private def isObjectAccessSafe(obj: Global)(using state: State): Boolean = + obj.moduleClass == state.thisClass + && (obj.enclosingClass == state.thisClass + || obj.appearsInside(state.thisClass) && state.superConstrCalled) + +// ------- checking construtor --------------------------- + /** Check that the given concrete class may be initialized safely * * It assumes that all definitions are properly summarized before-hand. @@ -197,6 +205,8 @@ object Checking { tpl.body.foreach { checkClassBodyStat(_) } } +// ------- checking effects --------------------------- + private def checkMethodCall(eff: MethodCall)(using state: State): Errors = val MethodCall(pot, sym) = eff pot match { @@ -234,12 +244,18 @@ object Checking { case hot: Hot => val target = resolve(hot.classSymbol, sym) - state.dependencies += StaticCall(hot.classSymbol, target)(state.path) - Errors.empty + if (target.hasSource && hot.classSymbol.owner == state.thisClass) { // for inner classes of objects + val effs = hot.effectsOf(target).toList + effs.flatMap { check(_) } + } + else { + state.dependencies += StaticCall(hot.classSymbol, target)(state.path) + Errors.empty + } case obj: Global => val target = resolve(obj.moduleClass, sym) - if obj.enclosingClass == state.thisClass && obj.moduleClass == state.thisClass then + if isObjectAccessSafe(obj) then check(MethodCall(ThisRef()(obj.source), target)(eff.source)) else state.dependencies += StaticCall(obj.moduleClass, target)(state.path) @@ -291,7 +307,7 @@ object Checking { // or all fields are already initialized val target = resolve(obj.moduleClass, field) if (target.is(Flags.Lazy)) check(MethodCall(obj, target)(eff.source)) - else if obj.enclosingClass == state.thisClass && obj.moduleClass == state.thisClass then + else if isObjectAccessSafe(obj) then check(FieldAccess(ThisRef()(obj.source), target)(eff.source)) else Errors.empty @@ -390,12 +406,12 @@ object Checking { private def checkAccessGlobal(eff: AccessGlobal)(using state: State): Errors = val obj = eff.potential - if obj.moduleClass != state.thisClass - || obj.enclosingClass != state.thisClass && !state.superConstrCalled - then + if !isObjectAccessSafe(obj) then state.dependencies += ObjectAccess(obj.symbol)(state.path) Errors.empty +// ------- expansion of potentials --------------------------- + private def expand(pot: Potential)(using state: State): Summary = trace("expand " + pot.show, init, _.asInstanceOf[Summary].show) { pot match { case MethodReturn(pot1, sym) => diff --git a/compiler/src/dotty/tools/dotc/transform/init/CycleChecker.scala b/compiler/src/dotty/tools/dotc/transform/init/CycleChecker.scala index 81ad248244ce..301bb07c27d0 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/CycleChecker.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/CycleChecker.scala @@ -13,7 +13,7 @@ import reporting._ import config.Printers.init import ast.tpd._ -import Errors._, Potentials._, Effects._ +import Errors._, Potentials._, Effects._, Util._ import scala.collection.mutable @@ -99,6 +99,8 @@ class CycleChecker(cache: Cache) { val res = op(using state2) res + def stackTrace: Vector[Tree] = trace.flatMap(_.source) + } def state(using ev: State) = ev @@ -160,6 +162,8 @@ class CycleChecker(cache: Cache) { if !classesInCurrentRun.contains(dep.cls) || !classesInCurrentRun.contains(dep.symbol.owner) then Util.traceIndented("skip " + dep.show + " which is not in current run ", init) Nil + else if !dep.symbol.hasSource then + CallUnknown(dep.symbol, dep.source.last, state.stackTrace) :: Nil else { val deps = methodDependencies(dep) deps.flatMap(check(_)) @@ -169,6 +173,8 @@ class CycleChecker(cache: Cache) { if !classesInCurrentRun.contains(dep.cls) || !classesInCurrentRun.contains(dep.symbol.owner) then Util.traceIndented("skip " + dep.show + " which is not in current run ", init) Nil + else if !dep.symbol.hasSource then + CallUnknown(dep.symbol, dep.source.last, state.stackTrace) :: Nil else { val deps = proxyDependencies(dep) deps.flatMap(check(_)) diff --git a/compiler/src/dotty/tools/dotc/transform/init/Potentials.scala b/compiler/src/dotty/tools/dotc/transform/init/Potentials.scala index cb01df7d40b8..d8ba611e9fc4 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Potentials.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Potentials.scala @@ -167,6 +167,9 @@ object Potentials { def show(using Context): String = symbol.show def moduleClass(using Context): ClassSymbol = symbol.moduleClass.asClass + + def appearsInside(sym: Symbol)(using Context): Boolean = + enclosingClass.ownersIterator.exists(_ == sym) } /** The potential of a hot object diff --git a/tests/init/neg/features-trees2.scala b/tests/init/neg/features-trees2.scala index 62b456ac5530..c236e56de17c 100644 --- a/tests/init/neg/features-trees2.scala +++ b/tests/init/neg/features-trees2.scala @@ -1,6 +1,6 @@ -object Trees { // error +object Trees { class ValDef { counter += 1 } class EmptyValDef extends ValDef val theEmptyValDef = new EmptyValDef - private var counter = 0 + private var counter = 0 // error } diff --git a/tests/init/neg/global-cycle11.scala b/tests/init/neg/global-cycle11.scala new file mode 100644 index 000000000000..303dcdb9c8f7 --- /dev/null +++ b/tests/init/neg/global-cycle11.scala @@ -0,0 +1,22 @@ +import scala.collection.mutable + +object NameKinds { + private val qualifiedNameKinds = mutable.HashMap[Int, QualifiedNameKind]() + + val QualifiedName: QualifiedNameKind = new QualifiedNameKind(2, ".") + + abstract class NameKind(val tag: Int) + + class QualifiedNameKind(tag: Int, val separator: String) + extends NameKind(tag) { + qualifiedNameKinds(tag) = this + } +} + +object A { // error + val n: Int = B.m +} + +object B { + val m: Int = A.n +} diff --git a/tests/init/neg/global-cycle9.scala b/tests/init/neg/global-cycle9.scala index ed706807521f..c2d047b97022 100644 --- a/tests/init/neg/global-cycle9.scala +++ b/tests/init/neg/global-cycle9.scala @@ -1,4 +1,4 @@ -object Names { // error +object Names { abstract class Name abstract class TermName extends Name: @@ -18,3 +18,11 @@ object Names { // error val nameTable = NameTable() } + +object A { // error + val n: Int = B.m +} + +object B { + val m: Int = A.n +} diff --git a/tests/init/neg/inner9.scala b/tests/init/neg/inner9.scala index 6188932dcdbb..da0c0acc815f 100644 --- a/tests/init/neg/inner9.scala +++ b/tests/init/neg/inner9.scala @@ -1,11 +1,11 @@ -object Flags { // error +object Flags { class Inner { println(b) } new Flags.Inner - val b = 5 + val b = 5 // error } object Flags2 { From b38aef5e9168ce1508d7c0dbab9f6e6c543bbba7 Mon Sep 17 00:00:00 2001 From: Liu Fengyun Date: Mon, 26 Apr 2021 10:35:44 +0200 Subject: [PATCH 38/47] More friendly stack trace --- .../dotc/transform/init/CycleChecker.scala | 26 ++++++++++++++++--- .../tools/dotc/transform/init/Errors.scala | 15 +++++++---- 2 files changed, 33 insertions(+), 8 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/transform/init/CycleChecker.scala b/compiler/src/dotty/tools/dotc/transform/init/CycleChecker.scala index 301bb07c27d0..35dcf09a3342 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/CycleChecker.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/CycleChecker.scala @@ -86,6 +86,12 @@ class CycleChecker(cache: Cache) { val classesInCurrentRun = mutable.Set.empty[Symbol] val objectsInCurrentRun = new mutable.ListBuffer[Symbol] + /** The limit of stack trace shown to programmers + * + * TODO: make it configurable from command-line for debugging purposes + */ + val traceNumberLimit = 10 + /** Checking state */ case class State(visited: mutable.Set[Dependency], path: Vector[Symbol], trace: Vector[Dependency]) { def visit[T](dep: Dependency)(op: State ?=> T): T = @@ -138,11 +144,25 @@ class CycleChecker(cache: Cache) { val obj = dep.symbol if state.path.contains(obj) then val cycle = state.path.dropWhile(_ != obj) - val trace = state.trace.dropWhile(_.symbol != obj).flatMap(_.source) ++ dep.source + val ctor = obj.moduleClass.primaryConstructor + var trace = state.trace.dropWhile(_.symbol != ctor) :+ dep + + val traceSuppress = trace.size > traceNumberLimit + if traceSuppress then + // truncate trace up to the first escape of object + val iter = trace.iterator + trace = Vector.empty + var elem = iter.next() + trace = trace :+ elem + while iter.hasNext && !elem.isInstanceOf[InstanceUsage] do + elem = iter.next() + trace = trace :+ elem + + val locations = trace.flatMap(_.source) if cycle.size > 1 then - CyclicObjectInit(cycle, trace) :: Nil + CyclicObjectInit(cycle, locations, traceSuppress) :: Nil else - ObjectLeakDuringInit(obj, trace) :: Nil + ObjectLeakDuringInit(obj, locations, traceSuppress) :: Nil else val constr = obj.moduleClass.primaryConstructor state.visitObject(dep) { diff --git a/compiler/src/dotty/tools/dotc/transform/init/Errors.scala b/compiler/src/dotty/tools/dotc/transform/init/Errors.scala index e159b8551974..3ea71307098a 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Errors.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Errors.scala @@ -23,12 +23,18 @@ object Errors { def trace: Seq[Tree] def show(using Context): String + def traceSuppressed: Boolean = false + def issue(using Context): Unit = report.warning(show + stacktrace, source.srcPos) def toErrors: Errors = this :: Nil - def stacktrace(using Context): String = if (trace.isEmpty) "" else " Calling trace:\n" + { + private def stacktracePrefix: String = + val str = if traceSuppressed then "suppressed" else "full" + " Calling trace (" + str + "):\n" + + def stacktrace(using Context): String = if (trace.isEmpty) "" else stacktracePrefix + { var indentCount = 0 var last: String = "" val sb = new StringBuilder @@ -69,7 +75,7 @@ object Errors { report.warning(show + stacktrace, field.srcPos) } - case class CyclicObjectInit(objs: Seq[Symbol], trace: Seq[Tree]) extends Error { + case class CyclicObjectInit(objs: Seq[Symbol], trace: Seq[Tree], override val traceSuppressed: Boolean) extends Error { def source: Tree = trace.last def show(using Context): String = "Cyclic object initialization for " + objs.map(_.show).mkString(", ") + "." @@ -78,10 +84,9 @@ object Errors { report.warning(show + stacktrace, objs.head.srcPos) } - case class ObjectLeakDuringInit(obj: Symbol, trace: Seq[Tree]) extends Error { + case class ObjectLeakDuringInit(obj: Symbol, trace: Seq[Tree], override val traceSuppressed: Boolean) extends Error { def source: Tree = trace.last - def show(using Context): String = - obj.show + " leaked during its initialization " + "." + def show(using Context): String = obj.show + " leaked during its initialization " + "." override def issue(using Context): Unit = report.warning(show + stacktrace, obj.srcPos) From 9f9f944cd8ab6bfb4d7b1225bd4316aedc6f3806 Mon Sep 17 00:00:00 2001 From: Liu Fengyun Date: Mon, 26 Apr 2021 11:43:18 +0200 Subject: [PATCH 39/47] See through static calls on inner objects --- .../tools/dotc/transform/init/Checking.scala | 7 ++++++ tests/init/neg/global-cycle12.scala | 24 +++++++++++++++++++ tests/init/neg/t3273.check | 8 +++---- 3 files changed, 35 insertions(+), 4 deletions(-) create mode 100644 tests/init/neg/global-cycle12.scala diff --git a/compiler/src/dotty/tools/dotc/transform/init/Checking.scala b/compiler/src/dotty/tools/dotc/transform/init/Checking.scala index 8bcea1e909d7..e60f83f71ba4 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Checking.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Checking.scala @@ -95,6 +95,10 @@ object Checking { && (obj.enclosingClass == state.thisClass || obj.appearsInside(state.thisClass) && state.superConstrCalled) + /** Whether the obj is directly nested inside the current class */ + private def isNestedObject(obj: Global)(using state: State): Boolean = + obj.symbol.owner == state.thisClass + // ------- checking construtor --------------------------- /** Check that the given concrete class may be initialized safely @@ -257,6 +261,9 @@ object Checking { val target = resolve(obj.moduleClass, sym) if isObjectAccessSafe(obj) then check(MethodCall(ThisRef()(obj.source), target)(eff.source)) + else if isNestedObject(obj) then + val pot = FieldReturn(ThisRef()(obj.source), obj.symbol)(obj.source) + check(MethodCall(pot, target)(eff.source)) else state.dependencies += StaticCall(obj.moduleClass, target)(state.path) Errors.empty diff --git a/tests/init/neg/global-cycle12.scala b/tests/init/neg/global-cycle12.scala new file mode 100644 index 000000000000..fc7558c02b69 --- /dev/null +++ b/tests/init/neg/global-cycle12.scala @@ -0,0 +1,24 @@ +// from Scala.js +object Names { + private final val ConstructorSimpleEncodedName: String = + "" + + final class SimpleMethodName(encoded: String) + + object SimpleMethodName { + def apply(name: String): SimpleMethodName = + val res = name == ConstructorSimpleEncodedName + new SimpleMethodName(name) + } + + val ConstructorSimpleName: SimpleMethodName = + SimpleMethodName(ConstructorSimpleEncodedName) +} + +object A { // error + val n: Int = B.m +} + +object B { + val m: Int = A.n +} diff --git a/tests/init/neg/t3273.check b/tests/init/neg/t3273.check index 74c016ef521f..6de88c1f3225 100644 --- a/tests/init/neg/t3273.check +++ b/tests/init/neg/t3273.check @@ -2,21 +2,21 @@ 4 | val num1: LazyList[Int] = 1 #:: num1.map(_ + 1) // error | ^^^^^^^^^^^^^^^ | Promoting the value to fully-initialized is unsafe. - | Calling trace: + | Calling trace (full): | -> val num1: LazyList[Int] = 1 #:: num1.map(_ + 1) // error [ t3273.scala:4 ] | | The unsafe promotion may cause the following problem(s): | - | 1. Access non-initialized value num1. Calling trace: + | 1. Access non-initialized value num1. Calling trace (full): | -> 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 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | Promoting the value to fully-initialized is unsafe. - | Calling trace: + | Calling trace (full): | -> val num2: LazyList[Int] = 1 #:: num2.iterator.map(_ + 1).to(LazyList) // error [ t3273.scala:5 ] | | The unsafe promotion may cause the following problem(s): | - | 1. Access non-initialized value num2. Calling trace: + | 1. Access non-initialized value num2. Calling trace (full): | -> val num2: LazyList[Int] = 1 #:: num2.iterator.map(_ + 1).to(LazyList) // error [ t3273.scala:5 ] From b70f63c121334a9386cf56ff8cfb45110b726deb Mon Sep 17 00:00:00 2001 From: Liu Fengyun Date: Mon, 26 Apr 2021 13:52:42 +0200 Subject: [PATCH 40/47] Support pinpointing in stacktrace --- .../dotc/transform/init/CycleChecker.scala | 15 ++++++--- .../tools/dotc/transform/init/Errors.scala | 32 +++++++++++++++++-- 2 files changed, 41 insertions(+), 6 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/transform/init/CycleChecker.scala b/compiler/src/dotty/tools/dotc/transform/init/CycleChecker.scala index 35dcf09a3342..38baa4e6ddef 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/CycleChecker.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/CycleChecker.scala @@ -147,6 +147,7 @@ class CycleChecker(cache: Cache) { val ctor = obj.moduleClass.primaryConstructor var trace = state.trace.dropWhile(_.symbol != ctor) :+ dep + val pinpointOpt = trace.find(_.isInstanceOf[InstanceUsage]) val traceSuppress = trace.size > traceNumberLimit if traceSuppress then // truncate trace up to the first escape of object @@ -159,10 +160,16 @@ class CycleChecker(cache: Cache) { trace = trace :+ elem val locations = trace.flatMap(_.source) - if cycle.size > 1 then - CyclicObjectInit(cycle, locations, traceSuppress) :: Nil - else - ObjectLeakDuringInit(obj, locations, traceSuppress) :: Nil + val warning = + if cycle.size > 1 then + CyclicObjectInit(cycle, locations, traceSuppress) + else + ObjectLeakDuringInit(obj, locations, traceSuppress) + + if pinpointOpt.nonEmpty then + warning.pinpoint(pinpointOpt.get.source.last, "Leaking the object may cause initialization problems") + + warning :: Nil else val constr = obj.moduleClass.primaryConstructor state.visitObject(dep) { diff --git a/compiler/src/dotty/tools/dotc/transform/init/Errors.scala b/compiler/src/dotty/tools/dotc/transform/init/Errors.scala index 3ea71307098a..f11440287027 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Errors.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Errors.scala @@ -8,6 +8,9 @@ import ast.tpd._ import core._ import Decorators._, printing.SyntaxHighlighting import Types._, Symbols._, Contexts._ +import util.{ SimpleIdentityMap, SourcePosition } + +import reporting.MessageRendering import Effects._, Potentials._ @@ -30,10 +33,31 @@ object Errors { def toErrors: Errors = this :: Nil + /** pinpoints in stacktrace */ + private var pinpoints: SimpleIdentityMap[Tree, String] = SimpleIdentityMap.empty + + def pinpoint(tree: Tree, msg: String): this.type = + this.pinpoints = this.pinpoints.updated(tree, msg) + this + private def stacktracePrefix: String = val str = if traceSuppressed then "suppressed" else "full" " Calling trace (" + str + "):\n" + private val render = new MessageRendering {} + + private def pinpointText(pos: SourcePosition, msg: String, offset: Int)(using Context): String = + val carets = render.hl("Warning") { + if (pos.startLine == pos.endLine) + "^" * math.max(1, pos.endColumn - pos.startColumn) + else "^" + } + + val padding = pos.startColumnPadding + (" " * offset) + val marker = padding + carets + val textline = padding + msg + "\n" + marker + "\n" + textline + def stacktrace(using Context): String = if (trace.isEmpty) "" else stacktracePrefix + { var indentCount = 0 var last: String = "" @@ -45,8 +69,12 @@ object Errors { val line = if pos.source.exists then val loc = "[ " + pos.source.file.name + ":" + (pos.line + 1) + " ]" - val code = SyntaxHighlighting.highlight(pos.lineContent.trim) - i"$code\t$loc" + val code = SyntaxHighlighting.highlight(pos.lineContent.stripLineEnd) + val pinpoint = + if !pinpoints.contains(tree) then "" + else pinpointText(pos, pinpoints(tree), indentCount + 3) + + i"$code\t$loc" + pinpoint else tree.show From 224732b61d58fd96f7eff35f14eac5fb3eb4af55 Mon Sep 17 00:00:00 2001 From: Liu Fengyun Date: Mon, 26 Apr 2021 16:45:37 +0200 Subject: [PATCH 41/47] Show file and line numbers at the beginning in stack trace --- .../tools/dotc/transform/init/Errors.scala | 29 ++++++++++--------- tests/init/neg/t3273.check | 22 +++++++------- 2 files changed, 26 insertions(+), 25 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/transform/init/Errors.scala b/compiler/src/dotty/tools/dotc/transform/init/Errors.scala index f11440287027..a12830d77333 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Errors.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Errors.scala @@ -8,9 +8,10 @@ import ast.tpd._ import core._ import Decorators._, printing.SyntaxHighlighting import Types._, Symbols._, Contexts._ -import util.{ SimpleIdentityMap, SourcePosition } +import util.{ SimpleIdentityMap, SourcePosition, NoSourcePosition } import reporting.MessageRendering +import printing.Highlighting import Effects._, Potentials._ @@ -56,31 +57,33 @@ object Errors { val padding = pos.startColumnPadding + (" " * offset) val marker = padding + carets val textline = padding + msg - "\n" + marker + "\n" + textline + marker + "\n" + textline + "\n" def stacktrace(using Context): String = if (trace.isEmpty) "" else stacktracePrefix + { var indentCount = 0 - var last: String = "" + var last: SourcePosition = NoSourcePosition val sb = new StringBuilder trace.foreach { tree => indentCount += 1 val pos = tree.sourcePos - val prefix = s"${ " " * indentCount }-> " + val prefix = (" " * indentCount) + "-> " + var pinpoint = "" val line = if pos.source.exists then - val loc = "[ " + pos.source.file.name + ":" + (pos.line + 1) + " ]" - val code = SyntaxHighlighting.highlight(pos.lineContent.stripLineEnd) - val pinpoint = - if !pinpoints.contains(tree) then "" - else pinpointText(pos, pinpoints(tree), indentCount + 3) + val locText = "[ " + pos.source.file.name + ":" + (pos.line + 1) + " ]" + val loc = Highlighting.Blue(locText) + val code = SyntaxHighlighting.highlight(pos.lineContent) + if pinpoints.contains(tree) then + pinpoint = pinpointText(pos, pinpoints(tree), locText.length + prefix.length) - i"$code\t$loc" + pinpoint + i"$loc$prefix$code" else tree.show - if (last != line) sb.append(prefix + line + "\n") + if (last.source != pos.source || last.line != pos.line) + sb.append(line + pinpoint) - last = line + last = pos } sb.toString } @@ -161,7 +164,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. " + stacktrace + "\nThe unsafe promotion may cause the following problem(s):\n" + (errors.flatMap(_.flatten).map { error => index += 1 diff --git a/tests/init/neg/t3273.check b/tests/init/neg/t3273.check index 6de88c1f3225..b355402c1e4a 100644 --- a/tests/init/neg/t3273.check +++ b/tests/init/neg/t3273.check @@ -1,22 +1,20 @@ -- 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. - | Calling trace (full): - | -> val num1: LazyList[Int] = 1 #:: num1.map(_ + 1) // error [ t3273.scala:4 ] + | Promoting the value to fully-initialized is unsafe. Calling trace (full): + | [ t3273.scala:4 ] -> val num1: LazyList[Int] = 1 #:: num1.map(_ + 1) // error | - | The unsafe promotion may cause the following problem(s): + | The unsafe promotion may cause the following problem(s): | - | 1. Access non-initialized value num1. Calling trace (full): - | -> val num1: LazyList[Int] = 1 #:: num1.map(_ + 1) // error [ t3273.scala:4 ] + | 1. Access non-initialized value num1. Calling trace (full): + | [ t3273.scala:4 ] -> val num1: LazyList[Int] = 1 #:: num1.map(_ + 1) // error -- 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. - | Calling trace (full): - | -> val num2: LazyList[Int] = 1 #:: num2.iterator.map(_ + 1).to(LazyList) // error [ t3273.scala:5 ] + | Promoting the value to fully-initialized is unsafe. Calling trace (full): + | [ t3273.scala:5 ] -> val num2: LazyList[Int] = 1 #:: num2.iterator.map(_ + 1).to(LazyList) // error | - | The unsafe promotion may cause the following problem(s): + | The unsafe promotion may cause the following problem(s): | - | 1. Access non-initialized value num2. Calling trace (full): - | -> val num2: LazyList[Int] = 1 #:: num2.iterator.map(_ + 1).to(LazyList) // error [ t3273.scala:5 ] + | 1. Access non-initialized value num2. Calling trace (full): + | [ t3273.scala:5 ] -> val num2: LazyList[Int] = 1 #:: num2.iterator.map(_ + 1).to(LazyList) // error From ef76448ac4ca8b6966dc8e2af8734c33604d7547 Mon Sep 17 00:00:00 2001 From: Liu Fengyun Date: Mon, 26 Apr 2021 17:37:37 +0200 Subject: [PATCH 42/47] Remove unsound skip Static calls should be checked unless the instance class is defined in a separate module. This is because the static call in library may contain virtual calls to the current project. --- .../src/dotty/tools/dotc/transform/init/CycleChecker.scala | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/transform/init/CycleChecker.scala b/compiler/src/dotty/tools/dotc/transform/init/CycleChecker.scala index 38baa4e6ddef..3d5777d738a3 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/CycleChecker.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/CycleChecker.scala @@ -186,7 +186,7 @@ class CycleChecker(cache: Cache) { deps.flatMap(check(_)) private def checkStaticCall(dep: StaticCall)(using Context, State): List[Error] = - if !classesInCurrentRun.contains(dep.cls) || !classesInCurrentRun.contains(dep.symbol.owner) then + if !classesInCurrentRun.contains(dep.cls) then Util.traceIndented("skip " + dep.show + " which is not in current run ", init) Nil else if !dep.symbol.hasSource then @@ -197,7 +197,7 @@ class CycleChecker(cache: Cache) { } private def checkProxyUsage(dep: ProxyUsage)(using Context, State): List[Error] = - if !classesInCurrentRun.contains(dep.cls) || !classesInCurrentRun.contains(dep.symbol.owner) then + if !classesInCurrentRun.contains(dep.cls) then Util.traceIndented("skip " + dep.show + " which is not in current run ", init) Nil else if !dep.symbol.hasSource then From f48d08196dc1c84d3b8581c49d058150a2ea4068 Mon Sep 17 00:00:00 2001 From: Liu Fengyun Date: Mon, 26 Apr 2021 21:40:50 +0200 Subject: [PATCH 43/47] Expand potentials on this of static objects --- .../tools/dotc/transform/init/Checking.scala | 22 ++++++++++++++----- .../dotc/transform/init/CycleChecker.scala | 2 +- tests/init/neg/global-cycle13.scala | 22 +++++++++++++++++++ 3 files changed, 40 insertions(+), 6 deletions(-) create mode 100644 tests/init/neg/global-cycle13.scala diff --git a/compiler/src/dotty/tools/dotc/transform/init/Checking.scala b/compiler/src/dotty/tools/dotc/transform/init/Checking.scala index e60f83f71ba4..ad731246f9d3 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Checking.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Checking.scala @@ -258,13 +258,13 @@ object Checking { } case obj: Global => - val target = resolve(obj.moduleClass, sym) if isObjectAccessSafe(obj) then - check(MethodCall(ThisRef()(obj.source), target)(eff.source)) + check(MethodCall(ThisRef()(obj.source), sym)(eff.source)) else if isNestedObject(obj) then val pot = FieldReturn(ThisRef()(obj.source), obj.symbol)(obj.source) - check(MethodCall(pot, target)(eff.source)) + check(MethodCall(pot, sym)(eff.source)) else + val target = resolve(obj.moduleClass, sym) state.dependencies += StaticCall(obj.moduleClass, target)(state.path) Errors.empty @@ -452,7 +452,13 @@ object Checking { if (target.hasSource) Summary(warm.potentialsOf(target), Effects.empty) else Summary.empty // warning already issued in call effect - case _: Hot | _: Global => + case obj: Global => + if isObjectAccessSafe(obj) then + expand(MethodReturn(ThisRef()(obj.source), sym)(pot.source)) + else + Summary(Promote(pot)(pot.source)) + + case _: Hot => Summary(Promote(pot)(pot.source)) case _: Cold => @@ -484,7 +490,13 @@ object Checking { if (target.hasSource) Summary(warm.potentialsOf(target), Effects.empty) else Summary(Cold()(pot.source)) - case _: Hot | _: Global => + case obj: Global => + if isObjectAccessSafe(obj) then + expand(FieldReturn(ThisRef()(obj.source), sym)(pot.source)) + else + Summary(Promote(pot)(pot.source)) + + case _: Hot => Summary(Promote(pot)(pot.source)) case _: Cold => diff --git a/compiler/src/dotty/tools/dotc/transform/init/CycleChecker.scala b/compiler/src/dotty/tools/dotc/transform/init/CycleChecker.scala index 3d5777d738a3..a33252522164 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/CycleChecker.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/CycleChecker.scala @@ -244,7 +244,7 @@ class CycleChecker(cache: Cache) { val env = Env(ctx.withOwner(dep.cls), cache) val state = new Checking.State( visited = Set.empty, - path = Vector.empty, + path = dep.source, thisClass = dep.cls, fieldsInited = mutable.Set.empty, parentsInited = mutable.Set.empty, diff --git a/tests/init/neg/global-cycle13.scala b/tests/init/neg/global-cycle13.scala new file mode 100644 index 000000000000..e7b0901281c5 --- /dev/null +++ b/tests/init/neg/global-cycle13.scala @@ -0,0 +1,22 @@ +object Names { // error + abstract class Name + + abstract class TermName extends Name: + def toTypeName: TypeName = ??? + + final class SimpleName(val start: Int, val length: Int) extends TermName { + def foo() = println(EmptyTermName.toString) + } + final class TypeName(val toTermName: TermName) extends Name + + class NameTable: + def add(index: Int, name: Name): Unit = () + add(0, EmptyTermName) + + var chrs: Array[Char] = new Array[Char](1024) + + val EmptyTermName: SimpleName = SimpleName(-1, 0) + val EmptyTypeName: TypeName = EmptyTermName.toTypeName + + val nameTable = NameTable() +} From dda3e79be34a2483cf11afefeb598679eeda51cd Mon Sep 17 00:00:00 2001 From: Liu Fengyun Date: Mon, 26 Apr 2021 22:05:45 +0200 Subject: [PATCH 44/47] Pinpoint the first leak correctly --- .../tools/dotc/transform/init/CycleChecker.scala | 6 +++--- .../src/dotty/tools/dotc/transform/init/Errors.scala | 4 ++-- tests/init/neg/global-cycle13.check | 11 +++++++++++ tests/init/neg/t3273.check | 8 ++++---- 4 files changed, 20 insertions(+), 9 deletions(-) create mode 100644 tests/init/neg/global-cycle13.check diff --git a/compiler/src/dotty/tools/dotc/transform/init/CycleChecker.scala b/compiler/src/dotty/tools/dotc/transform/init/CycleChecker.scala index a33252522164..8959b46a4926 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/CycleChecker.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/CycleChecker.scala @@ -56,7 +56,7 @@ case class ObjectAccess(symbol: Symbol)(val source: Vector[Tree]) extends Depend /** Depend on usage of an instance, which can be either a class instance or object */ case class InstanceUsage(symbol: ClassSymbol, instanceClass: ClassSymbol)(val source: Vector[Tree]) extends Dependency { - def show(using Context): String = "InstanceUsage(" + symbol.show + ")" + def show(using Context): String = "InstanceUsage(" + symbol.show + "," + instanceClass.show + ")" } /** A static method call detected from fine-grained analysis @@ -147,7 +147,7 @@ class CycleChecker(cache: Cache) { val ctor = obj.moduleClass.primaryConstructor var trace = state.trace.dropWhile(_.symbol != ctor) :+ dep - val pinpointOpt = trace.find(_.isInstanceOf[InstanceUsage]) + val pinpointOpt = trace.find(dep => dep.isInstanceOf[InstanceUsage] || dep.isInstanceOf[ProxyUsage]) val traceSuppress = trace.size > traceNumberLimit if traceSuppress then // truncate trace up to the first escape of object @@ -244,7 +244,7 @@ class CycleChecker(cache: Cache) { val env = Env(ctx.withOwner(dep.cls), cache) val state = new Checking.State( visited = Set.empty, - path = dep.source, + path = Vector.empty, thisClass = dep.cls, fieldsInited = mutable.Set.empty, parentsInited = mutable.Set.empty, diff --git a/compiler/src/dotty/tools/dotc/transform/init/Errors.scala b/compiler/src/dotty/tools/dotc/transform/init/Errors.scala index a12830d77333..0668ccda0a59 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Errors.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Errors.scala @@ -42,8 +42,8 @@ object Errors { this private def stacktracePrefix: String = - val str = if traceSuppressed then "suppressed" else "full" - " Calling trace (" + str + "):\n" + val note = if traceSuppressed then " (suppressed)" else "" + " Calling trace" + note + ":\n" private val render = new MessageRendering {} diff --git a/tests/init/neg/global-cycle13.check b/tests/init/neg/global-cycle13.check new file mode 100644 index 000000000000..ecbd4a8a09be --- /dev/null +++ b/tests/init/neg/global-cycle13.check @@ -0,0 +1,11 @@ +-- Error: tests/init/neg/global-cycle13.scala:1:0 ---------------------------------------------------------------------- +1 |object Names { // error + |^ + |object Names leaked during its initialization . Calling trace: + |[ global-cycle13.scala:1 ] -> object Names { // error + |[ global-cycle13.scala:21 ] -> val nameTable = NameTable() + |[ global-cycle13.scala:14 ] -> add(0, EmptyTermName) + | ^^^^^^^^^^^^^ + | Leaking the object may cause initialization problems + |[ global-cycle13.scala:18 ] -> val EmptyTermName: SimpleName = SimpleName(-1, 0) + |[ global-cycle13.scala:8 ] -> def foo() = println(EmptyTermName.toString) diff --git a/tests/init/neg/t3273.check b/tests/init/neg/t3273.check index b355402c1e4a..a0b915555106 100644 --- a/tests/init/neg/t3273.check +++ b/tests/init/neg/t3273.check @@ -1,20 +1,20 @@ -- 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. Calling trace (full): + | Promoting the value to fully-initialized is unsafe. Calling trace: | [ t3273.scala:4 ] -> val num1: LazyList[Int] = 1 #:: num1.map(_ + 1) // error | | The unsafe promotion may cause the following problem(s): | - | 1. Access non-initialized value num1. Calling trace (full): + | 1. Access non-initialized value num1. Calling trace: | [ t3273.scala:4 ] -> val num1: LazyList[Int] = 1 #:: num1.map(_ + 1) // error -- 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. Calling trace (full): + | Promoting the value to fully-initialized is unsafe. Calling trace: | [ t3273.scala:5 ] -> val num2: LazyList[Int] = 1 #:: num2.iterator.map(_ + 1).to(LazyList) // error | | The unsafe promotion may cause the following problem(s): | - | 1. Access non-initialized value num2. Calling trace (full): + | 1. Access non-initialized value num2. Calling trace: | [ t3273.scala:5 ] -> val num2: LazyList[Int] = 1 #:: num2.iterator.map(_ + 1).to(LazyList) // error From cabb31890f7beb9b3c0f8f24a5f61de91cb0b8c5 Mon Sep 17 00:00:00 2001 From: Liu Fengyun Date: Mon, 26 Apr 2021 22:27:38 +0200 Subject: [PATCH 45/47] Ignore contructor call of noInit traits and interface --- .../tools/dotc/transform/init/CycleChecker.scala | 13 ++++++++++--- tests/init/neg/global-cycle14.scala | 14 ++++++++++++++ 2 files changed, 24 insertions(+), 3 deletions(-) create mode 100644 tests/init/neg/global-cycle14.scala diff --git a/compiler/src/dotty/tools/dotc/transform/init/CycleChecker.scala b/compiler/src/dotty/tools/dotc/transform/init/CycleChecker.scala index 8959b46a4926..62454101a264 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/CycleChecker.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/CycleChecker.scala @@ -112,6 +112,13 @@ class CycleChecker(cache: Cache) { def state(using ev: State) = ev // ----- checking ------------------------------- + def allowExternalCall(meth: Symbol)(using Context): Boolean = + meth.isConstructor && + (meth.owner.isAllOf(Flags.JavaInterface) + || meth.owner.isAllOf(Flags.NoInitsTrait) + || defn.isFunctionClass(meth.owner) + ) + def checkCyclic()(using Context): Unit = { val state = State(visited = mutable.Set.empty, path = Vector.empty, trace = Vector.empty) objectsInCurrentRun.foreach { obj => @@ -186,11 +193,11 @@ class CycleChecker(cache: Cache) { deps.flatMap(check(_)) private def checkStaticCall(dep: StaticCall)(using Context, State): List[Error] = - if !classesInCurrentRun.contains(dep.cls) then + if !classesInCurrentRun.contains(dep.cls) || allowExternalCall(dep.symbol) then Util.traceIndented("skip " + dep.show + " which is not in current run ", init) Nil else if !dep.symbol.hasSource then - CallUnknown(dep.symbol, dep.source.last, state.stackTrace) :: Nil + CallUnknown(dep.symbol, dep.source.last, dep.source) :: Nil else { val deps = methodDependencies(dep) deps.flatMap(check(_)) @@ -201,7 +208,7 @@ class CycleChecker(cache: Cache) { Util.traceIndented("skip " + dep.show + " which is not in current run ", init) Nil else if !dep.symbol.hasSource then - CallUnknown(dep.symbol, dep.source.last, state.stackTrace) :: Nil + CallUnknown(dep.symbol, dep.source.last, dep.source) :: Nil else { val deps = proxyDependencies(dep) deps.flatMap(check(_)) diff --git a/tests/init/neg/global-cycle14.scala b/tests/init/neg/global-cycle14.scala new file mode 100644 index 000000000000..ab416424478b --- /dev/null +++ b/tests/init/neg/global-cycle14.scala @@ -0,0 +1,14 @@ +object O { + case class Data(x: Int) extends (Int => Int) { + def apply(x: Int) = x * x + } + val d = Data(3) +} + +object A { // error + val n: Int = B.m +} + +object B { + val m: Int = A.n +} From 6211f4c126e8276af81b117269f0d00b74320585 Mon Sep 17 00:00:00 2001 From: Liu Fengyun Date: Mon, 26 Apr 2021 23:23:18 +0200 Subject: [PATCH 46/47] Correct indentation independent of file name size --- .../tools/dotc/transform/init/Errors.scala | 18 +++++++++++++----- tests/init/neg/global-cycle13.check | 10 +++++----- tests/init/neg/t3273.check | 8 ++++---- 3 files changed, 22 insertions(+), 14 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/transform/init/Errors.scala b/compiler/src/dotty/tools/dotc/transform/init/Errors.scala index 0668ccda0a59..67439f202eba 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Errors.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Errors.scala @@ -64,21 +64,29 @@ object Errors { var last: SourcePosition = NoSourcePosition val sb = new StringBuilder trace.foreach { tree => - indentCount += 1 val pos = tree.sourcePos - val prefix = (" " * indentCount) + "-> " var pinpoint = "" val line = if pos.source.exists then val locText = "[ " + pos.source.file.name + ":" + (pos.line + 1) + " ]" val loc = Highlighting.Blue(locText) val code = SyntaxHighlighting.highlight(pos.lineContent) + + var prefix = loc + " " + if locText.size <= indentCount then + prefix = prefix + (" " * (indentCount - locText.size + 1)) + indentCount = indentCount + 1 + else + indentCount = locText.length + 1 + if pinpoints.contains(tree) then - pinpoint = pinpointText(pos, pinpoints(tree), locText.length + prefix.length) + pinpoint = pinpointText(pos, pinpoints(tree), indentCount + 4) - i"$loc$prefix$code" + i"$prefix-> $code" else - tree.show + indentCount += 1 + val prefix = " " * indentCount + i"$prefix-> ${tree.show}" if (last.source != pos.source || last.line != pos.line) sb.append(line + pinpoint) diff --git a/tests/init/neg/global-cycle13.check b/tests/init/neg/global-cycle13.check index ecbd4a8a09be..6318c994a21a 100644 --- a/tests/init/neg/global-cycle13.check +++ b/tests/init/neg/global-cycle13.check @@ -2,10 +2,10 @@ 1 |object Names { // error |^ |object Names leaked during its initialization . Calling trace: - |[ global-cycle13.scala:1 ] -> object Names { // error - |[ global-cycle13.scala:21 ] -> val nameTable = NameTable() - |[ global-cycle13.scala:14 ] -> add(0, EmptyTermName) + |[ global-cycle13.scala:1 ] -> object Names { // error + |[ global-cycle13.scala:21 ] -> val nameTable = NameTable() + |[ global-cycle13.scala:14 ] -> add(0, EmptyTermName) | ^^^^^^^^^^^^^ | Leaking the object may cause initialization problems - |[ global-cycle13.scala:18 ] -> val EmptyTermName: SimpleName = SimpleName(-1, 0) - |[ global-cycle13.scala:8 ] -> def foo() = println(EmptyTermName.toString) + |[ global-cycle13.scala:18 ] -> val EmptyTermName: SimpleName = SimpleName(-1, 0) + |[ global-cycle13.scala:8 ] -> def foo() = println(EmptyTermName.toString) diff --git a/tests/init/neg/t3273.check b/tests/init/neg/t3273.check index a0b915555106..f1a14e8fc642 100644 --- a/tests/init/neg/t3273.check +++ b/tests/init/neg/t3273.check @@ -2,19 +2,19 @@ 4 | val num1: LazyList[Int] = 1 #:: num1.map(_ + 1) // error | ^^^^^^^^^^^^^^^ | Promoting the value to fully-initialized is unsafe. Calling trace: - | [ t3273.scala:4 ] -> val num1: LazyList[Int] = 1 #:: num1.map(_ + 1) // error + | [ t3273.scala:4 ] -> val num1: LazyList[Int] = 1 #:: num1.map(_ + 1) // error | | The unsafe promotion may cause the following problem(s): | | 1. Access non-initialized value num1. Calling trace: - | [ t3273.scala:4 ] -> val num1: LazyList[Int] = 1 #:: num1.map(_ + 1) // error + | [ t3273.scala:4 ] -> val num1: LazyList[Int] = 1 #:: num1.map(_ + 1) // error -- 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. Calling trace: - | [ t3273.scala:5 ] -> val num2: LazyList[Int] = 1 #:: num2.iterator.map(_ + 1).to(LazyList) // error + | [ t3273.scala:5 ] -> val num2: LazyList[Int] = 1 #:: num2.iterator.map(_ + 1).to(LazyList) // error | | The unsafe promotion may cause the following problem(s): | | 1. Access non-initialized value num2. Calling trace: - | [ t3273.scala:5 ] -> val num2: LazyList[Int] = 1 #:: num2.iterator.map(_ + 1).to(LazyList) // error + | [ t3273.scala:5 ] -> val num2: LazyList[Int] = 1 #:: num2.iterator.map(_ + 1).to(LazyList) // error From 1c29167719fbb8e82ea12b0a8160942330c66961 Mon Sep 17 00:00:00 2001 From: Liu Fengyun Date: Tue, 27 Apr 2021 00:59:29 +0200 Subject: [PATCH 47/47] More compact trace --- .../dotty/tools/dotc/transform/init/CycleChecker.scala | 2 +- compiler/src/dotty/tools/dotc/transform/init/Errors.scala | 8 +++----- tests/init/neg/global-cycle13.check | 3 +-- 3 files changed, 5 insertions(+), 8 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/transform/init/CycleChecker.scala b/compiler/src/dotty/tools/dotc/transform/init/CycleChecker.scala index 62454101a264..b6ec77dd2f9a 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/CycleChecker.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/CycleChecker.scala @@ -174,7 +174,7 @@ class CycleChecker(cache: Cache) { ObjectLeakDuringInit(obj, locations, traceSuppress) if pinpointOpt.nonEmpty then - warning.pinpoint(pinpointOpt.get.source.last, "Leaking the object may cause initialization problems") + warning.pinpoint(pinpointOpt.get.source.last, "Unsafe leaking of object") warning :: Nil else diff --git a/compiler/src/dotty/tools/dotc/transform/init/Errors.scala b/compiler/src/dotty/tools/dotc/transform/init/Errors.scala index 67439f202eba..da75e5e354c4 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Errors.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Errors.scala @@ -48,16 +48,14 @@ object Errors { private val render = new MessageRendering {} private def pinpointText(pos: SourcePosition, msg: String, offset: Int)(using Context): String = - val carets = render.hl("Warning") { + val carets = render.hl("Warning")({ if (pos.startLine == pos.endLine) "^" * math.max(1, pos.endColumn - pos.startColumn) else "^" - } + } + " <~~ " + msg) val padding = pos.startColumnPadding + (" " * offset) - val marker = padding + carets - val textline = padding + msg - marker + "\n" + textline + "\n" + padding + carets + "\n" def stacktrace(using Context): String = if (trace.isEmpty) "" else stacktracePrefix + { var indentCount = 0 diff --git a/tests/init/neg/global-cycle13.check b/tests/init/neg/global-cycle13.check index 6318c994a21a..df1c40869a88 100644 --- a/tests/init/neg/global-cycle13.check +++ b/tests/init/neg/global-cycle13.check @@ -5,7 +5,6 @@ |[ global-cycle13.scala:1 ] -> object Names { // error |[ global-cycle13.scala:21 ] -> val nameTable = NameTable() |[ global-cycle13.scala:14 ] -> add(0, EmptyTermName) - | ^^^^^^^^^^^^^ - | Leaking the object may cause initialization problems + | ^^^^^^^^^^^^^ <~~ Unsafe leaking of object |[ global-cycle13.scala:18 ] -> val EmptyTermName: SimpleName = SimpleName(-1, 0) |[ global-cycle13.scala:8 ] -> def foo() = println(EmptyTermName.toString)