diff --git a/compiler/src/dotty/tools/dotc/Compiler.scala b/compiler/src/dotty/tools/dotc/Compiler.scala index 8c22bf5b9293..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 @@ -64,8 +65,7 @@ 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 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..b463d651d546 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,39 @@ class Checker extends MiniPhase { // cache of class summary private val cache = new Cache + 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 - 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.checkCyclic() + newUnits + + val traverser = new TreeTraverser { + override def traverse(tree: Tree)(using Context): Unit = + tree match { + case tdef: TypeDef if tdef.isClassDef => + checkClassDef(tdef) + cycleChecker.classesInCurrentRun += tdef.symbol + 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 +80,14 @@ class Checker extends MiniPhase { fieldsInited = mutable.Set.empty, parentsInited = mutable.Set.empty, safePromoted = mutable.Set.empty, + dependencies = mutable.Set.empty, + superConstrCalled = false, env = Env(ctx.withOwner(cls), cache) ) Checking.checkClassBody(tree) + + 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 cc8f95b20e8c..ad731246f9d3 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Checking.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Checking.scala @@ -30,14 +30,17 @@ 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` fieldsInited: mutable.Set[Symbol], parentsInited: mutable.Set[ClassSymbol], safePromoted: mutable.Set[Potential], // Potentials that can be safely promoted - env: Env + 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 ) { def withOwner[T](sym: Symbol)(op: State ?=> T): T = val state = this.copy(env = env.withOwner(sym)) @@ -45,6 +48,8 @@ object Checking { this.visited = state.visited res + def isFieldInitialized(field: Symbol): Boolean = + 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) @@ -53,17 +58,15 @@ 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) } } 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) @@ -71,14 +74,13 @@ 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) } } - } private def checkEffects(effs: Effects)(using state: State): Unit = traceOp("checking effects " + Effects.show(effs), init) { for { @@ -87,6 +89,18 @@ 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) + + /** 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 * * It assumes that all definitions are properly summarized before-hand. @@ -164,13 +178,21 @@ object Checking { } tpl.parents.foreach { - case tree @ Block(_, parent) => + case tree @ Block(stats, parent) => + 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) => + val argss = termArgss(parent) + checkStats((stats :: args :: argss).flatten, cls) + checkConstructor(funPart(parent).symbol, tree.tpe, tree) case parent : Apply => + val argss = termArgss(parent) + checkStats(argss.flatten, cls) + checkConstructor(funPart(parent).symbol, parent.tpe, parent) case ref => @@ -179,10 +201,16 @@ 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(_) } } +// ------- checking effects --------------------------- + private def checkMethodCall(eff: MethodCall)(using state: State): Errors = val MethodCall(pot, sym) = eff pot match { @@ -198,7 +226,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 +246,28 @@ object Checking { else Errors.empty + case hot: Hot => + val target = resolve(hot.classSymbol, sym) + 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 => + if isObjectAccessSafe(obj) then + 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, sym)(eff.source)) + else + val target = resolve(obj.moduleClass, sym) + state.dependencies += StaticCall(obj.moduleClass, target)(state.path) + Errors.empty + case _: Cold => CallCold(sym, eff.source, state.path).toErrors @@ -239,13 +289,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) => @@ -254,6 +304,20 @@ object Checking { if (target.is(Flags.Lazy)) check(MethodCall(pot, target)(eff.source)) else Errors.empty + case hot: Hot => + 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 + val target = resolve(obj.moduleClass, field) + if (target.is(Flags.Lazy)) check(MethodCall(obj, target)(eff.source)) + else if isObjectAccessSafe(obj) then + check(FieldAccess(ThisRef()(obj.source), target)(eff.source)) + else Errors.empty + case _: Cold => AccessCold(field, eff.source, state.path).toErrors @@ -278,7 +342,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 @@ -293,6 +357,34 @@ object Checking { if (errors.isEmpty) Errors.empty else PromoteWarm(pot, eff.source, state.path).toErrors + case obj: Global => + state.dependencies += InstanceUsage(obj.moduleClass, obj.moduleClass)(state.path) + Errors.empty + + case hot: Hot => + 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)(state.path) + Errors.empty + + case MethodReturn(obj: Global, sym) => + val target = resolve(obj.moduleClass, sym) + 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)(state.path) + Errors.empty + + case FieldReturn(obj: Global, sym) => + val target = resolve(obj.moduleClass, sym) + state.dependencies += ProxyUsage(obj.moduleClass, target)(state.path) + Errors.empty + case Fun(pots, effs) => val errs1 = state.test { effs.toList.flatMap(check(_)) @@ -319,6 +411,14 @@ object Checking { errs } + private def checkAccessGlobal(eff: AccessGlobal)(using state: State): Errors = + val obj = eff.potential + 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) => @@ -352,6 +452,15 @@ object Checking { if (target.hasSource) Summary(warm.potentialsOf(target), Effects.empty) else Summary.empty // warning already issued in call effect + 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 => Summary.empty // error already reported, ignore @@ -381,6 +490,15 @@ object Checking { if (target.hasSource) Summary(warm.potentialsOf(target), Effects.empty) else Summary(Cold()(pot.source)) + 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 => Summary.empty // error already reported, ignore @@ -392,12 +510,12 @@ 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 => - throw new Exception("Unexpected code reached") + throw new Exception("Unexpected code reached " + pot.show) case warm: Warm => Summary(warm.resolveOuter(cls)) @@ -408,7 +526,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..b6ec77dd2f9a --- /dev/null +++ b/compiler/src/dotty/tools/dotc/transform/init/CycleChecker.scala @@ -0,0 +1,420 @@ +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._ +import config.Printers.init +import ast.tpd._ + +import Errors._, Potentials._, Effects._, Util._ + +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 simple and fast, the algorithm uses a combination of + * coarse-grained analysis and fine-grained analysis. + * + * Fine-grained dependencies are collected from the initialization + * check for static objects. + * + * Coarse-grained dependencies are created 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. + * + */ +trait Dependency { + def symbol: Symbol + def source: Vector[Tree] + def show(using Context): String +} + +/** Depend on the initialization of another object */ +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: Vector[Tree]) extends Dependency { + def show(using Context): String = "InstanceUsage(" + symbol.show + "," + instanceClass.show + ")" +} + +/** 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. + * + * Note: Virtual method resolution should have been performed for the target. + * + */ +case class StaticCall(cls: ClassSymbol, symbol: Symbol)(val source: Vector[Tree]) extends Dependency { + def show(using Context): String = "StaticCall(" + cls.show + ", " + symbol.show + ")" +} + +/** 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: Vector[Tree]) extends Dependency { + def show(using Context): String = "ProxyUsage(" + cls.show + ", " + symbol.show + ")" +} + +class CycleChecker(cache: Cache) { + private val summaryCache = mutable.Map.empty[Symbol, List[Dependency]] + private val proxyCache = mutable.Map.empty[Symbol, List[Dependency]] + + 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 = + this.visited += dep + val state2: State = this.copy(trace = trace :+ dep) + val res = op(using state2) + res + + 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 + + def stackTrace: Vector[Tree] = trace.flatMap(_.source) + + } + + 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 => + val dep = ObjectAccess(obj)(Vector(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])) { + dep match + case dep: ObjectAccess => checkObjectAccess(dep) + case _ => + if state.visited.contains(dep) then Nil + else state.visit(dep) { + dep match + case dep: InstanceUsage => checkInstanceUsage(dep) + case dep: StaticCall => checkStaticCall(dep) + case dep: ProxyUsage => checkProxyUsage(dep) + } + } + + private def checkObjectAccess(dep: ObjectAccess)(using Context, State): List[Error] = + 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 ctor = obj.moduleClass.primaryConstructor + var trace = state.trace.dropWhile(_.symbol != ctor) :+ dep + + 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 + 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) + 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, "Unsafe leaking of object") + + warning :: Nil + else + val constr = obj.moduleClass.primaryConstructor + state.visitObject(dep) { + check(StaticCall(constr.owner.asClass, constr)(Vector(obj.moduleClass.defTree))) + } + } + + 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 deps = instanceDependencies(dep.symbol, dep.instanceClass) + deps.flatMap(check(_)) + + private def checkStaticCall(dep: StaticCall)(using Context, State): List[Error] = + 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, dep.source) :: Nil + else { + val deps = methodDependencies(dep) + deps.flatMap(check(_)) + } + + private def checkProxyUsage(dep: ProxyUsage)(using Context, State): List[Error] = + 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 + CallUnknown(dep.symbol, dep.source.last, dep.source) :: Nil + else { + val deps = proxyDependencies(dep) + deps.flatMap(check(_)) + } + +// ----- 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.asClass + + if isStaticObjectClass(cls) then + objectsInCurrentRun += cls.sourceModule + + 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, instanceClass) + summaryCache(cls) = deps + deps + } + + 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) { + if call.symbol.isOneOf(Flags.Method | Flags.Lazy) then + val deps = analyzeMethod(call) + summaryCache(call.symbol) = deps + deps + else + Nil + } + } + + 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)) proxyCache(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, + superConstrCalled = true, + env = env, + init = true + ) + + 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) + + 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 + + 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] + + var deps = new mutable.ListBuffer[Dependency] + + val traverser = new TreeTraverser { + override def traverse(tree: Tree)(using Context): Unit = + tree match { + case tree @ Template(_, parents, self, _) => + tree.body.foreach { + case ddef: DefDef if !ddef.symbol.isConstructor => + traverse(ddef) + case vdef: ValDef => + if vdef.symbol.is(Flags.Lazy) then + traverse(vdef) + else + 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)(Vector(tree)) + deps += StaticCall(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: Ident if tree.isTerm => + deps ++= analyzeType(tree.tpe, tree, exclude = cls) + + case tree: This => + deps ++= analyzeType(tree.tpe, tree, exclude = cls) + + case tree: ValOrDefDef => + traverseChildren(tree.rhs) + + case tdef: TypeDef => + // don't go into nested classes + + case _ => + traverseChildren(tree) + } + } + + // TODO: the traverser might create duplicate entries for parents + tpl.parents.foreach { tree => + val tp = tree.tpe + deps += InstanceUsage(tp.classSymbol.asClass, instanceClass)(Vector(tree)) + } + + traverser.traverse(tpl) + 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 + + case tmref: TermRef if isStaticObjectRef(tmref.symbol) => + val obj = tmref.symbol + val cls = obj.moduleClass.asClass + ObjectAccess(obj)(Vector(source)) :: InstanceUsage(cls, cls)(Vector(source)) :: Nil + + case tmref @ TermRef(prefix: TermRef, _) if isStaticObjectRef(prefix.symbol) => + 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) + + case ThisType(tref) => + if isStaticObjectRef(tref.symbol.sourceModule) && tref.symbol != exclude + then + val cls = tref.symbol.asClass + val obj = cls.sourceModule + ObjectAccess(obj)(Vector(source)) :: InstanceUsage(cls, cls)(Vector(source)) :: Nil + else + Nil + + case SuperType(thisTp, _) => + analyzeType(thisTp, source, exclude) + + case _: TypeRef | _: AppliedType => + // possible from parent list + Nil + + case AnnotatedType(tp, _) => + analyzeType(tp, source, exclude) + + case _ => + Nil + } + + private def analyzeMethod(dep: StaticCall)(using Context): List[Dependency] = { + val env = Env(ctx.withOwner(dep.cls), cache) + val state = 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, + superConstrCalled = true, + env = env, + init = true + ) + + val pot = Hot(dep.cls)(dep.source.last) + val effs = pot.effectsOf(dep.symbol)(using env) + + val errs = effs.flatMap(Checking.check(_)(using state)) + errs.foreach(_.issue) + + state.dependencies.toList + } + + +// ----- cleanup ------------------------ + + def clean() = { + summaryCache.clear() + proxyCache.clear() + classesInCurrentRun.clear() + objectsInCurrentRun.clear() + } +} 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..da75e5e354c4 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Errors.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Errors.scala @@ -8,6 +8,10 @@ import ast.tpd._ import core._ import Decorators._, printing.SyntaxHighlighting import Types._, Symbols._, Contexts._ +import util.{ SimpleIdentityMap, SourcePosition, NoSourcePosition } + +import reporting.MessageRendering +import printing.Highlighting import Effects._, Potentials._ @@ -20,33 +24,72 @@ object Errors { sealed trait Error { def source: Tree - def trace: Vector[Tree] + 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" + { + /** 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 note = if traceSuppressed then " (suppressed)" else "" + " Calling trace" + note + ":\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 "^" + } + " <~~ " + msg) + + val padding = pos.startColumnPadding + (" " * offset) + padding + carets + "\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 }-> " + var pinpoint = "" 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 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), indentCount + 4) + + i"$prefix-> $code" else - tree.show + indentCount += 1 + val prefix = " " * indentCount + i"$prefix-> ${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 } @@ -60,7 +103,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,40 +112,57 @@ object Errors { report.warning(show + stacktrace, field.srcPos) } + 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(", ") + "." + + override def issue(using Context): Unit = + report.warning(show + stacktrace, objs.head.srcPos) + } + + 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 " + "." + + 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 { + 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" + "." + "Unable to analyze external " + meth.show + "." } /** 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 = @@ -110,7 +170,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/compiler/src/dotty/tools/dotc/transform/init/Potentials.scala b/compiler/src/dotty/tools/dotc/transform/init/Potentials.scala index 39eac29a2741..d8ba611e9fc4 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) } @@ -40,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 @@ -52,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 @@ -153,6 +159,27 @@ object Potentials { "Fun[pots = " + potentials.map(_.show).mkString(";") + ", effs = " + effects.map(_.show).mkString(";") + "]" } + /** 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 + + def appearsInside(sym: Symbol)(using Context): Boolean = + enclosingClass.ownersIterator.exists(_ == sym) + } + + /** 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 +245,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..175ec48dd19a 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Summarization.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Summarization.scala @@ -40,11 +40,14 @@ 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) + 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) @@ -90,7 +93,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 +101,12 @@ 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 + if outer.isGlobal then Summary(Hot(cls)(expr)) + else summary.dropPotentials + Warm(cls, outer)(expr) } } @@ -236,6 +241,14 @@ 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 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) if (env.canIgnoreMethod(tmref.symbol)) Summary(effs) @@ -246,8 +259,15 @@ 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 + then + val sym = tref.symbol.sourceModule + val pot = Global(sym, enclosing)(source) + Summary(pot) + AccessGlobal(pot) + else + val cls = tref.symbol.asClass + resolveThis(cls, ThisRef()(source), enclosing, source) case SuperType(thisTp, superTp) => val Summary(pots, effs) = analyze(thisTp, source) @@ -285,12 +305,14 @@ 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 enclosing = env.ctx.owner.lexicallyEnclosingClass.asClass + val pot = Global(cls.sourceModule, enclosing)(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 +400,4 @@ object Summarization { ClassSummary(cls, parentOuter.toMap) } } - } 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/constant.scala b/tests/init/neg/constant.scala new file mode 100644 index 000000000000..01e17e1c9e62 --- /dev/null +++ b/tests/init/neg/constant.scala @@ -0,0 +1,4 @@ +class A { + final val a = b + final val b = 4 // error +} \ No newline at end of file diff --git a/tests/init/neg/features-trees2.scala b/tests/init/neg/features-trees2.scala new file mode 100644 index 000000000000..c236e56de17c --- /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/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-cycle1.scala b/tests/init/neg/global-cycle1.scala new file mode 100644 index 000000000000..35368191d9d0 --- /dev/null +++ b/tests/init/neg/global-cycle1.scala @@ -0,0 +1,10 @@ +object A { // error + val a: Int = B.b +} + +object B { + val b: Int = A.a +} + +@main +def Test = print(A.a) \ No newline at end of file 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-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-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/global-cycle13.check b/tests/init/neg/global-cycle13.check new file mode 100644 index 000000000000..df1c40869a88 --- /dev/null +++ b/tests/init/neg/global-cycle13.check @@ -0,0 +1,10 @@ +-- 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) + | ^^^^^^^^^^^^^ <~~ Unsafe leaking of object + |[ 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/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() +} 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 +} 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/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 diff --git a/tests/init/neg/global-cycle7.scala b/tests/init/neg/global-cycle7.scala new file mode 100644 index 000000000000..32fdc4eb7b10 --- /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 // error: but constant folding avoided the issue +} \ 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() +} + diff --git a/tests/init/neg/global-cycle9.scala b/tests/init/neg/global-cycle9.scala new file mode 100644 index 000000000000..c2d047b97022 --- /dev/null +++ b/tests/init/neg/global-cycle9.scala @@ -0,0 +1,28 @@ +object Names { + 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() +} + +object A { // error + val n: Int = B.m +} + +object B { + val m: Int = A.n +} 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/inner9.scala b/tests/init/neg/inner9.scala index db5198ea0138..da0c0acc815f 100644 --- a/tests/init/neg/inner9.scala +++ b/tests/init/neg/inner9.scala @@ -5,8 +5,7 @@ object Flags { new Flags.Inner - val a = this.b + 3 - val b = 5 // error + val b = 5 // error } object Flags2 { diff --git a/tests/init/neg/t3273.check b/tests/init/neg/t3273.check index 74c016ef521f..f1a14e8fc642 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: - | -> val num1: LazyList[Int] = 1 #:: num1.map(_ + 1) // error [ t3273.scala:4 ] + | 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): + | The unsafe promotion may cause the following problem(s): | - | 1. Access non-initialized value num1. Calling trace: - | -> val num1: LazyList[Int] = 1 #:: num1.map(_ + 1) // error [ t3273.scala:4 ] + | 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: - | -> 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: + | [ 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: - | -> val num2: LazyList[Int] = 1 #:: num2.iterator.map(_ + 1).to(LazyList) // error [ t3273.scala:5 ] + | 1. Access non-initialized value num2. Calling trace: + | [ t3273.scala:5 ] -> val num2: LazyList[Int] = 1 #:: num2.iterator.map(_ + 1).to(LazyList) // error diff --git a/tests/init/neg/t5366.scala b/tests/init/neg/t5366.scala new file mode 100644 index 000000000000..854bdfe0544b --- /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) + +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..620652b540e0 --- /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 + 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 new file mode 100644 index 000000000000..1e23bedb9b6a --- /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) diff --git a/tests/init/neg/t9312.scala b/tests/init/neg/t9312.scala new file mode 100644 index 000000000000..d4eed8d9e137 --- /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 + object Child2 extends Child + + final val children = Set(Child1, Child2) + } +} diff --git a/tests/init/neg/t9360.scala b/tests/init/neg/t9360.scala new file mode 100644 index 000000000000..71f0af4c0fa1 --- /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) + + object BObj extends BaseClass(s) + + val list = List(AObj, BObj) + + def print = { + println(list) + } +} + +object ObjectInit { + def main(args: Array[String]) = { + Obj.AObj.print + Obj.BObj.print + Obj.print + } +} diff --git a/tests/init/pos/constant.scala b/tests/init/pos/constant.scala deleted file mode 100644 index 042c264171ba..000000000000 --- a/tests/init/pos/constant.scala +++ /dev/null @@ -1,4 +0,0 @@ -class A { - final val a = b - final val b = 4 -} \ No newline at end of file