diff --git a/compiler/src/dotty/tools/dotc/config/ScalaSettings.scala b/compiler/src/dotty/tools/dotc/config/ScalaSettings.scala index 0ddde23dc39f..731430c175a7 100644 --- a/compiler/src/dotty/tools/dotc/config/ScalaSettings.scala +++ b/compiler/src/dotty/tools/dotc/config/ScalaSettings.scala @@ -381,6 +381,7 @@ private sealed trait YSettings: val YnoKindPolymorphism: Setting[Boolean] = BooleanSetting("-Yno-kind-polymorphism", "Disable kind polymorphism.") val YexplicitNulls: Setting[Boolean] = BooleanSetting("-Yexplicit-nulls", "Make reference types non-nullable. Nullable types can be expressed with unions: e.g. String|Null.") val YcheckInit: Setting[Boolean] = BooleanSetting("-Ysafe-init", "Ensure safe initialization of objects") + val YcheckInitGlobal: Setting[Boolean] = BooleanSetting("-Ysafe-init-global", "Check safe initialization of global objects") val YrequireTargetName: Setting[Boolean] = BooleanSetting("-Yrequire-targetName", "Warn if an operator is defined without a @targetName annotation") val YrecheckTest: Setting[Boolean] = BooleanSetting("-Yrecheck-test", "Run basic rechecking (internal test only)") val YccDebug: Setting[Boolean] = BooleanSetting("-Ycc-debug", "Used in conjunction with captureChecking language import, debug info for captured references") @@ -398,4 +399,3 @@ private sealed trait YSettings: val YforceInlineWhileTyping: Setting[Boolean] = BooleanSetting("-Yforce-inline-while-typing", "Make non-transparent inline methods inline when typing. Emulates the old inlining behavior of 3.0.0-M3.") end YSettings - diff --git a/compiler/src/dotty/tools/dotc/core/Definitions.scala b/compiler/src/dotty/tools/dotc/core/Definitions.scala index 3d11ad80733e..ca92364c5b9b 100644 --- a/compiler/src/dotty/tools/dotc/core/Definitions.scala +++ b/compiler/src/dotty/tools/dotc/core/Definitions.scala @@ -1047,6 +1047,11 @@ class Definitions { @tu lazy val JavaRepeatableAnnot: ClassSymbol = requiredClass("java.lang.annotation.Repeatable") + // Initialization annotations + @tu lazy val InitModule: Symbol = requiredModule("scala.annotation.init") + @tu lazy val InitWidenAnnot: ClassSymbol = InitModule.requiredClass("widen") + @tu lazy val InitRegionMethod: Symbol = InitModule.requiredMethod("region") + // A list of meta-annotations that are relevant for fields and accessors @tu lazy val NonBeanMetaAnnots: Set[Symbol] = Set(FieldMetaAnnot, GetterMetaAnnot, ParamMetaAnnot, SetterMetaAnnot, CompanionClassMetaAnnot, CompanionMethodMetaAnnot) diff --git a/compiler/src/dotty/tools/dotc/core/Symbols.scala b/compiler/src/dotty/tools/dotc/core/Symbols.scala index 07ac2be90819..c13591fb9f5a 100644 --- a/compiler/src/dotty/tools/dotc/core/Symbols.scala +++ b/compiler/src/dotty/tools/dotc/core/Symbols.scala @@ -83,7 +83,8 @@ object Symbols { ctx.settings.YretainTrees.value || denot.owner.isTerm || // no risk of leaking memory after a run for these denot.isOneOf(InlineOrProxy) || // need to keep inline info - ctx.settings.YcheckInit.value // initialization check + ctx.settings.YcheckInit.value || // initialization check + ctx.settings.YcheckInitGlobal.value /** The last denotation of this symbol */ private var lastDenot: SymDenotation = _ diff --git a/compiler/src/dotty/tools/dotc/core/Types.scala b/compiler/src/dotty/tools/dotc/core/Types.scala index bbec037ebef1..adf375344537 100644 --- a/compiler/src/dotty/tools/dotc/core/Types.scala +++ b/compiler/src/dotty/tools/dotc/core/Types.scala @@ -376,6 +376,15 @@ object Types { case _ => false } + /** Returns the annotation that is an instance of `cls` carried by the type. */ + @tailrec final def getAnnotation(cls: ClassSymbol)(using Context): Option[Annotation] = stripTypeVar match { + case AnnotatedType(tp, annot) => + if annot.matches(cls) then Some(annot) + else tp.getAnnotation(cls) + case _ => + None + } + /** Does this type have a supertype with an annotation satisfying given predicate `p`? */ def derivesAnnotWith(p: Annotation => Boolean)(using Context): Boolean = this match { case tp: AnnotatedType => p(tp.annot) || tp.parent.derivesAnnotWith(p) diff --git a/compiler/src/dotty/tools/dotc/transform/init/Checker.scala b/compiler/src/dotty/tools/dotc/transform/init/Checker.scala index 1efb3c88149e..a8def2bd7f48 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Checker.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Checker.scala @@ -28,7 +28,7 @@ class Checker extends Phase: override val runsAfter = Set(Pickler.name) override def isEnabled(using Context): Boolean = - super.isEnabled && ctx.settings.YcheckInit.value + super.isEnabled && (ctx.settings.YcheckInit.value || ctx.settings.YcheckInitGlobal.value) override def runOn(units: List[CompilationUnit])(using Context): List[CompilationUnit] = val checkCtx = ctx.fresh.setPhase(this.start) @@ -36,7 +36,11 @@ class Checker extends Phase: units.foreach { unit => traverser.traverse(unit.tpdTree) } val classes = traverser.getClasses() - Semantic.checkClasses(classes)(using checkCtx) + if ctx.settings.YcheckInit.value then + Semantic.checkClasses(classes)(using checkCtx) + + if ctx.settings.YcheckInitGlobal.value then + Objects.checkClasses(classes)(using checkCtx) units diff --git a/compiler/src/dotty/tools/dotc/transform/init/Objects.scala b/compiler/src/dotty/tools/dotc/transform/init/Objects.scala new file mode 100644 index 000000000000..c271067ff0db --- /dev/null +++ b/compiler/src/dotty/tools/dotc/transform/init/Objects.scala @@ -0,0 +1,1455 @@ +package dotty.tools.dotc +package transform +package init + +import core.* +import Contexts.* +import Symbols.* +import Types.* +import StdNames.* +import NameKinds.OuterSelectName +import NameKinds.SuperAccessorName + +import ast.tpd.* +import util.SourcePosition +import config.Printers.init as printer +import reporting.StoreReporter +import reporting.trace as log + +import Errors.* +import Trace.* +import Util.* + +import scala.collection.immutable.ListSet +import scala.collection.mutable +import scala.annotation.tailrec +import scala.annotation.constructorOnly + +/** Check initialization safety of static objects + * + * The problem is illustrated by the example below: + * + * class Foo(val opposite: Foo) + * case object A extends Foo(B) // A -> B + * case object B extends Foo(A) // B -> A + * + * In the code above, the initialization of object `A` depends on `B` and vice versa. There is no + * correct way to initialize the code above. The current checker issues a warning for the code + * above. + * + * At the high-level, the analysis has the following characteristics: + * + * 1. The check enforces the principle of "initialization-time irrelevance", which means that the + * time when an object is initialized should not change program semantics. For that purpose, it + * enforces the following rule: + * + * The initialization of a static object should not directly or indirectly read or write + * mutable state of another static object. + * + * This principle not only put initialization of static objects on a solid foundation, but also + * avoids whole-program analysis. + * + * 2. The design is based on the concept of "cold aliasing" --- a cold alias may not be actively + * used during initialization, i.e., it's forbidden to call methods or access fields of a cold + * alias. Method arguments are cold aliases by default unless specified to be sensitive. Method + * parameters captured in lambdas or inner classes are always cold aliases. + * + * 3. It is inter-procedural and flow-sensitive. + * + * 4. It is object-sensitive by default and parameter-sensitive on-demand. + * + * 5. The check is modular in the sense that each object is checked separately and there is no + * whole-program analysis. However, the check is not modular in terms of project boundaries. + * + */ +object Objects: + + // ----------------------------- abstract domain ----------------------------- + + sealed abstract class Value: + def show(using Context): String + + + /** + * A reference caches the values for outers and immutable fields. + */ + sealed abstract class Ref( + valsMap: mutable.Map[Symbol, Value], + varsMap: mutable.Map[Symbol, Heap.Addr], + outersMap: mutable.Map[ClassSymbol, Value]) + extends Value: + protected val vals: mutable.Map[Symbol, Value] = valsMap + protected val vars: mutable.Map[Symbol, Heap.Addr] = varsMap + protected val outers: mutable.Map[ClassSymbol, Value] = outersMap + + def isObjectRef: Boolean = this.isInstanceOf[ObjectRef] + + def klass: ClassSymbol + + def valValue(sym: Symbol): Value = vals(sym) + + def varAddr(sym: Symbol): Heap.Addr = vars(sym) + + def outerValue(cls: ClassSymbol): Value = outers(cls) + + def hasVal(sym: Symbol): Boolean = vals.contains(sym) + + def hasVar(sym: Symbol): Boolean = vars.contains(sym) + + def hasOuter(cls: ClassSymbol): Boolean = outers.contains(cls) + + def initVal(field: Symbol, value: Value)(using Context) = log("Initialize " + field.show + " = " + value + " for " + this, printer) { + assert(!field.is(Flags.Mutable), "Field is mutable: " + field.show) + assert(!vals.contains(field), "Field already set: " + field.show) + vals(field) = value + } + + def initVar(field: Symbol, addr: Heap.Addr)(using Context) = log("Initialize " + field.show + " = " + addr + " for " + this, printer) { + assert(field.is(Flags.Mutable), "Field is not mutable: " + field.show) + assert(!vars.contains(field), "Field already set: " + field.show) + vars(field) = addr + } + + def initOuter(cls: ClassSymbol, value: Value)(using Context) = log("Initialize outer " + cls.show + " = " + value + " for " + this, printer) { + assert(!outers.contains(cls), "Outer already set: " + cls) + outers(cls) = value + } + + /** A reference to a static object */ + case class ObjectRef(klass: ClassSymbol) + extends Ref(valsMap = mutable.Map.empty, varsMap = mutable.Map.empty, outersMap = mutable.Map.empty): + val owner = klass + + def show(using Context) = "ObjectRef(" + klass.show + ")" + + /** + * Represents values that are instances of the specified class. + * + * Note that the 2nd parameter block does not take part in the definition of equality. + */ + case class OfClass private ( + klass: ClassSymbol, outer: Value, ctor: Symbol, args: List[Value], env: Env.Data)( + valsMap: mutable.Map[Symbol, Value], varsMap: mutable.Map[Symbol, Heap.Addr], outersMap: mutable.Map[ClassSymbol, Value]) + extends Ref(valsMap, varsMap, outersMap): + def widenedCopy(outer: Value, args: List[Value], env: Env.Data): OfClass = + new OfClass(klass, outer, ctor, args, env)(this.valsMap, this.varsMap, this.outersMap) + + def show(using Context) = + val valFields = vals.map(_.show + " -> " + _.show) + "OfClass(" + klass.show + ", outer = " + outer + ", args = " + args.map(_.show) + ", vals = " + valFields + ")" + + object OfClass: + def apply( + klass: ClassSymbol, outer: Value, ctor: Symbol, args: List[Value], env: Env.Data)( + using Context + ): OfClass = + val instance = new OfClass(klass, outer, ctor, args, env)( + valsMap = mutable.Map.empty, varsMap = mutable.Map.empty, outersMap = mutable.Map.empty + ) + instance.initOuter(klass, outer) + instance + + /** + * Represents arrays. + * + * Note that the 2nd parameter block does not take part in the definition of equality. + * + * Different arrays are distinguished by the context. Currently the default context is the static + * object whose initialization triggers the creation of the array. + * + * In the future, it is possible that we introduce a mechanism for end-users to mark the context. + * + * @param owner The static object whose initialization creates the array. + */ + case class OfArray(owner: ClassSymbol, regions: Regions.Data)(using @constructorOnly ctx: Context) + extends Ref(valsMap = mutable.Map.empty, varsMap = mutable.Map.empty, outersMap = mutable.Map.empty): + val klass: ClassSymbol = defn.ArrayClass + val addr: Heap.Addr = Heap.arrayAddr(regions, owner) + def show(using Context) = "OfArray(owner = " + owner.show + ")" + + /** + * Represents a lambda expression + */ + case class Fun(code: Tree, thisV: Value, klass: ClassSymbol, env: Env.Data) extends Value: + def show(using Context) = "Fun(" + code.show + ", " + thisV.show + ", " + klass.show + ")" + + /** + * Represents a set of values + * + * It comes from `if` expressions. + */ + case class RefSet(refs: ListSet[Value]) extends Value: + assert(refs.forall(!_.isInstanceOf[RefSet])) + def show(using Context) = refs.map(_.show).mkString("[", ",", "]") + + /** A cold alias which should not be used during initialization. */ + case object Cold extends Value: + def show(using Context) = "Cold" + + val Bottom = RefSet(ListSet.empty) + + /** Checking state */ + object State: + class Data: + // objects under check + private[State] val checkingObjects = new mutable.ArrayBuffer[ObjectRef] + private[State] val checkedObjects = new mutable.ArrayBuffer[ObjectRef] + private[State] val pendingTraces = new mutable.ArrayBuffer[Trace] + end Data + + def currentObject(using data: Data): ClassSymbol = data.checkingObjects.last.klass + + private def doCheckObject(classSym: ClassSymbol)(using ctx: Context, data: Data) = + val tpl = classSym.defTree.asInstanceOf[TypeDef].rhs.asInstanceOf[Template] + + var count = 0 + given Cache.Data = new Cache.Data + + @tailrec + def iterate()(using Context): ObjectRef = + count += 1 + + given Trace = Trace.empty.add(classSym.defTree) + given Env.Data = Env.emptyEnv(tpl.constr.symbol) + given Heap.MutableData = Heap.empty() + given returns: Returns.Data = Returns.empty() + given regions: Regions.Data = Regions.empty // explicit name to avoid naming conflict + + val obj = ObjectRef(classSym) + log("Iteration " + count) { + data.checkingObjects += obj + init(tpl, obj, classSym) + assert(data.checkingObjects.last.klass == classSym, "Expect = " + classSym.show + ", found = " + data.checkingObjects.last.klass) + data.checkingObjects.remove(data.checkingObjects.size - 1) + } + + val hasError = ctx.reporter.pendingMessages.nonEmpty + if cache.hasChanged && !hasError then + cache.prepareForNextIteration() + iterate() + else + data.checkedObjects += obj + obj + end iterate + + val reporter = new StoreReporter(ctx.reporter) + val obj = iterate()(using ctx.fresh.setReporter(reporter)) + for warning <- reporter.pendingMessages do + ctx.reporter.report(warning) + + obj + end doCheckObject + + def checkObjectAccess(clazz: ClassSymbol)(using data: Data, ctx: Context, pendingTrace: Trace): Value = + val index = data.checkingObjects.indexOf(ObjectRef(clazz)) + + if index != -1 then + if data.checkingObjects.size - 1 > index then + // Only report errors for non-trivial cycles, ignore self cycles. + val joinedTrace = data.pendingTraces.slice(index + 1, data.checkingObjects.size).foldLeft(pendingTrace) { (a, acc) => acc ++ a } + val callTrace = Trace.buildStacktrace(joinedTrace, "Calling trace:\n") + val cycle = data.checkingObjects.slice(index, data.checkingObjects.size) + val pos = clazz.defTree + report.warning("Cyclic initialization: " + cycle.map(_.klass.show).mkString(" -> ") + " -> " + clazz.show + ". " + callTrace, pos) + end if + data.checkingObjects(index) + else + val objOpt = data.checkedObjects.find(_.klass == clazz) + objOpt match + case Some(obj) => obj + + case None => + data.pendingTraces += pendingTrace + val obj = doCheckObject(clazz) + data.pendingTraces.remove(data.pendingTraces.size - 1) + obj + end checkObjectAccess + end State + + /** Environment for parameters */ + object Env: + abstract class Data: + private[Env] def getVal(x: Symbol)(using Context): Option[Value] + private[Env] def getVar(x: Symbol)(using Context): Option[Heap.Addr] + + def widen(height: Int)(using Context): Data + + def level: Int + + def show(using Context): String + + /** Local environments can be deeply nested, therefore we need `outer`. + * + * For local variables in rhs of class field definitions, the `meth` is the primary constructor. + */ + private case class LocalEnv + (private[Env] val params: Map[Symbol, Value], meth: Symbol, outer: Data) + (valsMap: mutable.Map[Symbol, Value], varsMap: mutable.Map[Symbol, Heap.Addr]) + (using Context) + extends Data: + val level = outer.level + 1 + + if (level > 3) + report.warning("[Internal error] Deeply nested environment, level = " + level + ", " + meth.show + " in " + meth.enclosingClass.show, meth.defTree) + + private[Env] val vals: mutable.Map[Symbol, Value] = valsMap + private[Env] val vars: mutable.Map[Symbol, Heap.Addr] = varsMap + + private[Env] def getVal(x: Symbol)(using Context): Option[Value] = + if x.is(Flags.Param) then params.get(x) + else vals.get(x) + + private[Env] def getVar(x: Symbol)(using Context): Option[Heap.Addr] = + vars.get(x) + + def widen(height: Int)(using Context): Data = + new LocalEnv(params.map(_ -> _.widen(height)), meth, outer.widen(height))(this.vals, this.vars) + + def show(using Context) = + "owner: " + meth.show + "\n" + + "params: " + params.map(_.show + " ->" + _.show).mkString("{", ", ", "}") + "\n" + + "vals: " + vals.map(_.show + " ->" + _.show).mkString("{", ", ", "}") + "\n" + + "vars: " + vars.map(_.show + " ->" + _).mkString("{", ", ", "}") + "\n" + + "outer = {\n" + outer.show + "\n}" + + end LocalEnv + + object NoEnv extends Data: + val level = 0 + + private[Env] def getVal(x: Symbol)(using Context): Option[Value] = + throw new RuntimeException("Invalid usage of non-existent env") + + private[Env] def getVar(x: Symbol)(using Context): Option[Heap.Addr] = + throw new RuntimeException("Invalid usage of non-existent env") + + def widen(height: Int)(using Context): Data = this + + def show(using Context): String = "NoEnv" + end NoEnv + + /** An empty environment can be used for non-method environments, e.g., field initializers. + * + * The owner for the local environment for field initializers is the primary constructor of the + * enclosing class. + */ + def emptyEnv(meth: Symbol)(using Context): Data = + new LocalEnv(Map.empty, meth, NoEnv)(valsMap = mutable.Map.empty, varsMap = mutable.Map.empty) + + def valValue(x: Symbol)(using data: Data, ctx: Context): Value = data.getVal(x).get + + def varAddr(x: Symbol)(using data: Data, ctx: Context): Heap.Addr = data.getVar(x).get + + def getVal(x: Symbol)(using data: Data, ctx: Context): Option[Value] = data.getVal(x) + + def getVar(x: Symbol)(using data: Data, ctx: Context): Option[Heap.Addr] = data.getVar(x) + + def of(ddef: DefDef, args: List[Value], outer: Data)(using Context): Data = + val params = ddef.termParamss.flatten.map(_.symbol) + assert(args.size == params.size, "arguments = " + args.size + ", params = " + params.size) + assert(ddef.symbol.owner.isClass ^ (outer != NoEnv), "ddef.owner = " + ddef.symbol.owner.show + ", outer = " + outer + ", " + ddef.source) + new LocalEnv(params.zip(args).toMap, ddef.symbol, outer)(valsMap = mutable.Map.empty, varsMap = mutable.Map.empty) + + def setLocalVal(x: Symbol, value: Value)(using data: Data, ctx: Context): Unit = + assert(!x.isOneOf(Flags.Param | Flags.Mutable), "Only local immutable variable allowed") + data match + case localEnv: LocalEnv => + assert(!localEnv.vals.contains(x), "Already initialized local " + x.show) + localEnv.vals(x) = value + case _ => + throw new RuntimeException("Incorrect local environment for initializing " + x.show) + + def setLocalVar(x: Symbol, addr: Heap.Addr)(using data: Data, ctx: Context): Unit = + assert(x.is(Flags.Mutable, butNot = Flags.Param), "Only local mutable variable allowed") + data match + case localEnv: LocalEnv => + assert(!localEnv.vars.contains(x), "Already initialized local " + x.show) + localEnv.vars(x) = addr + case _ => + throw new RuntimeException("Incorrect local environment for initializing " + x.show) + + /** + * Resolve the environment owned by the given method. + * + * The method could be located in outer scope with intermixed classes between its definition + * site and usage site. + * + * Due to widening, the corresponding environment might not exist. As a result reading the local + * variable will return `Cold` and it's forbidden to write to the local variable. + * + * @param meth The method which owns the environment + * @param thisV The value for `this` of the enclosing class where the local variable is referenced. + * @param env The local environment where the local variable is referenced. + * + * @return the environment and value for `this` owned by the given method. + */ + def resolveEnv(meth: Symbol, thisV: Value, env: Data)(using Context): Option[(Value, Data)] = log("Resolving env for " + meth.show + ", this = " + thisV.show + ", env = " + env.show, printer) { + env match + case localEnv: LocalEnv => + if localEnv.meth == meth then Some(thisV -> env) + else resolveEnv(meth, thisV, localEnv.outer) + case NoEnv => + // TODO: handle RefSet + thisV match + case ref: OfClass => + resolveEnv(meth, ref.outer, ref.env) + case _ => + None + } + + def withEnv[T](env: Data)(fn: Data ?=> T): T = fn(using env) + end Env + + /** Abstract heap for mutable fields + */ + object Heap: + abstract class Addr: + /** The static object which owns the mutable slot */ + def owner: ClassSymbol + + /** The address for mutable fields of objects. */ + private case class FieldAddr(regions: Regions.Data, field: Symbol, owner: ClassSymbol) extends Addr + + /** The address for mutable local variables . */ + private case class LocalVarAddr(regions: Regions.Data, sym: Symbol, owner: ClassSymbol) extends Addr + + /** Immutable heap data used in the cache. + * + * We need to use structural equivalence so that in different iterations the cache can be effective. + * + * TODO: speed up equality check for heap. + */ + opaque type Data = Map[Addr, Value] + + /** Store the heap as a mutable field to avoid threading it through the program. */ + class MutableData(private[Heap] var heap: Data): + private[Heap] def update(addr: Addr, value: Value): Unit = + heap.get(addr) match + case None => + heap = heap.updated(addr, value) + + case Some(current) => + val value2 = value.join(current) + if value2 != current then + heap = heap.updated(addr, value2) + + + def empty(): MutableData = new MutableData(Map.empty) + + def contains(addr: Addr)(using mutable: MutableData): Boolean = + mutable.heap.contains(addr) + + def read(addr: Addr)(using mutable: MutableData): Value = + mutable.heap(addr) + + def write(addr: Addr, value: Value)(using mutable: MutableData): Unit = + mutable.update(addr, value) + + def localVarAddr(regions: Regions.Data, sym: Symbol, owner: ClassSymbol): Addr = + LocalVarAddr(regions, sym, owner) + + def fieldVarAddr(regions: Regions.Data, sym: Symbol, owner: ClassSymbol): Addr = + FieldAddr(regions, sym, owner) + + def arrayAddr(regions: Regions.Data, owner: ClassSymbol)(using Context): Addr = + FieldAddr(regions, defn.ArrayClass, owner) + + def getHeapData()(using mutable: MutableData): Data = mutable.heap + + /** Cache used to terminate the check */ + object Cache: + case class Config(thisV: Value, env: Env.Data, heap: Heap.Data) + case class Res(value: Value, heap: Heap.Data) + + class Data extends Cache[Config, Res]: + def get(thisV: Value, expr: Tree)(using Heap.MutableData, Env.Data): Option[Value] = + val config = Config(thisV, summon[Env.Data], Heap.getHeapData()) + super.get(config, expr).map(_.value) + + def cachedEval(thisV: Value, expr: Tree, cacheResult: Boolean)(fun: Tree => Value)(using Heap.MutableData, Env.Data): Value = + val config = Config(thisV, summon[Env.Data], Heap.getHeapData()) + val result = super.cachedEval(config, expr, cacheResult, default = Res(Bottom, Heap.getHeapData())) { expr => + Res(fun(expr), Heap.getHeapData()) + } + result.value + end Cache + + /** + * Region context for mutable states + * + * By default, the region context is empty. + */ + object Regions: + opaque type Data = List[SourcePosition] + val empty: Data = Nil + def extend(pos: SourcePosition)(using data: Data): Data = pos :: data + def exists(pos: SourcePosition)(using data: Data): Boolean = data.indexOf(pos) >= 0 + def show(using data: Data, ctx: Context): String = data.map(_.show).mkString("[", ", ", "]") + + inline def cache(using c: Cache.Data): Cache.Data = c + + + /** + * Handle return statements in methods and non-local returns in functions. + */ + object Returns: + private class ReturnData(val method: Symbol, val values: mutable.ArrayBuffer[Value]) + opaque type Data = mutable.ArrayBuffer[ReturnData] + + def empty(): Data = mutable.ArrayBuffer() + + def installHandler(meth: Symbol)(using data: Data): Unit = + data.addOne(ReturnData(meth, mutable.ArrayBuffer())) + + def popHandler(meth: Symbol)(using data: Data): Value = + val returnData = data.remove(data.size - 1) + assert(returnData.method == meth, "Symbol mismatch in return handlers, expect = " + meth + ", found = " + returnData.method) + returnData.values.join + + def handle(meth: Symbol, value: Value)(using data: Data, trace: Trace, ctx: Context): Unit = + data.findLast(_.method == meth) match + case Some(returnData) => + returnData.values.addOne(value) + + case None => + report.warning("[Internal error] Unhandled return for method " + meth + " in " + meth.owner.show + ". Trace:\n" + Trace.show, Trace.position) + + type Contextual[T] = (Context, State.Data, Env.Data, Cache.Data, Heap.MutableData, Regions.Data, Returns.Data, Trace) ?=> T + + // --------------------------- domain operations ----------------------------- + + type ArgInfo = TraceValue[Value] + + extension (a: Value) + def join(b: Value): Value = + (a, b) match + case (Cold, b) => Cold + case (a, Cold) => Cold + case (Bottom, b) => b + case (a, Bottom) => a + case (RefSet(refs1), RefSet(refs2)) => RefSet(refs1 ++ refs2) + case (a, RefSet(refs)) => RefSet(refs + a) + case (RefSet(refs), b) => RefSet(refs + b) + case (a, b) => RefSet(ListSet(a, b)) + + def widen(height: Int)(using Context): Value = + if height == 0 then Cold + else + a match + case Bottom => Bottom + + case RefSet(refs) => + refs.map(ref => ref.widen(height)).join + + case Fun(code, thisV, klass, env) => + Fun(code, thisV.widen(height), klass, env.widen(height)) + + case ref @ OfClass(klass, outer, _, args, env) => + val outer2 = outer.widen(height - 1) + val args2 = args.map(_.widen(height - 1)) + val env2 = env.widen(height - 1) + ref.widenedCopy(outer2, args2, env2) + + case _ => a + + extension (values: Iterable[Value]) + def join: Value = if values.isEmpty then Bottom else values.reduce { (v1, v2) => v1.join(v2) } + + def widen(height: Int): Contextual[List[Value]] = values.map(_.widen(height)).toList + + /** Handle method calls `e.m(args)`. + * + * @param value The value for the receiver. + * @param meth The symbol of the target method (could be virtual or abstract method). + * @param args Arguments of the method call (all parameter blocks flatten to a list). + * @param receiver The type of the receiver. + * @param superType The type of the super in a super call. NoType for non-super calls. + * @param needResolve Whether the target of the call needs resolution? + */ + def call(value: Value, meth: Symbol, args: List[ArgInfo], receiver: Type, superType: Type, needResolve: Boolean = true): Contextual[Value] = log("call " + meth.show + ", args = " + args.map(_.value.show), printer, (_: Value).show) { + value match + case Cold => + report.warning("Using cold alias. Calling trace:\n" + Trace.show, Trace.position) + Bottom + + case Bottom => + Bottom + + case arr: OfArray => + val target = resolve(defn.ArrayClass, meth) + + if target == defn.Array_apply || target == defn.Array_clone then + if arr.addr.owner == State.currentObject then + Heap.read(arr.addr) + else + errorReadOtherStaticObject(State.currentObject, arr.addr.owner) + Bottom + else if target == defn.Array_update then + assert(args.size == 2, "Incorrect number of arguments for Array update, found = " + args.size) + if arr.addr.owner != State.currentObject then + errorMutateOtherStaticObject(State.currentObject, arr.addr.owner) + else + Heap.write(arr.addr, args.tail.head.value) + Bottom + else + // Array.length is OK + Bottom + + case ref: Ref => + val isLocal = !meth.owner.isClass + val target = + if !needResolve then + meth + else if superType.exists then + meth + else if meth.name.is(SuperAccessorName) then + ResolveSuper.rebindSuper(ref.klass, meth) + else + resolve(ref.klass, meth) + + if target.isOneOf(Flags.Method) then + if target.hasSource then + val cls = target.owner.enclosingClass.asClass + val ddef = target.defTree.asInstanceOf[DefDef] + val meth = ddef.symbol + + val (thisV, outerEnv) = + if meth.owner.isClass then + (ref, Env.NoEnv) + else + Env.resolveEnv(meth.owner.enclosingMethod, ref, summon[Env.Data]).getOrElse(Cold -> Env.NoEnv) + + val env2 = Env.of(ddef, args.map(_.value), outerEnv) + extendTrace(ddef) { + given Env.Data = env2 + // eval(ddef.rhs, ref, cls, cacheResult = true) + cache.cachedEval(ref, ddef.rhs, cacheResult = true) { expr => + Returns.installHandler(meth) + val res = cases(expr, thisV, cls) + val returns = Returns.popHandler(meth) + res.join(returns) + } + } + else + Bottom + else if target.exists then + select(ref, target, receiver, needResolve = false) + else + if ref.klass.isSubClass(receiver.widenSingleton.classSymbol) then + report.warning("[Internal error] Unexpected resolution failure: ref.klass = " + ref.klass.show + ", meth = " + meth.show + Trace.show, Trace.position) + Bottom + else + // This is possible due to incorrect type cast. + // See tests/init/pos/Type.scala + Bottom + + case Fun(code, thisV, klass, env) => + // meth == NoSymbol for poly functions + if meth.name.toString == "tupled" then + value // a call like `fun.tupled` + else + code match + case ddef: DefDef => + given Env.Data = Env.of(ddef, args.map(_.value), env) + extendTrace(code) { eval(ddef.rhs, thisV, klass, cacheResult = true) } + + case _ => + // by-name closure + given Env.Data = env + extendTrace(code) { eval(code, thisV, klass, cacheResult = true) } + + case RefSet(vs) => + vs.map(v => call(v, meth, args, receiver, superType)).join + } + + /** Handle constructor calls `(args)`. + * + * @param thisV The value for the receiver. + * @param ctor The symbol of the target method. + * @param args Arguments of the constructor call (all parameter blocks flatten to a list). + */ + def callConstructor(thisV: Value, ctor: Symbol, args: List[ArgInfo]): Contextual[Value] = log("call " + ctor.show + ", args = " + args.map(_.value.show), printer, (_: Value).show) { + + thisV match + case ref: Ref => + if ctor.hasSource then + val cls = ctor.owner.enclosingClass.asClass + val ddef = ctor.defTree.asInstanceOf[DefDef] + val argValues = args.map(_.value) + + given Env.Data = Env.of(ddef, argValues, Env.NoEnv) + if ctor.isPrimaryConstructor then + val tpl = cls.defTree.asInstanceOf[TypeDef].rhs.asInstanceOf[Template] + extendTrace(cls.defTree) { eval(tpl, ref, cls, cacheResult = true) } + else + extendTrace(ddef) { eval(ddef.rhs, ref, cls, cacheResult = true) } + else + // no source code available + Bottom + + case _ => + report.warning("[Internal error] unexpected constructor call, meth = " + ctor + ", this = " + thisV + Trace.show, Trace.position) + Bottom + } + + /** Handle selection `e.f`. + * + * @param value The value for the receiver. + * @param field The symbol of the target field (could be virtual or abstract). + * @param receiver The type of the receiver. + * @param needResolve Whether the target of the selection needs resolution? + */ + def select(thisV: Value, field: Symbol, receiver: Type, needResolve: Boolean = true): Contextual[Value] = log("select " + field.show + ", this = " + thisV.show, printer, (_: Value).show) { + thisV match + case Cold => + report.warning("Using cold alias", Trace.position) + Bottom + + case ref: Ref => + val target = if needResolve then resolve(ref.klass, field) else field + if target.is(Flags.Lazy) then + given Env.Data = Env.emptyEnv(target.owner.asInstanceOf[ClassSymbol].primaryConstructor) + val rhs = target.defTree.asInstanceOf[ValDef].rhs + eval(rhs, ref, target.owner.asClass, cacheResult = true) + else if target.exists then + if target.isOneOf(Flags.Mutable) then + if ref.hasVar(target) then + val addr = ref.varAddr(target) + if addr.owner == State.currentObject then + Heap.read(addr) + else + errorReadOtherStaticObject(State.currentObject, addr.owner) + Bottom + else if ref.isObjectRef && ref.klass.hasSource then + report.warning("Access uninitialized field " + field.show + ". Call trace: " + Trace.show, Trace.position) + Bottom + else + // initialization error, reported by the initialization checker + Bottom + else if ref.hasVal(target) then + ref.valValue(target) + else if ref.isObjectRef && ref.klass.hasSource then + report.warning("Access uninitialized field " + field.show + ". Call trace: " + Trace.show, Trace.position) + Bottom + else + // initialization error, reported by the initialization checker + Bottom + + else + if ref.klass.isSubClass(receiver.widenSingleton.classSymbol) then + report.warning("[Internal error] Unexpected resolution failure: ref.klass = " + ref.klass.show + ", field = " + field.show + Trace.show, Trace.position) + Bottom + else + // This is possible due to incorrect type cast. + // See tests/init/pos/Type.scala + Bottom + + case fun: Fun => + report.warning("[Internal error] unexpected tree in selecting a function, fun = " + fun.code.show + Trace.show, fun.code) + Bottom + + case Bottom => + if field.isStaticObject then ObjectRef(field.moduleClass.asClass) + else Bottom + + case RefSet(refs) => + refs.map(ref => select(ref, field, receiver)).join + } + + /** Handle assignment `lhs.f = rhs`. + * + * @param lhs The value of the object to be mutated. + * @param field The symbol of the target field. + * @param rhs The value to be assigned. + * @param rhsTyp The type of the right-hand side. + */ + def assign(lhs: Value, field: Symbol, rhs: Value, rhsTyp: Type): Contextual[Value] = log("Assign" + field.show + " of " + lhs.show + ", rhs = " + rhs.show, printer, (_: Value).show) { + lhs match + case fun: Fun => + report.warning("[Internal error] unexpected tree in assignment, fun = " + fun.code.show + Trace.show, Trace.position) + + case Cold => + report.warning("Assigning to cold aliases is forbidden. Calling trace:\n" + Trace.show, Trace.position) + + case Bottom => + + case RefSet(refs) => + refs.foreach(ref => assign(ref, field, rhs, rhsTyp)) + + case ref: Ref => + if ref.hasVar(field) then + val addr = ref.varAddr(field) + if addr.owner != State.currentObject then + errorMutateOtherStaticObject(State.currentObject, addr.owner) + else + Heap.write(addr, rhs) + else + report.warning("Mutating a field before its initialization: " + field.show + ". Calling trace:\n" + Trace.show, Trace.position) + end match + + Bottom + } + + /** Handle new expression `new p.C(args)`. + * + * @param outer The value for `p`. + * @param klass The symbol of the class `C`. + * @param ctor The symbol of the target constructor. + * @param args The arguments passsed to the constructor. + */ + def instantiate(outer: Value, klass: ClassSymbol, ctor: Symbol, args: List[ArgInfo]): Contextual[Value] = log("instantiating " + klass.show + ", outer = " + outer + ", args = " + args.map(_.value.show), printer, (_: Value).show) { + outer match + + case _ : Fun | _: OfArray => + report.warning("[Internal error] unexpected outer in instantiating a class, outer = " + outer.show + ", class = " + klass.show + ", " + Trace.show, Trace.position) + Bottom + + case value: (Bottom.type | ObjectRef | OfClass | Cold.type) => + // The outer can be a bottom value for top-level classes. + + if klass == defn.ArrayClass then + val arr = OfArray(State.currentObject, summon[Regions.Data]) + Heap.write(arr.addr, Bottom) + arr + else + // Widen the outer to finitize the domain. Arguments already widened in `evalArgs`. + val (outerWidened, envWidened) = + if klass.owner.isClass then + (outer.widen(1), Env.NoEnv) + else + // klass.enclosingMethod returns its primary constructor + Env.resolveEnv(klass.owner.enclosingMethod, outer, summon[Env.Data]).getOrElse(Cold -> Env.NoEnv) + + val instance = OfClass(klass, outerWidened, ctor, args.map(_.value), envWidened) + callConstructor(instance, ctor, args) + instance + + case RefSet(refs) => + refs.map(ref => instantiate(ref, klass, ctor, args)).join + } + + /** Handle local variable definition, `val x = e` or `var x = e`. + * + * @param ref The value for `this` where the variable is defined. + * @param sym The symbol of the variable. + * @param value The value of the initializer. + */ + def initLocal(ref: Ref, sym: Symbol, value: Value): Contextual[Unit] = log("initialize local " + sym.show + " with " + value.show, printer) { + if sym.is(Flags.Mutable) then + val addr = Heap.localVarAddr(summon[Regions.Data], sym, State.currentObject) + Env.setLocalVar(sym, addr) + Heap.write(addr, value) + else + Env.setLocalVal(sym, value) + } + + /** Read local variable `x`. + * + * @param thisV The value for `this` where the variable is used. + * @param sym The symbol of the variable. + */ + def readLocal(thisV: Value, sym: Symbol): Contextual[Value] = log("reading local " + sym.show, printer, (_: Value).show) { + def isByNameParam(sym: Symbol) = sym.is(Flags.Param) && sym.info.isInstanceOf[ExprType] + Env.resolveEnv(sym.enclosingMethod, thisV, summon[Env.Data]) match + case Some(thisV -> env) => + if sym.is(Flags.Mutable) then + // Assume forward reference check is doing a good job + given Env.Data = env + val addr = Env.varAddr(sym) + if addr.owner == State.currentObject then + Heap.read(addr) + else + errorReadOtherStaticObject(State.currentObject, addr.owner) + Bottom + end if + else if sym.isPatternBound then + // TODO: handle patterns + Cold + else + given Env.Data = env + try + // Assume forward reference check is doing a good job + val value = Env.valValue(sym) + if isByNameParam(sym) then + value match + case fun: Fun => + given Env.Data = fun.env + eval(fun.code, fun.thisV, fun.klass) + case Cold => + report.warning("Calling cold by-name alias. Call trace: \n" + Trace.show, Trace.position) + Bottom + case _: RefSet | _: Ref => + report.warning("[Internal error] Unexpected by-name value " + value.show + ". Calling trace:\n" + Trace.show, Trace.position) + Bottom + else + value + + catch ex => + report.warning("[Internal error] Not found " + sym.show + "\nenv = " + env.show + ". Calling trace:\n" + Trace.show, Trace.position) + Bottom + + case _ => + if isByNameParam(sym) then + report.warning("Calling cold by-name alias. Call trace: \n" + Trace.show, Trace.position) + Bottom + else + Cold + } + + /** Handle local variable assignmenbt, `x = e`. + * + * @param thisV The value for `this` where the assignment locates. + * @param sym The symbol of the variable. + * @param value The value of the rhs of the assignment. + */ + def writeLocal(thisV: Value, sym: Symbol, value: Value): Contextual[Value] = log("write local " + sym.show + " with " + value.show, printer, (_: Value).show) { + + assert(sym.is(Flags.Mutable), "Writing to immutable variable " + sym.show) + Env.resolveEnv(sym.enclosingMethod, thisV, summon[Env.Data]) match + case Some(thisV -> env) => + given Env.Data = env + val addr = Env.varAddr(sym) + if addr.owner != State.currentObject then + errorMutateOtherStaticObject(State.currentObject, addr.owner) + else + Heap.write(addr, value) + + case _ => + report.warning("Assigning to variables in outer scope. Calling trace:\n" + Trace.show, Trace.position) + + Bottom + } + + // -------------------------------- algorithm -------------------------------- + + /** Check an individual object */ + private def accessObject(classSym: ClassSymbol)(using Context, State.Data, Trace): Value = log("accessing " + classSym.show, printer, (_: Value).show) { + if classSym.hasSource then + State.checkObjectAccess(classSym) + else + ObjectRef(classSym) + } + + + def checkClasses(classes: List[ClassSymbol])(using Context): Unit = + given State.Data = new State.Data + given Trace = Trace.empty + + for + classSym <- classes if classSym.isStaticObject + do + accessObject(classSym) + + /** Evaluate an expression with the given value for `this` in a given class `klass` + * + * Note that `klass` might be a super class of the object referred by `thisV`. + * The parameter `klass` is needed for `this` resolution. Consider the following code: + * + * class A { + * A.this + * class B extends A { A.this } + * } + * + * As can be seen above, the meaning of the expression `A.this` depends on where + * it is located. + * + * This method only handles cache logic and delegates the work to `cases`. + * + * @param expr The expression to be evaluated. + * @param thisV The value for `C.this` where `C` is represented by the parameter `klass`. + * @param klass The enclosing class where the expression is located. + * @param cacheResult It is used to reduce the size of the cache. + */ + def eval(expr: Tree, thisV: Value, klass: ClassSymbol, cacheResult: Boolean = false): Contextual[Value] = log("evaluating " + expr.show + ", this = " + thisV.show + ", regions = " + Regions.show + " in " + klass.show, printer, (_: Value).show) { + cache.cachedEval(thisV, expr, cacheResult) { expr => cases(expr, thisV, klass) } + } + + + /** Evaluate a list of expressions */ + def evalExprs(exprs: List[Tree], thisV: Value, klass: ClassSymbol): Contextual[List[Value]] = + exprs.map { expr => eval(expr, thisV, klass) } + + /** Handles the evaluation of different expressions + * + * Note: Recursive call should go to `eval` instead of `cases`. + * + * @param expr The expression to be evaluated. + * @param thisV The value for `C.this` where `C` is represented by the parameter `klass`. + * @param klass The enclosing class where the expression `expr` is located. + */ + def cases(expr: Tree, thisV: Value, klass: ClassSymbol): Contextual[Value] = log("evaluating " + expr.show + ", this = " + thisV.show + " in " + klass.show, printer, (_: Value).show) { + val trace2 = trace.add(expr) + + expr match + case Ident(nme.WILDCARD) => + // TODO: disallow `var x: T = _` + Bottom + + case id @ Ident(name) if !id.symbol.is(Flags.Method) => + assert(name.isTermName, "type trees should not reach here") + withTrace(trace2) { evalType(expr.tpe, thisV, klass) } + + case NewExpr(tref, New(tpt), ctor, argss) => + // check args + val args = evalArgs(argss.flatten, thisV, klass) + + val cls = tref.classSymbol.asClass + withTrace(trace2) { + val outer = outerValue(tref, thisV, klass) + instantiate(outer, cls, ctor, args) + } + + case Apply(ref, arg :: Nil) if ref.symbol == defn.InitRegionMethod => + val regions2 = Regions.extend(expr.sourcePos) + if Regions.exists(expr.sourcePos) then + report.warning("Cyclic region detected. Trace:\n" + Trace.show, expr) + Bottom + else + given Regions.Data = regions2 + eval(arg, thisV, klass) + + case Call(ref, argss) => + // check args + val args = evalArgs(argss.flatten, thisV, klass) + + ref match + case Select(supert: Super, _) => + val SuperType(thisTp, superTp) = supert.tpe: @unchecked + val thisValue2 = extendTrace(ref) { resolveThis(thisTp.classSymbol.asClass, thisV, klass) } + withTrace(trace2) { call(thisValue2, ref.symbol, args, thisTp, superTp) } + + case Select(qual, _) => + val receiver = eval(qual, thisV, klass) + if ref.symbol.isConstructor then + withTrace(trace2) { callConstructor(receiver, ref.symbol, args) } + else + withTrace(trace2) { call(receiver, ref.symbol, args, receiver = qual.tpe, superType = NoType) } + + case id: Ident => + id.tpe match + case TermRef(NoPrefix, _) => + // resolve this for the local method + val enclosingClass = id.symbol.owner.enclosingClass.asClass + val thisValue2 = extendTrace(ref) { resolveThis(enclosingClass, thisV, klass) } + // local methods are not a member, but we can reuse the method `call` + withTrace(trace2) { call(thisValue2, id.symbol, args, receiver = NoType, superType = NoType, needResolve = false) } + case TermRef(prefix, _) => + val receiver = withTrace(trace2) { evalType(prefix, thisV, klass) } + if id.symbol.isConstructor then + withTrace(trace2) { callConstructor(receiver, id.symbol, args) } + else + withTrace(trace2) { call(receiver, id.symbol, args, receiver = prefix, superType = NoType) } + + case Select(qualifier, name) => + val qual = eval(qualifier, thisV, klass) + + name match + case OuterSelectName(_, _) => + val current = qualifier.tpe.classSymbol + val target = expr.tpe.widenSingleton.classSymbol.asClass + withTrace(trace2) { resolveThis(target, qual, current.asClass) } + case _ => + withTrace(trace2) { select(qual, expr.symbol, receiver = qualifier.tpe) } + + case _: This => + evalType(expr.tpe, thisV, klass) + + case Literal(_) => + Bottom + + case Typed(expr, tpt) => + if tpt.tpe.hasAnnotation(defn.UncheckedAnnot) then + Bottom + else + eval(expr, thisV, klass) + + case NamedArg(name, arg) => + eval(arg, thisV, klass) + + case Assign(lhs, rhs) => + var isLocal = false + val receiver = + lhs match + case Select(qual, _) => + eval(qual, thisV, klass) + case id: Ident => + id.tpe match + case TermRef(NoPrefix, _) => + isLocal = true + thisV + case TermRef(prefix, _) => + extendTrace(id) { evalType(prefix, thisV, klass) } + + val value = eval(rhs, thisV, klass) + + if isLocal then + writeLocal(thisV, lhs.symbol, value) + else + withTrace(trace2) { assign(receiver, lhs.symbol, value, rhs.tpe) } + + case closureDef(ddef) => + Fun(ddef, thisV, klass, summon[Env.Data]) + + case PolyFun(ddef) => + Fun(ddef, thisV, klass, summon[Env.Data]) + + case Block(stats, expr) => + evalExprs(stats, thisV, klass) + eval(expr, thisV, klass) + + case If(cond, thenp, elsep) => + eval(cond, thisV, klass) + evalExprs(thenp :: elsep :: Nil, thisV, klass).join + + case Annotated(arg, annot) => + if expr.tpe.hasAnnotation(defn.UncheckedAnnot) then + Bottom + else + eval(arg, thisV, klass) + + case Match(selector, cases) => + eval(selector, thisV, klass) + // TODO: handle pattern match properly + report.warning("[initChecker] Pattern match is skipped. Trace:\n" + Trace.show, expr) + Bottom + + case Return(expr, from) => + Returns.handle(from.symbol, eval(expr, thisV, klass)) + Bottom + + case WhileDo(cond, body) => + evalExprs(cond :: body :: Nil, thisV, klass) + Bottom + + case Labeled(_, expr) => + eval(expr, thisV, klass) + + case Try(block, cases, finalizer) => + val res = evalExprs(block :: cases.map(_.body), thisV, klass).join + if !finalizer.isEmpty then + eval(finalizer, thisV, klass) + res + + case SeqLiteral(elems, elemtpt) => + evalExprs(elems, thisV, klass).join + + case Inlined(call, bindings, expansion) => + evalExprs(bindings, thisV, klass) + eval(expansion, thisV, klass) + + case Thicket(List()) => + // possible in try/catch/finally, see tests/crash/i6914.scala + Bottom + + case vdef : ValDef => + // local val definition + val rhs = eval(vdef.rhs, thisV, klass) + val sym = vdef.symbol + initLocal(thisV.asInstanceOf[Ref], vdef.symbol, rhs) + Bottom + + case ddef : DefDef => + // local method + Bottom + + case tdef: TypeDef => + // local type definition + Bottom + + case _: Import | _: Export => + Bottom + + case tpl: Template => + init(tpl, thisV.asInstanceOf[Ref], klass) + + case _ => + report.warning("[Internal error] unexpected tree: " + expr + "\n" + Trace.show, expr) + Bottom + } + + /** Handle semantics of leaf nodes + * + * For leaf nodes, their semantics is determined by their types. + * + * @param tp The type to be evaluated. + * @param thisV The value for `C.this` where `C` is represented by `klass`. + * @param klass The enclosing class where the type `tp` is located. + * @param elideObjectAccess Whether object access should be omitted. + * + * Object access elission happens when the object access is used as a prefix + * in `new o.C` and `C` does not need an outer. + */ + def evalType(tp: Type, thisV: Value, klass: ClassSymbol, elideObjectAccess: Boolean = false): Contextual[Value] = log("evaluating " + tp.show, printer, (_: Value).show) { + tp match + case _: ConstantType => + Bottom + + case tmref: TermRef if tmref.prefix == NoPrefix => + val sym = tmref.symbol + if sym.is(Flags.Package) then + Bottom + else if sym.owner.isClass then + // The typer incorrectly assigns a TermRef with NoPrefix for `config`, + // while the actual denotation points to the symbol of the class member + // instead of the parameter symbol for the primary constructor. + // + // abstract class Base(implicit config: Int) + // case class A(x: Int)(implicit config: Int) extends Base + evalType(sym.termRef, thisV, klass, elideObjectAccess) + else + readLocal(thisV, sym) + + case tmref: TermRef => + val sym = tmref.symbol + if sym.isStaticObject then + if elideObjectAccess then + ObjectRef(sym.moduleClass.asClass) + else + accessObject(sym.moduleClass.asClass) + else + val value = evalType(tmref.prefix, thisV, klass) + select(value, tmref.symbol, tmref.prefix) + + case tp @ ThisType(tref) => + val sym = tref.symbol + if sym.is(Flags.Package) then + Bottom + else if sym.isStaticObject && sym != klass then + // The typer may use ThisType to refer to an object outside its definition. + if elideObjectAccess then + ObjectRef(sym.moduleClass.asClass) + else + accessObject(sym.moduleClass.asClass) + + else + resolveThis(tref.classSymbol.asClass, thisV, klass) + + case _ => + throw new Exception("unexpected type: " + tp) + } + + /** Evaluate arguments of methods and constructors */ + def evalArgs(args: List[Arg], thisV: Value, klass: ClassSymbol): Contextual[List[ArgInfo]] = + val argInfos = new mutable.ArrayBuffer[ArgInfo] + args.foreach { arg => + val res = + if arg.isByName then + Fun(arg.tree, thisV, klass, summon[Env.Data]) + else + eval(arg.tree, thisV, klass) + + val widened = + arg.tree.tpe.getAnnotation(defn.InitWidenAnnot) match + case Some(annot) => + annot.argument(0).get match + case arg @ Literal(c: Constants.Constant) => + val height = c.intValue + if height < 0 then + report.warning("The argument should be positive", arg) + res.widen(1) + else + res.widen(c.intValue) + case arg => + report.warning("The argument should be a constant integer value", arg) + res.widen(1) + case _ => + res.widen(1) + + argInfos += TraceValue(widened, trace.add(arg.tree)) + } + argInfos.toList + + /** Initialize part of an abstract object in `klass` of the inheritance chain + * + * @param tpl The class body to be evaluated. + * @param thisV The value of the current object to be initialized. + * @param klass The class to which the template belongs. + */ + def init(tpl: Template, thisV: Ref, klass: ClassSymbol): Contextual[Value] = log("init " + klass.show, printer, (_: Value).show) { + val paramsMap = tpl.constr.termParamss.flatten.map { vdef => + vdef.name -> Env.valValue(vdef.symbol) + }.toMap + + // init param fields + klass.paramGetters.foreach { acc => + val value = paramsMap(acc.name.toTermName) + if acc.is(Flags.Mutable) then + val addr = Heap.fieldVarAddr(summon[Regions.Data], acc, State.currentObject) + thisV.initVar(acc, addr) + Heap.write(addr, value) + else + thisV.initVal(acc, value) + printer.println(acc.show + " initialized with " + value) + } + + // Tasks is used to schedule super constructor calls. + // Super constructor calls are delayed until all outers are set. + type Tasks = mutable.ArrayBuffer[() => Unit] + def superCall(tref: TypeRef, ctor: Symbol, args: List[ArgInfo], tasks: Tasks): Unit = + val cls = tref.classSymbol.asClass + // update outer for super class + val res = outerValue(tref, thisV, klass) + thisV.initOuter(cls, res) + + // follow constructor + if cls.hasSource then + tasks.append { () => + printer.println("init super class " + cls.show) + callConstructor(thisV, ctor, args) + () + } + + // parents + def initParent(parent: Tree, tasks: Tasks) = + parent match + case tree @ Block(stats, NewExpr(tref, New(tpt), ctor, argss)) => // can happen + evalExprs(stats, thisV, klass) + val args = evalArgs(argss.flatten, thisV, klass) + superCall(tref, ctor, args, tasks) + + case tree @ NewExpr(tref, New(tpt), ctor, argss) => // extends A(args) + val args = evalArgs(argss.flatten, thisV, klass) + superCall(tref, ctor, args, tasks) + + case _ => // extends A or extends A[T] + val tref = typeRefOf(parent.tpe) + superCall(tref, tref.classSymbol.primaryConstructor, Nil, tasks) + + // see spec 5.1 about "Template Evaluation". + // https://www.scala-lang.org/files/archive/spec/2.13/05-classes-and-objects.html + if !klass.is(Flags.Trait) then + // outers are set first + val tasks = new mutable.ArrayBuffer[() => Unit] + + // 1. first init parent class recursively + // 2. initialize traits according to linearization order + val superParent = tpl.parents.head + val superCls = superParent.tpe.classSymbol.asClass + extendTrace(superParent) { initParent(superParent, tasks) } + + val parents = tpl.parents.tail + val mixins = klass.baseClasses.tail.takeWhile(_ != superCls) + + // The interesting case is the outers for traits. The compiler + // synthesizes proxy accessors for the outers in the class that extends + // the trait. As those outers must be stable values, they are initialized + // immediately following class parameters and before super constructor + // calls and user code in the class body. + mixins.reverse.foreach { mixin => + parents.find(_.tpe.classSymbol == mixin) match + case Some(parent) => + extendTrace(parent) { initParent(parent, tasks) } + case None => + // According to the language spec, if the mixin trait requires + // arguments, then the class must provide arguments to it explicitly + // in the parent list. That means we will encounter it in the Some + // branch. + // + // When a trait A extends a parameterized trait B, it cannot provide + // term arguments to B. That can only be done in a concrete class. + val tref = typeRefOf(klass.typeRef.baseType(mixin).typeConstructor) + val ctor = tref.classSymbol.primaryConstructor + if ctor.exists then + // The parameter check of traits comes late in the mixin phase. + // To avoid crash we supply hot values for erroneous parent calls. + // See tests/neg/i16438.scala. + val args: List[ArgInfo] = ctor.info.paramInfoss.flatten.map(_ => new ArgInfo(Bottom, Trace.empty)) + extendTrace(superParent) { + superCall(tref, ctor, args, tasks) + } + } + + // initialize super classes after outers are set + tasks.foreach(task => task()) + end if + + // class body + tpl.body.foreach { + case vdef : ValDef if !vdef.symbol.is(Flags.Lazy) && !vdef.rhs.isEmpty => + val res = eval(vdef.rhs, thisV, klass) + val sym = vdef.symbol + if sym.is(Flags.Mutable) then + val addr = Heap.fieldVarAddr(summon[Regions.Data], sym, State.currentObject) + thisV.initVar(sym, addr) + Heap.write(addr, res) + else + thisV.initVal(sym, res) + + case _: MemberDef => + + case tree => + eval(tree, thisV, klass) + } + + thisV + } + + + /** Resolve C.this that appear in `klass` + * + * @param target The class symbol for `C` for which `C.this` is to be resolved. + * @param thisV The value for `D.this` where `D` is represented by the parameter `klass`. + * @param klass The enclosing class where the type `C.this` is located. + * @param elideObjectAccess Whether object access should be omitted. + * + * Object access elision happens when the object access is used as a prefix + * in `new o.C` and `C` does not need an outer. + */ + def resolveThis(target: ClassSymbol, thisV: Value, klass: ClassSymbol, elideObjectAccess: Boolean = false): Contextual[Value] = log("resolveThis target = " + target.show + ", this = " + thisV.show, printer, (_: Value).show) { + if target == klass then + thisV + else if target.is(Flags.Package) then + Bottom + else if target.isStaticObject then + val res = ObjectRef(target.moduleClass.asClass) + if elideObjectAccess then res + else accessObject(target) + else + thisV match + case Bottom => Bottom + case Cold => Cold + case ref: Ref => + val outerCls = klass.owner.lexicallyEnclosingClass.asClass + if !ref.hasOuter(klass) then + val error = "[Internal error] outer not yet initialized, target = " + target + ", klass = " + klass + Trace.show + report.warning(error, Trace.position) + Bottom + else + resolveThis(target, ref.outerValue(klass), outerCls) + case RefSet(refs) => + refs.map(ref => resolveThis(target, ref, klass)).join + case fun: Fun => + report.warning("[Internal error] unexpected thisV = " + thisV + ", target = " + target.show + ", klass = " + klass.show + Trace.show, Trace.position) + Bottom + } + + /** Compute the outer value that corresponds to `tref.prefix` + * + * @param tref The type whose prefix is to be evaluated. + * @param thisV The value for `C.this` where `C` is represented by the parameter `klass`. + * @param klass The enclosing class where the type `tref` is located. + */ + def outerValue(tref: TypeRef, thisV: Value, klass: ClassSymbol): Contextual[Value] = + val cls = tref.classSymbol.asClass + if tref.prefix == NoPrefix then + val enclosing = cls.owner.lexicallyEnclosingClass.asClass + resolveThis(enclosing, thisV, klass, elideObjectAccess = cls.isStatic) + else + if cls.isAllOf(Flags.JavaInterface) then Bottom + else evalType(tref.prefix, thisV, klass, elideObjectAccess = cls.isStatic) + + def errorMutateOtherStaticObject(currentObj: ClassSymbol, otherObj: ClassSymbol)(using Trace, Context) = + val msg = + s"Mutating ${otherObj.show} during initialization of ${currentObj.show}.\n" + + "Mutating other static objects during the initialization of one static object is forbidden. " + + "Calling trace:\n" + Trace.show + + report.warning(msg, Trace.position) + + def errorReadOtherStaticObject(currentObj: ClassSymbol, otherObj: ClassSymbol)(using Trace, Context) = + val msg = + "Reading mutable state of " + otherObj.show + " during initialization of " + currentObj.show + ".\n" + + "Reading mutable state of other static objects is forbidden as it breaks initialization-time irrelevance. " + + "Calling trace: " + Trace.show + + report.warning(msg, Trace.position) diff --git a/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala b/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala index 4548dccb598f..261577cb718f 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala @@ -589,7 +589,7 @@ object Semantic: Hot else if ref.klass.isSubClass(receiver.widenSingleton.classSymbol) then - report.error("[Internal error] Unexpected resolution failure: ref.klass = " + ref.klass.show + ", field = " + field.show + Trace.show, Trace.position) + report.warning("[Internal error] Unexpected resolution failure: ref.klass = " + ref.klass.show + ", field = " + field.show + Trace.show, Trace.position) Hot else // This is possible due to incorrect type cast. @@ -597,7 +597,7 @@ object Semantic: Hot case fun: Fun => - report.error("[Internal error] unexpected tree in selecting a function, fun = " + fun.expr.show + Trace.show, fun.expr) + report.warning("[Internal error] unexpected tree in selecting a function, fun = " + fun.expr.show + Trace.show, fun.expr) Hot case RefSet(refs) => @@ -725,7 +725,7 @@ object Semantic: value.select(target, receiver, needResolve = false) else if ref.klass.isSubClass(receiver.widenSingleton.classSymbol) then - report.error("[Internal error] Unexpected resolution failure: ref.klass = " + ref.klass.show + ", meth = " + meth.show + Trace.show, Trace.position) + report.warning("[Internal error] Unexpected resolution failure: ref.klass = " + ref.klass.show + ", meth = " + meth.show + Trace.show, Trace.position) Hot else // This is possible due to incorrect type cast. @@ -755,7 +755,7 @@ object Semantic: value match { case Hot | Cold | _: RefSet | _: Fun => - report.error("[Internal error] unexpected constructor call, meth = " + ctor + ", value = " + value + Trace.show, Trace.position) + report.warning("[Internal error] unexpected constructor call, meth = " + ctor + ", value = " + value + Trace.show, Trace.position) Hot case ref: Warm if ref.isPopulatingParams => @@ -862,7 +862,7 @@ object Semantic: warm case Fun(body, thisV, klass) => - report.error("[Internal error] unexpected tree in instantiating a function, fun = " + body.show + Trace.show, Trace.position) + report.warning("[Internal error] unexpected tree in instantiating a function, fun = " + body.show + Trace.show, Trace.position) Hot case RefSet(refs) => @@ -882,7 +882,7 @@ object Semantic: case Hot => Hot case ref: Ref => ref.objekt.field(sym) case _ => - report.error("[Internal error] unexpected this value accessing local variable, sym = " + sym.show + ", thisValue = " + thisValue2.show + Trace.show, Trace.position) + report.warning("[Internal error] unexpected this value accessing local variable, sym = " + sym.show + ", thisValue = " + thisValue2.show + Trace.show, Trace.position) Hot else if sym.is(Flags.Param) then Hot @@ -900,7 +900,7 @@ object Semantic: case ref: Ref => eval(vdef.rhs, ref, enclosingClass, cacheResult = sym.is(Flags.Lazy)) case _ => - report.error("[Internal error] unexpected this value when accessing local variable, sym = " + sym.show + ", thisValue = " + thisValue2.show + Trace.show, Trace.position) + report.warning("[Internal error] unexpected this value when accessing local variable, sym = " + sym.show + ", thisValue = " + thisValue2.show + Trace.show, Trace.position) Hot end match @@ -1040,7 +1040,7 @@ object Semantic: // // This invariant holds because of the Scala/Java/JVM restriction that we cannot use `this` in super constructor calls. if subClassSegmentHot && !isHotSegment then - report.error("[Internal error] Expect current segment to be transitively initialized (Hot) in promotion, current klass = " + klass.show + + report.warning("[Internal error] Expect current segment to be transitively initialized (Hot) in promotion, current klass = " + klass.show + ", subclass = " + subClass.show + Trace.show, Trace.position) // If the outer and parameters of a class are all hot, then accessing fields and methods of the current @@ -1300,8 +1300,8 @@ object Semantic: case closureDef(ddef) => Fun(ddef.rhs, thisV, klass) - case PolyFun(body) => - Fun(body, thisV, klass) + case PolyFun(ddef) => + Fun(ddef.rhs, thisV, klass) case Block(stats, expr) => eval(stats, thisV, klass) @@ -1374,7 +1374,7 @@ object Semantic: Hot case _ => - report.error("[Internal error] unexpected tree" + Trace.show, expr) + report.warning("[Internal error] unexpected tree" + Trace.show, expr) Hot /** Handle semantics of leaf nodes @@ -1418,7 +1418,7 @@ object Semantic: Hot case _ => - report.error("[Internal error] unexpected type " + tp + Trace.show, Trace.position) + report.warning("[Internal error] unexpected type " + tp + Trace.show, Trace.position) Hot } @@ -1439,14 +1439,14 @@ object Semantic: val outerCls = klass.owner.lexicallyEnclosingClass.asClass if !obj.hasOuter(klass) then val error = "[Internal error] outer not yet initialized, target = " + target + ", klass = " + klass + ", object = " + obj + Trace.show - report.error(error, Trace.position) + report.warning(error, Trace.position) Hot else resolveThis(target, obj.outer(klass), outerCls) case RefSet(refs) => refs.map(ref => resolveThis(target, ref, klass)).join case fun: Fun => - report.error("[Internal error] unexpected thisV = " + thisV + ", target = " + target.show + ", klass = " + klass.show + Trace.show, Trace.position) + report.warning("[Internal error] unexpected thisV = " + thisV + ", target = " + target.show + ", klass = " + klass.show + Trace.show, Trace.position) Cold case Cold => Cold diff --git a/compiler/src/dotty/tools/dotc/transform/init/Util.scala b/compiler/src/dotty/tools/dotc/transform/init/Util.scala index 4e60c1325b09..ad7d2afffbaf 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Util.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Util.scala @@ -64,14 +64,14 @@ object Util: case _ => None object PolyFun: - def unapply(tree: Tree)(using Context): Option[Tree] = + def unapply(tree: Tree)(using Context): Option[DefDef] = tree match case Block((cdef: TypeDef) :: Nil, Typed(NewExpr(tref, _, _, _), _)) if tref.symbol.isAnonymousClass && tref <:< defn.PolyFunctionType => val body = cdef.rhs.asInstanceOf[Template].body val apply = body.head.asInstanceOf[DefDef] - Some(apply.rhs) + Some(apply) case _ => None @@ -100,3 +100,8 @@ object Util: // A concrete class may not be instantiated if the self type is not satisfied instantiable && cls.enclosingPackageClass != defn.StdLibPatchesPackage.moduleClass + + /** Whether the class or its super class/trait contains any mutable fields? */ + def isMutable(cls: ClassSymbol)(using Context): Boolean = + cls.classInfo.decls.exists(_.is(Flags.Mutable)) || + cls.parentSyms.exists(parentCls => isMutable(parentCls.asClass)) diff --git a/compiler/test/dotty/tools/dotc/CompilationTests.scala b/compiler/test/dotty/tools/dotc/CompilationTests.scala index 4e86a3b83383..cab318b3201e 100644 --- a/compiler/test/dotty/tools/dotc/CompilationTests.scala +++ b/compiler/test/dotty/tools/dotc/CompilationTests.scala @@ -274,6 +274,14 @@ class CompilationTests { compileFilesInDir("tests/explicit-nulls/run", explicitNullsOptions) }.checkRuns() + // initialization tests + @Test def checkInitGlobal: Unit = { + implicit val testGroup: TestGroup = TestGroup("checkInitGlobal") + val options = defaultOptions.and("-Ysafe-init-global", "-Xfatal-warnings") + compileFilesInDir("tests/init-global/neg", options).checkExpectedErrors() + compileFilesInDir("tests/init-global/pos", options).checkCompile() + } + // initialization tests @Test def checkInit: Unit = { implicit val testGroup: TestGroup = TestGroup("checkInit") diff --git a/library/src/scala/annotation/init.scala b/library/src/scala/annotation/init.scala new file mode 100644 index 000000000000..7dea418664e6 --- /dev/null +++ b/library/src/scala/annotation/init.scala @@ -0,0 +1,58 @@ +package scala.annotation + +/** Annotations to control the behavior of the compiler check for safe initialization of static obects. + * + * Programmers usually do not need to use any annotations. They are intended for complex initialization + * code in static objects. + */ +@experimental +object init: + + /** Widen the abstract value of the argument so that its height is below the specified height. + * + * It can be used to mark method or constructor arguments, as the following example shows: + * + * class A(x: Int): + * def square(): Int = x*x + * + * class C(val a: A) + * + * object B: + * val a = build(new C(new A(10)): @widen(2)) // <-- usage + * + * def build(c: C) = new A(c.a.square()) // calling methods on parameter + * + * By default, method and constructor arguments are widened to height 1. In + * the code above, without using `@widen(2)` we will have the abstract value + * `C(a = Cold)` for the argument `c` of the method `build`. Consequently, + * the checker will issue a warning for the method call `c.a.square()` because + * it is forbidden to call methods or access fields on cold values. + */ + @experimental + final class widen(height: Int) extends StaticAnnotation + + /** Introduce a region context. + * + * The same mutable field in the same region have the same abstract representation. + * + * The concept of regions is intended to make context-sensitivity tunable for complex use cases. + * + * Example: + * + * trait B { def foo(): Int } + * class C(var x: Int) extends B { def foo(): Int = 20 } + * class D(var y: Int) extends B { def foo(): Int = A.m } + * class Box(var value: B) + * + * object A: + * val box1: Box = region { new Box(new C(5)) } + * val box2: Box = region { new Box(new D(10)) } + * val m: Int = box1.value.foo() + * + * In the above, without the two region annotations, the two objects `box1` and `box2` are in the same region. + * Therefore, the field `box1.value` and `box2.value` points to both instances of `C` and `D`. Consequently, + * the method call `box1.value.foo()` will be invalid, because it reaches `A.m`, which is not yet initialized. + * The explicit context annotation solves the problem. + */ + @experimental + def region[T](v: T): T = v diff --git a/tests/init-global/neg/context-sensitivity.scala b/tests/init-global/neg/context-sensitivity.scala new file mode 100755 index 000000000000..626fd41bb43f --- /dev/null +++ b/tests/init-global/neg/context-sensitivity.scala @@ -0,0 +1,17 @@ +trait Foo: + def foo(): Int + +class C(var x: Int) extends Foo { + def foo(): Int = 20 +} + +class D(var y: Int) extends Foo { + def foo(): Int = A.m // error +} + +class Box(var value: Foo) + +object A: + val box1: Box = new Box(new C(5)) + val box2: Box = new Box(new D(10)) + val m: Int = box1.value.foo() diff --git a/tests/init-global/neg/global-cycle1.check b/tests/init-global/neg/global-cycle1.check new file mode 100644 index 000000000000..e0125630c077 --- /dev/null +++ b/tests/init-global/neg/global-cycle1.check @@ -0,0 +1,22 @@ +-- Error: tests/init-global/neg/global-cycle1.scala:1:7 ---------------------------------------------------------------- +1 |object A { // error + |^ + |Cyclic initialization: object A -> object B -> object A. Calling trace: + |-> object A { // error [ global-cycle1.scala:1 ] + | ^ + |-> val a: Int = B.b [ global-cycle1.scala:2 ] + | ^ + |-> object B { [ global-cycle1.scala:5 ] + | ^ + |-> val b: Int = A.a // error [ global-cycle1.scala:6 ] + | ^ +2 | val a: Int = B.b +3 |} +-- Error: tests/init-global/neg/global-cycle1.scala:6:17 --------------------------------------------------------------- +6 | val b: Int = A.a // error + | ^^^ + | Access uninitialized field value a. Call trace: + | -> object B { [ global-cycle1.scala:5 ] + | ^ + | -> val b: Int = A.a // error [ global-cycle1.scala:6 ] + | ^^^ diff --git a/tests/init-global/neg/global-cycle1.scala b/tests/init-global/neg/global-cycle1.scala new file mode 100644 index 000000000000..592f5a652dc8 --- /dev/null +++ b/tests/init-global/neg/global-cycle1.scala @@ -0,0 +1,10 @@ +object A { // error + val a: Int = B.b +} + +object B { + val b: Int = A.a // error +} + +@main +def Test = print(A.a) diff --git a/tests/init-global/neg/global-cycle14.scala b/tests/init-global/neg/global-cycle14.scala new file mode 100644 index 000000000000..bcacbebb74fa --- /dev/null +++ b/tests/init-global/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 // error +} diff --git a/tests/init-global/neg/global-cycle2.scala b/tests/init-global/neg/global-cycle2.scala new file mode 100644 index 000000000000..d86fe09fb0fa --- /dev/null +++ b/tests/init-global/neg/global-cycle2.scala @@ -0,0 +1,7 @@ +object A { + val a: Int = B.foo() +} + +object B { + def foo(): Int = A.a * 2 // error +} diff --git a/tests/init-global/neg/global-cycle3.scala b/tests/init-global/neg/global-cycle3.scala new file mode 100644 index 000000000000..405e007b044a --- /dev/null +++ b/tests/init-global/neg/global-cycle3.scala @@ -0,0 +1,7 @@ +class A(x: Int) { + def foo(): Int = B.a + 10 // error +} + +object B { + val a: Int = A(4).foo() +} diff --git a/tests/init/full/neg/global-cycle4.scala b/tests/init-global/neg/global-cycle4.scala similarity index 70% rename from tests/init/full/neg/global-cycle4.scala rename to tests/init-global/neg/global-cycle4.scala index 3de0533cb521..8c1627afeae4 100644 --- a/tests/init/full/neg/global-cycle4.scala +++ b/tests/init-global/neg/global-cycle4.scala @@ -7,7 +7,7 @@ class B extends A { } class C extends A { - def foo(): Int = O.a + 10 + def foo(): Int = O.a + 10 // error } class D(x: Int) { @@ -15,5 +15,5 @@ class D(x: Int) { } object O { - val a: Int = D(5).bar().foo() // error + val a: Int = D(5).bar().foo() } diff --git a/tests/init-global/neg/global-cycle5.scala b/tests/init-global/neg/global-cycle5.scala new file mode 100755 index 000000000000..1ba3435d3830 --- /dev/null +++ b/tests/init-global/neg/global-cycle5.scala @@ -0,0 +1,23 @@ +class X { + def foo(): Int = 10 +} + +object A { + var a: X = new X() +} + +object B { + val b: Int = A.a.foo() // error +} + +class Y extends X { + override def foo() = C.c +} + +object C { + val c: Int = B.b +} + +def main = { + A.a = new Y(); C +} \ No newline at end of file diff --git a/tests/init-global/neg/global-cycle6.scala b/tests/init-global/neg/global-cycle6.scala new file mode 100644 index 000000000000..2d4f23c25187 --- /dev/null +++ b/tests/init-global/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-global/neg/global-cycle7.scala b/tests/init-global/neg/global-cycle7.scala new file mode 100644 index 000000000000..aea75726fbf7 --- /dev/null +++ b/tests/init-global/neg/global-cycle7.scala @@ -0,0 +1,18 @@ +object A { // error + val n: Int = B.m +} + +object B { + val m: Int = A.n // error +} + +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 +} diff --git a/tests/init-global/neg/global-cycle8.scala b/tests/init-global/neg/global-cycle8.scala new file mode 100644 index 000000000000..344dc3241395 --- /dev/null +++ b/tests/init-global/neg/global-cycle8.scala @@ -0,0 +1,20 @@ +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-global/neg/global-irrelevance1.scala b/tests/init-global/neg/global-irrelevance1.scala new file mode 100644 index 000000000000..903d3b14ae18 --- /dev/null +++ b/tests/init-global/neg/global-irrelevance1.scala @@ -0,0 +1,5 @@ +object A: + var x = 6 + +object B: + var y = A.x * 2 // error \ No newline at end of file diff --git a/tests/init-global/neg/global-irrelevance2.scala b/tests/init-global/neg/global-irrelevance2.scala new file mode 100644 index 000000000000..66b06677b689 --- /dev/null +++ b/tests/init-global/neg/global-irrelevance2.scala @@ -0,0 +1,8 @@ +object A: + var x = 6 + +class B(b: Int): + A.x = b * 2 // error + +object B: + new B(10) diff --git a/tests/init-global/neg/global-irrelevance3.scala b/tests/init-global/neg/global-irrelevance3.scala new file mode 100644 index 000000000000..2f36d65d917e --- /dev/null +++ b/tests/init-global/neg/global-irrelevance3.scala @@ -0,0 +1,14 @@ +object A: + class Pair(val f: Int => Unit, val g: () => Int) + val p: Pair = foo() + + def foo(): Pair = + var x = 6 + new Pair( + y => x = y, + (() => x) // error + ) + + +object B: + var y = A.p.g() diff --git a/tests/init-global/neg/global-irrelevance4.scala b/tests/init-global/neg/global-irrelevance4.scala new file mode 100644 index 000000000000..7a2a778814b2 --- /dev/null +++ b/tests/init-global/neg/global-irrelevance4.scala @@ -0,0 +1,13 @@ +object A: + class Pair(val f: Int => Unit, val g: () => Int) + val p: Pair = foo() + + def foo(): Pair = + var x = 6 + new Pair( + (y => x = y), // error + () => x + ) + +object B: + A.p.f(10) diff --git a/tests/init-global/neg/global-irrelevance5.scala b/tests/init-global/neg/global-irrelevance5.scala new file mode 100644 index 000000000000..fd5bde3032aa --- /dev/null +++ b/tests/init-global/neg/global-irrelevance5.scala @@ -0,0 +1,6 @@ +object A: + val array: Array[Int] = new Array(1) + array(0) = 10 + +object B: + var y = A.array(0) * 2 // error diff --git a/tests/init-global/neg/global-irrelevance6.scala b/tests/init-global/neg/global-irrelevance6.scala new file mode 100644 index 000000000000..78699b6988b6 --- /dev/null +++ b/tests/init-global/neg/global-irrelevance6.scala @@ -0,0 +1,9 @@ +class Box(x: Int): + def foo(): Int = 100 + +object A: + val array: Array[Box] = new Array(1) + val n = array(0).foo() // ok, no crash + +object B: + var y = A.array(0).foo() * 2 // error diff --git a/tests/init-global/neg/global-irrelevance7.scala b/tests/init-global/neg/global-irrelevance7.scala new file mode 100644 index 000000000000..2c860cbc4259 --- /dev/null +++ b/tests/init-global/neg/global-irrelevance7.scala @@ -0,0 +1,10 @@ +class Box(x: Int): + def foo(): Int = 100 + +object A: + val array: Array[Box] = new Array(1) + array(0) = new Box(10) + val n = array(0).foo() // ok + +object B: + var y = A.array(0).foo() * 2 // error diff --git a/tests/init-global/neg/global-list.scala b/tests/init-global/neg/global-list.scala new file mode 100755 index 000000000000..cdef6dbf3bbe --- /dev/null +++ b/tests/init-global/neg/global-list.scala @@ -0,0 +1,9 @@ +case class Foo(name: String) + +object O: // error + val a = Foo("Apple") + val b = Foo("Banana") + val c = Foo("Cherry") + +object Foo: + val all: List[Foo] = List(O.a, O.b, O.c) // error // error // error \ No newline at end of file diff --git a/tests/init-global/neg/global-local-var.scala b/tests/init-global/neg/global-local-var.scala new file mode 100644 index 000000000000..6965a42bd37f --- /dev/null +++ b/tests/init-global/neg/global-local-var.scala @@ -0,0 +1,16 @@ +class A(x: Int) { + def foo(): Int = { + val to = x + var sum = 0 + var i = 0 + while i < to do + sum += i + i += 1 + + B.a + 10 + sum // error + } +} + +object B { + val a: Int = A(4).foo() +} diff --git a/tests/init-global/neg/global-region1.scala b/tests/init-global/neg/global-region1.scala new file mode 100644 index 000000000000..48473717b5b5 --- /dev/null +++ b/tests/init-global/neg/global-region1.scala @@ -0,0 +1,9 @@ +trait B { def foo(): Int } +class C(var x: Int) extends B { def foo(): Int = 20 } +class D(var y: Int) extends B { def foo(): Int = A.m } // error +class Box(var value: B) + +object A: + val box1: Box = new Box(new C(5)) + val box2: Box = new Box(new D(10)) + val m: Int = box1.value.foo() diff --git a/tests/init-global/neg/i11262.scala b/tests/init-global/neg/i11262.scala new file mode 100644 index 000000000000..c1c01f6aad8c --- /dev/null +++ b/tests/init-global/neg/i11262.scala @@ -0,0 +1,2 @@ +object A { val x: String = B.y } // error +object B { val y: String = A.x } // error diff --git a/tests/init-global/neg/i12544b.scala b/tests/init-global/neg/i12544b.scala new file mode 100644 index 000000000000..586b88df04bd --- /dev/null +++ b/tests/init-global/neg/i12544b.scala @@ -0,0 +1,12 @@ +enum Enum: + case Case + +object Enum: + object nested: // error + val a: Enum = Case + + val b: Enum = f(nested.a) // error + + def f(e: Enum): Enum = e + +@main def main(): Unit = println(Enum.b) diff --git a/tests/init/full/neg/i9176.scala b/tests/init-global/neg/i9176.scala similarity index 81% rename from tests/init/full/neg/i9176.scala rename to tests/init-global/neg/i9176.scala index abb8a6394dd2..c93a16f2f8b1 100644 --- a/tests/init/full/neg/i9176.scala +++ b/tests/init-global/neg/i9176.scala @@ -1,9 +1,9 @@ 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) println(B.opposite) } -} \ No newline at end of file +} diff --git a/tests/init-global/neg/mutable-read1.scala b/tests/init-global/neg/mutable-read1.scala new file mode 100755 index 000000000000..507a8b7d74ad --- /dev/null +++ b/tests/init-global/neg/mutable-read1.scala @@ -0,0 +1,10 @@ +class Box(var value: Int) + +object A: + val box: Box = new Box(4) + +object B: + val boxB: Box = new Box(5) + val boxA: Box = A.box + val m: Int = boxB.value + val n: Int = boxA.value // error \ No newline at end of file diff --git a/tests/init-global/neg/mutable-read2.scala b/tests/init-global/neg/mutable-read2.scala new file mode 100755 index 000000000000..e7653c63d8bb --- /dev/null +++ b/tests/init-global/neg/mutable-read2.scala @@ -0,0 +1,10 @@ +object A: + class Box(var value: Int) { + val initial: Int = value + } + val box: Box = new Box(0) + +object B: + val box: A.Box = A.box + val a: Int = box.initial + val b: Int = box.value // error \ No newline at end of file diff --git a/tests/init-global/neg/mutable-read3.scala b/tests/init-global/neg/mutable-read3.scala new file mode 100755 index 000000000000..d103e112f372 --- /dev/null +++ b/tests/init-global/neg/mutable-read3.scala @@ -0,0 +1,9 @@ +object A: + class Box(var value: Int) + val box: Box = new Box(0) + +object B: + val boxes: Array[A.Box] = new Array(1) + boxes(0) = A.box + val box: A.Box = boxes(0) + val x: Int = box.value // error \ No newline at end of file diff --git a/tests/init-global/neg/mutable-read4.scala b/tests/init-global/neg/mutable-read4.scala new file mode 100755 index 000000000000..507a8b7d74ad --- /dev/null +++ b/tests/init-global/neg/mutable-read4.scala @@ -0,0 +1,10 @@ +class Box(var value: Int) + +object A: + val box: Box = new Box(4) + +object B: + val boxB: Box = new Box(5) + val boxA: Box = A.box + val m: Int = boxB.value + val n: Int = boxA.value // error \ No newline at end of file diff --git a/tests/init-global/neg/mutable-read5.scala b/tests/init-global/neg/mutable-read5.scala new file mode 100755 index 000000000000..c166295bf9fa --- /dev/null +++ b/tests/init-global/neg/mutable-read5.scala @@ -0,0 +1,9 @@ +object Names: + class Name(val start: Int, val length: Int) + var chrs: Array[Char] = new Array[Char](0x20000) + def name(s: String): Name = Name(0, chrs.length) // error + +object StdNames: + val AnyRef: Names.Name = Names.name("AnyRef") + val Array: Names.Name = Names.name("Array") + val List: Names.Name = Names.name("List") \ No newline at end of file diff --git a/tests/init-global/neg/mutable-read6.scala b/tests/init-global/neg/mutable-read6.scala new file mode 100755 index 000000000000..8b00eeaf4216 --- /dev/null +++ b/tests/init-global/neg/mutable-read6.scala @@ -0,0 +1,15 @@ +class SourceFile + +object Contexts: + val NoContext: Context = new Context + class Context: + private var _source: SourceFile = null + final def source: SourceFile = _source // error + def setSource(source: SourceFile) = { + this._source = source + } + +object Implicits: + import Contexts.* + case class SearchFailure(tag: Int, source: SourceFile) + val NoMatchingFailure: SearchFailure = SearchFailure(1, NoContext.source) \ No newline at end of file diff --git a/tests/init-global/neg/mutable-read7.scala b/tests/init-global/neg/mutable-read7.scala new file mode 100755 index 000000000000..ad9d154d74f5 --- /dev/null +++ b/tests/init-global/neg/mutable-read7.scala @@ -0,0 +1,13 @@ +object Positioned: + var debug: Boolean = false + var debugId = Int.MinValue + var nextId: Int = 0 + +abstract class Positioned: + if (Positioned.debug) { // error + println("do debugging") + } + +object Trees: + class Tree extends Positioned + val emptyTree = new Tree \ No newline at end of file diff --git a/tests/init-global/neg/mutable-read8.scala b/tests/init-global/neg/mutable-read8.scala new file mode 100755 index 000000000000..e830fa65be73 --- /dev/null +++ b/tests/init-global/neg/mutable-read8.scala @@ -0,0 +1,11 @@ +object Stats { + var monitored: Boolean = false +} + +class UncachedGroundType { + if (Stats.monitored) println("record stats") // error +} + +class LazyType extends UncachedGroundType + +object NoCompleter extends LazyType \ No newline at end of file diff --git a/tests/init-global/neg/partial-ordering.scala b/tests/init-global/neg/partial-ordering.scala new file mode 100755 index 000000000000..1bc1b251fb72 --- /dev/null +++ b/tests/init-global/neg/partial-ordering.scala @@ -0,0 +1,8 @@ +object Names: // error + val ctorString = "" + val ctorName: MethodName = MethodName.apply(ctorString) + +class MethodName(encoded: String) +object MethodName: + val ctor: MethodName = new MethodName(Names.ctorString) + def apply(name: String): MethodName = new MethodName(name) \ No newline at end of file diff --git a/tests/init-global/neg/return.scala b/tests/init-global/neg/return.scala new file mode 100755 index 000000000000..5cbf6915fc0e --- /dev/null +++ b/tests/init-global/neg/return.scala @@ -0,0 +1,10 @@ +object A: + def foo(x: Int): Int => Int = + if x <= 0 then + return (a: Int) => a + B.n // error + + (a: Int) => a * a + x + +object B: + val n = A.foo(-10)(20) + diff --git a/tests/init-global/neg/return2.scala b/tests/init-global/neg/return2.scala new file mode 100755 index 000000000000..6a4dec50c2dd --- /dev/null +++ b/tests/init-global/neg/return2.scala @@ -0,0 +1,13 @@ +object A: + def foo(x: Int): Int => Int = + val f = (a: Int) => a + B.n // error + var i = 0 + + val g = () => return f + + if x <= 0 then g() + + (a: Int) => a * a + x + +object B: + val n = A.foo(-10)(20) diff --git a/tests/init-global/neg/t5366.scala b/tests/init-global/neg/t5366.scala new file mode 100644 index 000000000000..854bdfe0544b --- /dev/null +++ b/tests/init-global/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-global/neg/t9115.scala b/tests/init-global/neg/t9115.scala new file mode 100644 index 000000000000..e7cfe09e560c --- /dev/null +++ b/tests/init-global/neg/t9115.scala @@ -0,0 +1,8 @@ +object D { + 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 + println(D1) + println(D2) +} diff --git a/tests/init-global/neg/t9261.scala b/tests/init-global/neg/t9261.scala new file mode 100644 index 000000000000..1e23bedb9b6a --- /dev/null +++ b/tests/init-global/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-global/neg/t9312.scala b/tests/init-global/neg/t9312.scala new file mode 100644 index 000000000000..703cf67e05c4 --- /dev/null +++ b/tests/init-global/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 { + 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) + } +} diff --git a/tests/init-global/neg/t9360.scala b/tests/init-global/neg/t9360.scala new file mode 100644 index 000000000000..291c4dd05db1 --- /dev/null +++ b/tests/init-global/neg/t9360.scala @@ -0,0 +1,25 @@ +class BaseClass(s: String) { + def print: Unit = () +} + +object Obj { + val s: String = "hello" + + object AObj extends BaseClass(s) // error + + 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-global/pos/global-by-name.scala b/tests/init-global/pos/global-by-name.scala new file mode 100644 index 000000000000..623d7af8335c --- /dev/null +++ b/tests/init-global/pos/global-by-name.scala @@ -0,0 +1,11 @@ +def time[A](f: => A): A = + val start = System.nanoTime + val res = f + val elapsed = (System.nanoTime - start) + res + +case class Foo(data: Int) + +object o: + val foo = time(Foo(3)) + println(foo.data) diff --git a/tests/init-global/pos/global-cycle10.scala b/tests/init-global/pos/global-cycle10.scala new file mode 100644 index 000000000000..9d6200cd884d --- /dev/null +++ b/tests/init-global/pos/global-cycle10.scala @@ -0,0 +1,17 @@ +abstract class Base { + val msg: String = "hello" + def foo(): Unit + foo() +} + +object O extends Base { // error + + class Inner { + println(msg) + } + + def foo() = new Inner +} + +@main +def Test = O diff --git a/tests/init-global/pos/global-cycle11.scala b/tests/init-global/pos/global-cycle11.scala new file mode 100644 index 000000000000..bbd33bd9b105 --- /dev/null +++ b/tests/init-global/pos/global-cycle11.scala @@ -0,0 +1,14 @@ +import scala.collection.mutable + +object NameKinds { // error + 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 + } +} diff --git a/tests/init-global/pos/global-cycle12.scala b/tests/init-global/pos/global-cycle12.scala new file mode 100644 index 000000000000..300ef02ee8a9 --- /dev/null +++ b/tests/init-global/pos/global-cycle12.scala @@ -0,0 +1,16 @@ +// from Scala.js +object Names { // error + 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) +} diff --git a/tests/init-global/pos/global-cycle9.scala b/tests/init-global/pos/global-cycle9.scala new file mode 100644 index 000000000000..4c2421f190d8 --- /dev/null +++ b/tests/init-global/pos/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() +} diff --git a/tests/init-global/pos/global-fun.scala b/tests/init-global/pos/global-fun.scala new file mode 100644 index 000000000000..97c3cb394e81 --- /dev/null +++ b/tests/init-global/pos/global-fun.scala @@ -0,0 +1,13 @@ +class C: + def double(x: Int): Int = x * 2 + +object A: + val n: Int = 10 + val f: C => Int = foo(n) + + def foo(x: Int): C => Int = + c => c.double(x) + + +object B: + var y = A.f(new C) diff --git a/tests/init-global/pos/global-instantiation.scala b/tests/init-global/pos/global-instantiation.scala new file mode 100755 index 000000000000..6964901e964e --- /dev/null +++ b/tests/init-global/pos/global-instantiation.scala @@ -0,0 +1,7 @@ +class A(x: Int) { + def foo(): Int = B.m +} + +object B: + val m: Int = 20 + val n: Int = new A(10).foo() \ No newline at end of file diff --git a/tests/init-global/pos/global-read.scala b/tests/init-global/pos/global-read.scala new file mode 100755 index 000000000000..5f2386d12c2c --- /dev/null +++ b/tests/init-global/pos/global-read.scala @@ -0,0 +1,8 @@ +object A: + val a: Int = 10 + val b: Int = 20 + +object B: + var n: Int = A.a * A.b + +@main def entry() = println(B.n) \ No newline at end of file diff --git a/tests/init-global/pos/global-recursion.scala b/tests/init-global/pos/global-recursion.scala new file mode 100644 index 000000000000..42c80c46fde3 --- /dev/null +++ b/tests/init-global/pos/global-recursion.scala @@ -0,0 +1,6 @@ +object Recursion: + def foo(): Int = + def fact(x: Int): Int = if x == 0 then 1 else x * fact(x - 1) + fact(5) + + val n = foo() \ No newline at end of file diff --git a/tests/init-global/pos/global-recursion2.scala b/tests/init-global/pos/global-recursion2.scala new file mode 100755 index 000000000000..5a9e3edfad14 --- /dev/null +++ b/tests/init-global/pos/global-recursion2.scala @@ -0,0 +1,6 @@ +class A(val a: A) + +object B: + val a: A = loop(ofA()) + def ofA(): A = ofA().a + def loop(a: A): A = loop(new A(a)) \ No newline at end of file diff --git a/tests/init-global/pos/global-region1.scala b/tests/init-global/pos/global-region1.scala new file mode 100644 index 000000000000..db56fe45e1a4 --- /dev/null +++ b/tests/init-global/pos/global-region1.scala @@ -0,0 +1,11 @@ +import scala.annotation.init.region + +trait B { def foo(): Int } +class C(var x: Int) extends B { def foo(): Int = 20 } +class D(var y: Int) extends B { def foo(): Int = A.m } +class Box(var value: B) + +object A: + val box1: Box = region { new Box(new C(5)) } + val box2: Box = region { new Box(new D(10)) } + val m: Int = box1.value.foo() // ok diff --git a/tests/init-global/pos/global-this.scala b/tests/init-global/pos/global-this.scala new file mode 100755 index 000000000000..b6807416bbd1 --- /dev/null +++ b/tests/init-global/pos/global-this.scala @@ -0,0 +1,11 @@ +object NameKinds: + abstract class NameKind(val tag: Int): + class Info + class QualifiedNameKind(tag: Int, val separator: String) extends NameKind(tag): + qualifiedNameKinds(tag) = this // error + + val MAX_TAG = 8 + val qualifiedNameKinds = new Array[QualifiedNameKind](MAX_TAG) + + val QualifiedName: QualifiedNameKind = new QualifiedNameKind(0, ".") + val FlatName: QualifiedNameKind = new QualifiedNameKind(1, "$") diff --git a/tests/init-global/pos/global-trivial-cycle.scala b/tests/init-global/pos/global-trivial-cycle.scala new file mode 100644 index 000000000000..b9371e8600db --- /dev/null +++ b/tests/init-global/pos/global-trivial-cycle.scala @@ -0,0 +1,9 @@ +object C: + val n: Int = A.n + +object A: + val x: Int = 10 + val n: Int = B.foo() + +object B: + def foo(): Int = A.x diff --git a/tests/init-global/pos/global-val-owner.scala b/tests/init-global/pos/global-val-owner.scala new file mode 100644 index 000000000000..164e56d9e776 --- /dev/null +++ b/tests/init-global/pos/global-val-owner.scala @@ -0,0 +1,15 @@ +object Test: + val n = { + def fact(x: Int): Int = if x == 0 then 1 else x * fact(x - 1) + fact(5) + } + + def foo() = + val n = { + def fact(x: Int): Int = if x == 0 then 1 else x * fact(x - 1) + fact(5) + } + n + + val x = foo() + diff --git a/tests/init-global/pos/global-val-owner2.scala b/tests/init-global/pos/global-val-owner2.scala new file mode 100644 index 000000000000..8d84b792cc81 --- /dev/null +++ b/tests/init-global/pos/global-val-owner2.scala @@ -0,0 +1,5 @@ +object Test: + abstract class Base(implicit config: Int) + case class A(x: Int)(implicit config: Int) extends Base + + val a: A = A(3)(using 5) diff --git a/tests/init-global/pos/tree-counter.scala b/tests/init-global/pos/tree-counter.scala new file mode 100755 index 000000000000..2201911af608 --- /dev/null +++ b/tests/init-global/pos/tree-counter.scala @@ -0,0 +1,8 @@ +object Trees: + private var counter = 0 + class Tree { + counter += 1 + } + + class EmptyTree extends Tree + val theEmptyTree = new EmptyTree \ No newline at end of file diff --git a/tests/init/crash/by-name.scala b/tests/init/crash/by-name.scala new file mode 100644 index 000000000000..623d7af8335c --- /dev/null +++ b/tests/init/crash/by-name.scala @@ -0,0 +1,11 @@ +def time[A](f: => A): A = + val start = System.nanoTime + val res = f + val elapsed = (System.nanoTime - start) + res + +case class Foo(data: Int) + +object o: + val foo = time(Foo(3)) + println(foo.data) diff --git a/tests/init/crash/t6888.scala b/tests/init/crash/t6888.scala new file mode 100644 index 000000000000..d339f840c86f --- /dev/null +++ b/tests/init/crash/t6888.scala @@ -0,0 +1,19 @@ +class C { + val x = 1 + object `$` { + val y = x + x + class abc$ { + def xy = x + y + } + object abc$ { + def xy = x + y + } + } +} + +object Test extends App { + val c = new C() + println(c.`$`.y) + println(c.`$`.abc$.xy) + println(new c.`$`.abc$().xy) +} 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/full/neg/global-cycle3.scala b/tests/init/full/neg/global-cycle3.scala deleted file mode 100644 index 7fae20dbe894..000000000000 --- a/tests/init/full/neg/global-cycle3.scala +++ /dev/null @@ -1,7 +0,0 @@ -class A(x: Int) { - def foo(): Int = B.a + 10 -} - -object B { - val a: Int = A(4).foo() // error -} diff --git a/tests/run-custom-args/tasty-inspector/stdlibExperimentalDefinitions.scala b/tests/run-custom-args/tasty-inspector/stdlibExperimentalDefinitions.scala index 1eb2ce3beee0..4860d19dbc8b 100644 --- a/tests/run-custom-args/tasty-inspector/stdlibExperimentalDefinitions.scala +++ b/tests/run-custom-args/tasty-inspector/stdlibExperimentalDefinitions.scala @@ -63,6 +63,12 @@ val experimentalDefinitionInLibrary = Set( //// New feature: Macro annotations "scala.annotation.MacroAnnotation", + //// New feature: -Ysafe-init-global + "scala.annotation.init", + "scala.annotation.init$", + "scala.annotation.init$.widen", + "scala.annotation.init$.region", + //// New APIs: Quotes // Should be stabilized in 3.4.0 "scala.quoted.Quotes.reflectModule.defnModule.FunctionClass",