diff --git a/compiler/src/dotty/tools/dotc/transform/init/Objects.scala b/compiler/src/dotty/tools/dotc/transform/init/Objects.scala index 226e8dbf3fb4..cc17f62780e5 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Objects.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Objects.scala @@ -18,6 +18,7 @@ import util.{ SourcePosition, NoSourcePosition } import config.Printers.init as printer import reporting.StoreReporter import reporting.trace as log +import reporting.trace.force as forcelog import typer.Applications.* import Errors.* @@ -55,16 +56,9 @@ import dotty.tools.dotc.util.SrcPos * 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 "Top" --- a Top value may not be actively - * used during initialization, i.e., it's forbidden to call methods or access fields of a Top. - * Method arguments are widened to Top by default unless specified to be sensitive. - * Method parameters captured in lambdas or inner classes are always widened to Top. + * 2. It is inter-procedural and flow-sensitive. * - * 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 + * 3. 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. * */ @@ -90,117 +84,115 @@ class Objects(using Context @constructorOnly): /** Syntax for the data structure abstraction used in abstract domain: * * ve ::= ObjectRef(class) // global object - * | OfClass(class, vs[outer], ctor, args, env) // instance of a class - * | OfArray(object[owner], regions) - * | Fun(..., env) // value elements that can be contained in ValueSet + * | OfClass(class, ownerObject, ctor, regions) // instance of a class + * | OfArray(ownerObject, regions) // represents values of native array class in Array.scala + * | Fun(code, LocalEnv) // value elements that can be contained in ValueSet * | SafeValue // values on which method calls and field accesses won't cause warnings. Int, String, etc. + * | UnknownValue // values whose source are unknown at compile time * vs ::= ValueSet(ve) // set of abstract values - * Bottom ::= ValueSet(Empty) - * val ::= ve | Top | UnknownValue | vs | Package // all possible abstract values in domain - * Ref ::= ObjectRef | OfClass // values that represent a reference to some (global or instance) object - * ThisValue ::= Ref | Top // possible values for 'this' - * - * refMap = Ref -> ( valsMap, varsMap, outersMap ) // refMap stores field informations of an object or instance - * valsMap = valsym -> val // maps immutable fields to their values - * varsMap = valsym -> addr // each mutable field has an abstract address - * outersMap = class -> val // maps outer objects to their values - * - * arrayMap = OfArray -> addr // an array has one address that stores the join value of every element - * - * heap = addr -> val // heap is mutable + * Bottom ::= ValueSet(Empty) // unreachable code + * val ::= ve | vs | Package + * Ref ::= ObjectRef | OfClass | OfArray // values that represent a reference to some (global or instance) object + * ThisValue ::= Ref | Set(Ref) // possible values for 'this' + * LocalEnv(meth, ownerObject) // represents environments for methods or functions + * Scope ::= Ref | LocalEnv + * ScopeSet ::= Set(Scope) * - * env = (valsMap, Option[env]) // stores local variables in the residing method, and possibly outer environments - * - * addr ::= localVarAddr(regions, valsym, owner) - * | fieldVarAddr(regions, valsym, owner) // independent of OfClass/ObjectRef - * | arrayAddr(regions, owner) // independent of array element type + * valsMap = sym -> val // maps variables to their values + * outersMap = sym -> ScopeSet // maps the possible outer scopes for a corresponding (parent) class + * heap.MutableData = Scope -> (valsMap, outersMap) // heap is mutable * * regions ::= List(sourcePosition) */ - sealed abstract class Value: + sealed trait Value: def show(using Context): String - /** ValueElement are elements that can be contained in a RefSet */ - sealed abstract class ValueElement extends Value + /** ValueElement are elements that can be contained in a ValueSet */ + sealed trait ValueElement extends Value /** * 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 ValueElement: - 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 - + sealed abstract class Scope(using trace: Trace): // TODO: rename it to reflect that it is key to the heap def isObjectRef: Boolean = this.isInstanceOf[ObjectRef] - def klass: ClassSymbol + def getTrace: Trace = trace + + def isRef = this.isInstanceOf[Ref] - def valValue(sym: Symbol): Value = vals(sym) + def isEnv = this.isInstanceOf[Env.LocalEnv] - def varAddr(sym: Symbol): Heap.Addr = vars(sym) + def asRef: Ref = this.asInstanceOf[Ref] - def outerValue(cls: ClassSymbol): Value = outers(cls) + def asEnv: Env.LocalEnv = this.asInstanceOf[Env.LocalEnv] - def hasVal(sym: Symbol): Boolean = vals.contains(sym) + def owner: ClassSymbol - def hasVar(sym: Symbol): Boolean = vars.contains(sym) + def show(using Context): String + + def outer(using Heap.MutableData): ScopeSet + + def valValue(sym: Symbol)(using Heap.MutableData): Value = Heap.readVal(this, sym) + + def varValue(sym: Symbol)(using Heap.MutableData): Value = Heap.readVal(this, sym) + + def hasVal(sym: Symbol)(using Heap.MutableData): Boolean = Heap.containsVal(this, sym) - def hasOuter(cls: ClassSymbol): Boolean = outers.contains(cls) + def hasVar(sym: Symbol)(using Heap.MutableData): Boolean = Heap.containsVal(this, sym) - def initVal(field: Symbol, value: Value)(using Context) = log("Initialize " + field.show + " = " + value + " for " + this, printer) { + def initVal(field: Symbol, value: Value)(using Context, Heap.MutableData) = 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 + Heap.writeJoinVal(this, field, value) } - def initVar(field: Symbol, addr: Heap.Addr)(using Context) = log("Initialize " + field.show + " = " + addr + " for " + this, printer) { + def initVar(field: Symbol, value: Value)(using Context, Heap.MutableData) = log("Initialize " + field.show + " = " + value + " 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 + Heap.writeJoinVal(this, field, value) } - 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 + def initOuter(sym: Symbol, outerScope: ScopeSet)(using Context, Heap.MutableData) = log("Initialize outer " + sym.show + " = " + outerScope + " for " + this, printer) { + Heap.writeJoinOuter(this, sym, outerScope) } + sealed abstract class Ref(using Trace) extends Scope with ValueElement: + def klass: ClassSymbol + + def outerValue(sym: Symbol)(using Heap.MutableData): ScopeSet = Heap.readOuter(this, sym) + + def outer(using Heap.MutableData): ScopeSet = this.outerValue(klass) + /** 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 + case class ObjectRef private (klass: ClassSymbol)(using Trace) extends Ref: + def owner = klass def show(using Context) = "ObjectRef(" + klass.show + ")" + object ObjectRef: + def apply(klass: ClassSymbol)(using Context, Heap.MutableData, Trace): ObjectRef = + val obj = new ObjectRef(klass) + obj.initOuter(klass, Env.NoEnv) + obj + /** * 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) - + klass: ClassSymbol, owner: ClassSymbol, ctor: Symbol, regions: Regions.Data)(using Trace) + extends Ref: def show(using Context) = - val valFields = vals.map(_.show + " -> " + _.show) - "OfClass(" + klass.show + ", outer = " + outer + ", args = " + args.map(_.show) + " env = " + env.show + ", vals = " + valFields + ")" + "OfClass(" + klass.show + ", ctor = " + ctor.show + ", owner = " + owner + ")" object OfClass: def apply( - klass: ClassSymbol, outer: Value, ctor: Symbol, args: List[Value], env: Env.Data)( - using Context + klass: ClassSymbol, outerScope: ScopeSet, ctor: Symbol)( + using Context, Heap.MutableData, State.Data, Regions.Data, Trace ): 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) + val owner = State.currentObject + val instance = new OfClass(klass, owner, ctor, summon[Regions.Data]) + instance.initOuter(klass, outerScope) instance /** @@ -215,17 +207,30 @@ class Objects(using Context @constructorOnly): * * @param owner The static object whose initialization creates the array. */ - case class OfArray(owner: ClassSymbol, regions: Regions.Data)(using @constructorOnly ctx: Context, @constructorOnly trace: Trace) extends ValueElement: - val klass: ClassSymbol = defn.ArrayClass - val addr: Heap.Addr = Heap.arrayAddr(regions, owner) + case class OfArray private (owner: ClassSymbol, regions: Regions.Data)(using Trace) extends Ref: + val elementSymbol = defn.ArrayConstructor + + def klass: ClassSymbol = defn.ArrayClass + def show(using Context) = "OfArray(owner = " + owner.show + ")" + def readElement(using Heap.MutableData) = valValue(elementSymbol) + + def writeElement(value: Value)(using Heap.MutableData) = Heap.writeJoinVal(this, elementSymbol, value) + + object OfArray: + def apply(owner: ClassSymbol, regions: Regions.Data)(using Context, Trace, Heap.MutableData): OfArray = + val arr = new OfArray(owner, regions) + arr.initVal(arr.elementSymbol, Bottom) + arr.initOuter(arr.klass, Env.NoEnv) + arr + /** * Represents a lambda expression * @param klass The enclosing class of the anonymous function's creation site */ - case class Fun(code: Tree, thisV: ThisValue, klass: ClassSymbol, env: Env.Data) extends ValueElement: - def show(using Context) = "Fun(" + code.show + ", " + thisV.show + ", " + klass.show + ")" + case class Fun(code: Tree, thisV: ThisValue, klass: ClassSymbol, scope: Scope) extends ValueElement: + def show(using Context) = "Fun(" + code.show + ", " + scope.show + ", " + klass.show + ")" /** * Represents common base values like Int, String, etc. @@ -264,6 +269,11 @@ class Objects(using Context @constructorOnly): assert(typeSymbol.isDefined, "Invalid creation of SafeValue with type " + tpe) new SafeValue(typeSymbol.get) + /** Represents values unknown to the checker, such as values loaded without source + */ + case object UnknownValue extends ValueElement: + def show(using Context): String = "UnknownValue" + /** * Represents a set of values * @@ -272,7 +282,22 @@ class Objects(using Context @constructorOnly): case class ValueSet(values: Set[ValueElement]) extends Value: def show(using Context) = values.map(_.show).mkString("[", ",", "]") - case class Package(packageModuleClass: ClassSymbol) extends Value: + def isRefSet = values.forall(_.isInstanceOf[Ref]) + + def toScopeSet: ScopeSet = ScopeSet(values.asInstanceOf[Set[Scope]]) + + case class ScopeSet(scopes: Set[Scope]): + assert(scopes.forall(_.isRef) || scopes.forall(_.isEnv), "All scopes should have the same type!") + + def show(using Context) = scopes.map(_.show).mkString("[", ",", "]") + + def toValueSet: ValueSet = ValueSet(scopes.asInstanceOf[Set[ValueElement]]) + + def lookupSymbol(sym: Symbol)(using Heap.MutableData) = scopes.map(_.valValue(sym)).join + + def outers(using Heap.MutableData): ScopeSet = scopes.map(_.outer).join + + case class Package(packageModuleClass: ClassSymbol) extends Value: // TODO: try to remove packages def show(using Context): String = "Package(" + packageModuleClass.show + ")" object Package: @@ -280,29 +305,10 @@ class Objects(using Context @constructorOnly): assert(packageSym.is(Flags.Package), "Invalid symbol to create Package!") Package(packageSym.moduleClass.asClass) - /** Represents values unknown to the checker, such as values loaded without source - * UnknownValue is not ValueElement since RefSet containing UnknownValue - * is equivalent to UnknownValue - */ - case object UnknownValue extends Value: - def show(using Context): String = "UnknownValue" - - /** Represents values lost due to widening - * - * This is the top of the abstract domain lattice, which should not - * be used during initialization. - * - * Top is not ValueElement since RefSet containing Top - * is equivalent to Top - */ - - case object Top extends Value: - def show(using Context): String = "Top" - val Bottom = ValueSet(ListSet.empty) /** Possible types for 'this' */ - type ThisValue = Ref | Top.type + type ThisValue = Ref | ValueSet /** Checking state */ object State: @@ -315,7 +321,7 @@ class Objects(using Context @constructorOnly): def currentObject(using data: Data): ClassSymbol = data.checkingObjects.last.klass - private def doCheckObject(classSym: ClassSymbol)(using ctx: Context, data: Data) = + private def doCheckObject(classSym: ClassSymbol)(using ctx: Context, data: Data, heap: Heap.MutableData) = val tpl = classSym.defTree.asInstanceOf[TypeDef].rhs.asInstanceOf[Template] var count = 0 @@ -326,12 +332,12 @@ class Objects(using Context @constructorOnly): count += 1 given Trace = Trace.empty.add(classSym.defTree) - given Env.Data = Env.emptyEnv(tpl.constr.symbol) - given Heap.MutableData = Heap.empty() + // 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) + given Scope = obj log("Iteration " + count) { data.checkingObjects += obj init(tpl, obj, classSym) @@ -356,8 +362,8 @@ class Objects(using Context @constructorOnly): obj end doCheckObject - def checkObjectAccess(clazz: ClassSymbol)(using data: Data, ctx: Context, pendingTrace: Trace): ObjectRef = - val index = data.checkingObjects.indexOf(ObjectRef(clazz)) + def checkObjectAccess(clazz: ClassSymbol)(using data: Data, ctx: Context, pendingTrace: Trace, heap: Heap.MutableData): ObjectRef = + val index = data.checkingObjects.indexWhere(_.klass == clazz) if index != -1 then if data.checkingObjects.size - 1 > index then @@ -384,121 +390,103 @@ class Objects(using Context @constructorOnly): /** 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) - + case class LocalEnv(meth: Symbol, owner: ClassSymbol)(using Trace) extends Scope: 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}" + "meth: " + meth.show + "\n" + + "owner: " + owner.show + def outer(using Heap.MutableData): ScopeSet = Heap.readOuter(this, meth) 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 + val NoEnv = ScopeSet(Set.empty) /** 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 emptyEnv(meth: Symbol)(using Context, State.Data, Heap.MutableData, Trace): LocalEnv = + _of(Map.empty, meth, NoEnv) - def valValue(x: Symbol)(using data: Data, ctx: Context, trace: Trace): Value = - data.getVal(x) match - case Some(theValue) => - theValue - case _ => - report.warning("[Internal error] Value not found " + x.show + "\nenv = " + data.show + ". " + Trace.show, Trace.position) + def valValue(x: Symbol)(using scope: Scope, ctx: Context, trace: Trace, heap: Heap.MutableData): Value = + if scope.hasVal(x) then + scope.valValue(x) + else + report.warning("[Internal error] Value not found " + x.show + "\nscope = " + scope.show + ". " + Trace.show, Trace.position) Bottom - def getVal(x: Symbol)(using data: Data, ctx: Context): Option[Value] = data.getVal(x) + private[Env] def _of(argMap: Map[Symbol, Value], meth: Symbol, outerSet: ScopeSet) + (using State.Data, Heap.MutableData, Trace): LocalEnv = + val env = LocalEnv(meth, State.currentObject) + argMap.foreach(env.initVal(_, _)) + env.initOuter(meth, outerSet) + env - def getVar(x: Symbol)(using data: Data, ctx: Context): Option[Heap.Addr] = data.getVar(x) + /** + * The main procedure for searching through the outer chain + * @param target The symbol to search for if `bySymbol = true`; otherwise the method symbol of the target environment + * @param scopeSet The set of scopes as starting point + * @return The scopes that contains symbol `target` or whose method is `target`, + * and the value for `C.this` where C is the enclosing class of the result scopes + */ + private[Env] def resolveEnvRecur( + target: Symbol, scopeSet: ScopeSet, bySymbol: Boolean = true) + : Contextual[Option[(ThisValue, ScopeSet)]] = + if scopeSet == Env.NoEnv then None + else + val targetClass = target.owner.lexicallyEnclosingClass.asClass + val head = scopeSet.scopes.head + val filter = + if bySymbol then + scopeSet.scopes.filter(_.hasVal(target)) + else + scopeSet.scopes.filter(s => s.isEnv && s.asEnv.meth == target) + + assert(filter.isEmpty || filter.size == scopeSet.scopes.size, "Either all scopes or no scopes contain " + target) + if (!filter.isEmpty) then + val resultSet = ScopeSet(filter) + val outerThis = resolveThisRecur(targetClass, resultSet) + Some((outerThis, resultSet)) + else + val outerScopes = scopeSet.outers + resolveEnvRecur(target, outerScopes, bySymbol) - private[Env] def _of(argMap: Map[Symbol, Value], meth: Symbol, outer: Data): Data = - new LocalEnv(argMap, meth, outer)(valsMap = mutable.Map.empty, varsMap = mutable.Map.empty) - def ofDefDef(ddef: DefDef, args: List[Value], outer: Data)(using Context): Data = + def ofDefDef(ddef: DefDef, args: List[Value], outer: ScopeSet) + (using State.Data, Heap.MutableData, Trace): LocalEnv = 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) + // assert(ddef.symbol.owner.isClass ^ (outer != NoEnv), "ddef.owner = " + ddef.symbol.owner.show + ", outer = " + outer + ", " + ddef.source) _of(params.zip(args).toMap, ddef.symbol, outer) - def ofByName(byNameParam: Symbol, outer: Data): Data = + def ofByName(byNameParam: Symbol, outer: Scope)(using State.Data, Heap.MutableData, Trace): LocalEnv = assert(byNameParam.is(Flags.Param) && byNameParam.info.isInstanceOf[ExprType]); - _of(Map.empty, byNameParam, outer) + _of(Map.empty, byNameParam, ScopeSet(Set(outer))) - def setLocalVal(x: Symbol, value: Value)(using data: Data, ctx: Context): Unit = + def setLocalVal(x: Symbol, value: Value)(using scope: Scope, ctx: Context, heap: Heap.MutableData): Unit = assert(!x.isOneOf(Flags.Param | Flags.Mutable), "Only local immutable variable allowed") - data match + scope 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) + localEnv.initVal(x, value) + case ref: Ref => + ref.initVal(x, value) // TODO: This is possible for match statement in class body. Report warning? - def setLocalVar(x: Symbol, addr: Heap.Addr)(using data: Data, ctx: Context): Unit = + def setLocalVar(x: Symbol, value: Value)(using scope: Scope, ctx: Context, heap: Heap.MutableData): Unit = assert(x.is(Flags.Mutable, butNot = Flags.Param), "Only local mutable variable allowed") - data match + scope 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) + localEnv.initVar(x, value) + case ref: Ref => + ref.initVar(x, value) // TODO: This is possible for match statement in class body. Report warning? /** * Resolve the environment by searching for a given symbol. * - * Searches for the environment that owns `target`, starting from `env` as the innermost. + * Searches for the environment that defines `target`, starting from `env` as the innermost. * * 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. @@ -507,29 +495,15 @@ class Objects(using Context @constructorOnly): * @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 that owns the `target` and value for `this` owned by the given method. + * @return the environment that owns the `target` and value for `this` that owns the owner of target. */ - def resolveEnvByValue(target: Symbol, thisV: ThisValue, env: Data)(using Context): Option[(ThisValue, Data)] = log("Resolving env by value for " + target.show + ", this = " + thisV.show + ", env = " + env.show, printer) { - env match - case localEnv: LocalEnv => - if localEnv.getVal(target).isDefined then Some(thisV -> localEnv) - else if localEnv.getVar(target).isDefined then Some(thisV -> localEnv) - else resolveEnvByValue(target, thisV, localEnv.outer) - case NoEnv => - thisV match - case ref: OfClass => - ref.outer match - case outer : ThisValue => - resolveEnvByValue(target, outer, ref.env) - case _ => - // TODO: properly handle the case where ref.outer is ValueSet - None - case _ => - None + def resolveEnvByValue(target: Symbol, thisV: ThisValue, scope: Scope) + (using Context, Heap.MutableData): Contextual[Option[(ThisValue, ScopeSet)]] = log("Resolving env by value for " + target.show + ", this = " + thisV.show + ", scope = " + scope.show, printer) { + resolveEnvRecur(target, ScopeSet(Set(scope))) } /** - * Resolve the environment owned by the given method `enclosing`. + * Resolve the environment associated by the given method `enclosing`, starting from `env` as the innermost. * * The method could be located in outer scope with intermixed classes between its definition * site and usage site. @@ -544,42 +518,28 @@ class Objects(using Context @constructorOnly): * * @return the environment and value for `this` owned by the given method. */ - def resolveEnvByOwner(enclosing: Symbol, thisV: ThisValue, env: Data)(using Context): Option[(ThisValue, Data)] = log("Resolving env by owner for " + enclosing.show + ", this = " + thisV.show + ", env = " + env.show, printer) { + def resolveEnvByMethod(enclosing: Symbol, thisV: ThisValue, scope: Scope)(using Context, Heap.MutableData): Contextual[(ThisValue, ScopeSet)] = log("Resolving env which corresponds to method " + enclosing.show + ", this = " + thisV.show + ", scope = " + scope.show, printer) { assert(enclosing.is(Flags.Method), "Only method symbols allows, got " + enclosing.show) - env match - case localEnv: LocalEnv => - if localEnv.meth == enclosing then Some(thisV -> env) - else resolveEnvByOwner(enclosing, thisV, localEnv.outer) - case NoEnv => - thisV match - case ref: OfClass => - ref.outer match - case outer : ThisValue => - resolveEnvByOwner(enclosing, outer, ref.env) - case _ => - // TODO: properly handle the case where ref.outer is ValueSet - None - case _ => - None + val result = resolveEnvRecur(enclosing, ScopeSet(Set(scope)), bySymbol = false) + assert(!result.isEmpty, "Failed to find environment for " + enclosing + "!") + result.get } - def withEnv[T](env: Data)(fn: Data ?=> T): T = fn(using env) + def withEnv[T](env: LocalEnv)(fn: LocalEnv ?=> 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 - def getTrace: Trace = Trace.empty + private case class ScopeBody( + valsMap: Map[Symbol, Value], + outersMap: Map[Symbol, ScopeSet] + ) - /** The address for mutable fields of objects. */ - private case class FieldAddr(regions: Regions.Data, field: Symbol, owner: ClassSymbol)(trace: Trace) extends Addr: - override def getTrace: Trace = trace - - /** The address for mutable local variables . */ - private case class LocalVarAddr(regions: Regions.Data, sym: Symbol, owner: ClassSymbol) extends Addr + private def emptyScopeBody(): ScopeBody = ScopeBody( + valsMap = Map.empty, + outersMap = Map.empty + ) /** Immutable heap data used in the cache. * @@ -587,40 +547,65 @@ class Objects(using Context @constructorOnly): * * TODO: speed up equality check for heap. */ - opaque type Data = Map[Addr, Value] + opaque type Data = Map[Scope, ScopeBody] /** Store the heap as a mutable field to avoid threading it through the program. */ class MutableData(private[Heap] var heap: Data): - private[Heap] def writeJoin(addr: Addr, value: Value): Unit = - heap.get(addr) match + private[Heap] def writeJoinVal(scope: Scope, valSymbol: Symbol, value: Value): Unit = + heap.get(scope) match case None => - heap = heap.updated(addr, value) + heap = heap.updated(scope, Heap.emptyScopeBody()) + writeJoinVal(scope, valSymbol, value) case Some(current) => - val value2 = value.join(current) - if value2 != current then - heap = heap.updated(addr, value2) + val newValsMap = current.valsMap.join(valSymbol, value) + heap = heap.updated(scope, new ScopeBody( + valsMap = newValsMap, + outersMap = current.outersMap + )) + + private[Heap] def writeJoinOuter(scope: Scope, outerSymbol: Symbol, outerScope: ScopeSet): Unit = + heap.get(scope) match + case None => + heap = heap.updated(scope, Heap.emptyScopeBody()) + writeJoinOuter(scope, outerSymbol, outerScope) + + case Some(current) => + val newOutersMap = current.outersMap.join(outerSymbol, outerScope) + heap = heap.updated(scope, new ScopeBody( + valsMap = current.valsMap, + outersMap = newOutersMap + )) end MutableData - def empty(): MutableData = new MutableData(Map.empty) + def empty: MutableData = new MutableData(Map.empty) - def contains(addr: Addr)(using mutable: MutableData): Boolean = - mutable.heap.contains(addr) + def contains(scope: Scope)(using mutable: MutableData): Boolean = + mutable.heap.contains(scope) + + def containsVal(scope: Scope, value: Symbol)(using mutable: MutableData): Boolean = + if mutable.heap.contains(scope) then + mutable.heap(scope).valsMap.contains(value) + else + false - def read(addr: Addr)(using mutable: MutableData): Value = - mutable.heap(addr) + def containsOuter(scope: Scope, outer: Symbol)(using mutable: MutableData): Boolean = + if mutable.heap.contains(scope) then + mutable.heap(scope).outersMap.contains(outer) + else + false - def writeJoin(addr: Addr, value: Value)(using mutable: MutableData): Unit = - mutable.writeJoin(addr, value) + def readVal(scope: Scope, value: Symbol)(using mutable: MutableData): Value = + mutable.heap(scope).valsMap(value) - def localVarAddr(regions: Regions.Data, sym: Symbol, owner: ClassSymbol): Addr = - LocalVarAddr(regions, sym, owner) + def readOuter(scope: Scope, outer: Symbol)(using mutable: MutableData): ScopeSet = + mutable.heap(scope).outersMap(outer) - def fieldVarAddr(regions: Regions.Data, sym: Symbol, owner: ClassSymbol)(using Trace): Addr = - FieldAddr(regions, sym, owner)(summon[Trace]) + def writeJoinVal(scope: Scope, valSymbol: Symbol, value: Value)(using mutable: MutableData): Unit = + mutable.writeJoinVal(scope, valSymbol, value) - def arrayAddr(regions: Regions.Data, owner: ClassSymbol)(using Trace, Context): Addr = - FieldAddr(regions, defn.ArrayClass, owner)(summon[Trace]) + def writeJoinOuter(scope: Scope, outer: Symbol, outerScope: ScopeSet)(using mutable: MutableData): Unit = + mutable.writeJoinOuter(scope, outer, outerScope) def getHeapData()(using mutable: MutableData): Data = mutable.heap @@ -628,16 +613,16 @@ class Objects(using Context @constructorOnly): /** Cache used to terminate the check */ object Cache: - case class Config(thisV: Value, env: Env.Data, heap: Heap.Data) + case class Config(thisV: Value, scope: Scope, 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()) + def get(thisV: Value, expr: Tree)(using Heap.MutableData, Scope): Option[Value] = + val config = Config(thisV, summon[Scope], Heap.getHeapData()) super.get(config, expr).map(_.value) - def cachedEval(thisV: ThisValue, expr: Tree, cacheResult: Boolean)(fun: Tree => Value)(using Heap.MutableData, Env.Data): Value = - val config = Config(thisV, summon[Env.Data], Heap.getHeapData()) + def cachedEval(thisV: ThisValue, expr: Tree, cacheResult: Boolean)(fun: Tree => Value)(using Heap.MutableData, Scope): Value = + val config = Config(thisV, summon[Scope], Heap.getHeapData()) val result = super.cachedEval(config, expr, cacheResult, default = Res(Bottom, Heap.getHeapData())) { expr => Res(fun(expr), Heap.getHeapData()) } @@ -685,28 +670,30 @@ class Objects(using Context @constructorOnly): 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 + type Contextual[T] = (Context, State.Data, Scope, Cache.Data, Heap.MutableData, Regions.Data, Returns.Data, Trace) ?=> T // --------------------------- domain operations ----------------------------- case class ArgInfo(value: Value, trace: Trace, tree: Tree) - extension (a: Value) - def join(b: Value): Value = - assert(!a.isInstanceOf[Package] && !b.isInstanceOf[Package], "Unexpected join between " + a + " and " + b) - (a, b) match - case (Top, _) => Top - case (_, Top) => Top - case (UnknownValue, _) => UnknownValue - case (_, UnknownValue) => UnknownValue - case (Bottom, b) => b - case (a, Bottom) => a - case (ValueSet(values1), ValueSet(values2)) => ValueSet(values1 ++ values2) - case (a : ValueElement, ValueSet(values)) => ValueSet(values + a) - case (ValueSet(values), b : ValueElement) => ValueSet(values + b) - case (a : ValueElement, b : ValueElement) => ValueSet(Set(a, b)) - case _ => Bottom + trait Join[V]: + extension (v1: V) + def join(v2: V): V + + given Join[Value] with + extension (a: Value) + def join(b: Value): Value = + assert(!a.isInstanceOf[Package] && !b.isInstanceOf[Package], "Unexpected join between " + a + " and " + b) + (a, b) match + case (Bottom, b) => b + case (a, Bottom) => a + case (ValueSet(values1), ValueSet(values2)) => ValueSet(values1 ++ values2) + case (a : ValueElement, ValueSet(values)) => ValueSet(values + a) + case (ValueSet(values), b : ValueElement) => ValueSet(values + b) + case (a : ValueElement, b : ValueElement) => ValueSet(Set(a, b)) + case _ => Bottom + extension (a: Value) def remove(b: Value): Value = (a, b) match case (ValueSet(values1), b: ValueElement) => ValueSet(values1 - b) case (ValueSet(values1), ValueSet(values2)) => ValueSet(values1.removedAll(values2)) @@ -715,27 +702,6 @@ class Objects(using Context @constructorOnly): case (a: Package, b: Package) if a == b => Bottom case _ => a - def widen(height: Int)(using Context): Value = log("widening value " + a.show + " down to height " + height, printer, (_: Value).show) { - if height == 0 then Top - else - a match - case Bottom => Bottom - - case ValueSet(values) => - values.map(ref => ref.widen(height)).join - - case Fun(code, thisV, klass, env) => - Fun(code, thisV.widenThisValue(height), klass, env.widen(height - 1)) - - 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 - } - def filterType(tpe: Type)(using Context): Value = tpe match case t @ SAMType(_, _) if a.isInstanceOf[Fun] => a // if tpe is SAMType and a is Fun, allow it @@ -751,7 +717,7 @@ class Objects(using Context @constructorOnly): else val klass = sym.asClass a match - case UnknownValue | Top => a + case UnknownValue => a case Package(packageModuleClass) => // the typer might mistakenly set the receiver to be a package instead of package object. // See pos/packageObjectStringInterpolator.scala @@ -759,19 +725,31 @@ class Objects(using Context @constructorOnly): case v: SafeValue => if v.typeSymbol.asClass.isSubClass(klass) then a else Bottom case ref: Ref => if ref.klass.isSubClass(klass) then ref else Bottom case ValueSet(values) => values.map(v => v.filterClass(klass)).join - case arr: OfArray => if defn.ArrayClass.isSubClass(klass) then arr else Bottom case fun: Fun => if klass.isOneOf(AbstractOrTrait) && klass.baseClasses.exists(defn.isFunctionClass) then fun else Bottom - extension (value: ThisValue) - def widenThisValue(height : Int)(using Context) : ThisValue = - assert(height > 0, "Cannot call widenThisValue with height 0!") - value.widen(height).asInstanceOf[ThisValue] + given Join[ScopeSet] with + extension (a: ScopeSet) + def join(b: ScopeSet): ScopeSet = ScopeSet(a.scopes ++ b.scopes) extension (values: Iterable[Value]) - def join: Value = if values.isEmpty then Bottom else values.reduce { (v1, v2) => v1.join(v2) } + 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 + extension (scopes: Iterable[ScopeSet]) + def join: ScopeSet = + if scopes.isEmpty then + Env.NoEnv + else + scopes.reduce { (s1, s2) => s1.join(s2) } + + extension [V : Join](map: Map[Symbol, V]) + def join(sym: Symbol, value: V): Map[Symbol, V] = + if !map.contains(sym) then map.updated(sym, value) + else map.updated(sym, map(sym).join(value)) /** Check if the checker option reports warnings about unknown code */ @@ -795,9 +773,6 @@ class Objects(using Context @constructorOnly): */ def call(value: Value, meth: Symbol, args: List[ArgInfo], receiver: Type, superType: Type, needResolve: Boolean = true): Contextual[Value] = log("call " + meth.show + ", this = " + value.show + ", args = " + args.map(_.value.show), printer, (_: Value).show) { value.filterClass(meth.owner) match - case Top => - report.warning("Value is unknown to the checker due to widening. " + Trace.show, Trace.position) - Bottom case UnknownValue => reportWarningForUnknownValue("Using unknown value. " + Trace.show, Trace.position) @@ -842,7 +817,7 @@ class Objects(using Context @constructorOnly): val ddef = target.defTree.asInstanceOf[DefDef] val cls = target.owner.enclosingClass.asClass // convert SafeType to an OfClass before analyzing method body - val ref = OfClass(cls, Bottom, NoSymbol, Nil, Env.NoEnv) + val ref = OfClass(cls, Env.NoEnv, NoSymbol) call(ref, meth, args, receiver, superType, needResolve) case Bottom => @@ -856,24 +831,23 @@ class Objects(using Context @constructorOnly): 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) + if arr.owner == State.currentObject then + arr.readElement else - errorReadOtherStaticObject(State.currentObject, arr.addr) + errorReadOtherStaticObject(State.currentObject, arr) 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) + if arr.owner != State.currentObject then + errorMutateOtherStaticObject(State.currentObject, arr) else - Heap.writeJoin(arr.addr, args.tail.head.value) + arr.writeElement(args.tail.head.value) Bottom else // Array.length is OK SafeValue(defn.IntType) case ref: Ref => - val isLocal = !meth.owner.isClass val target = if !needResolve then meth @@ -887,7 +861,7 @@ class Objects(using Context @constructorOnly): if target.isOneOf(Flags.Method) then if target.owner == defn.ArrayModuleClass && target.name == nme.apply then val arr = OfArray(State.currentObject, summon[Regions.Data]) - Heap.writeJoin(arr.addr, args.map(_.value).join) + arr.writeElement(args.map(_.value).join) arr else if target.equals(defn.Predef_classOf) then // Predef.classOf is a stub method in tasty and is replaced in backend @@ -896,16 +870,17 @@ class Objects(using Context @constructorOnly): val cls = target.owner.enclosingClass.asClass val ddef = target.defTree.asInstanceOf[DefDef] val meth = ddef.symbol - val (thisV : ThisValue, outerEnv) = - if meth.owner.isClass then - (ref, Env.NoEnv) + if meth.owner.enclosingMethod == cls.primaryConstructor then + // meth is top-level method, outer is a ref + (ref, ScopeSet(Set(ref))) else - Env.resolveEnvByOwner(meth.owner.enclosingMethod, ref, summon[Env.Data]).getOrElse(Top -> Env.NoEnv) + val enclosingMethod = meth.owner.enclosingMethod + Env.resolveEnvByMethod(enclosingMethod, ref, summon[Scope]) val env2 = Env.ofDefDef(ddef, args.map(_.value), outerEnv) extendTrace(ddef) { - given Env.Data = env2 + given Scope = env2 cache.cachedEval(ref, ddef.rhs, cacheResult = true) { expr => Returns.installHandler(meth) val res = cases(expr, thisV, cls) @@ -934,7 +909,7 @@ class Objects(using Context @constructorOnly): code match case ddef: DefDef => if meth.name == nme.apply then - given Env.Data = Env.ofDefDef(ddef, args.map(_.value), env) + given Scope = Env.ofDefDef(ddef, args.map(_.value), ScopeSet(Set(env))) extendTrace(code) { eval(ddef.rhs, thisV, klass, cacheResult = true) } else // The methods defined in `Any` and `AnyRef` are trivial and don't affect initialization. @@ -968,11 +943,13 @@ class Objects(using Context @constructorOnly): 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.ofDefDef(ddef, argValues, Env.NoEnv) + given Scope = ref if ctor.isPrimaryConstructor then val tpl = cls.defTree.asInstanceOf[TypeDef].rhs.asInstanceOf[Template] + val params = tpl.constr.termParamss.flatten.map(_.symbol) + val paramMap = params.zip(args.map(_.value)) + paramMap.foreach(ref.initVal(_, _)) extendTrace(cls.defTree) { eval(tpl, ref, cls, cacheResult = true) } else extendTrace(ddef) { // The return values for secondary constructors can be ignored @@ -985,6 +962,9 @@ class Objects(using Context @constructorOnly): // no source code available UnknownValue + case ValueSet(values) if values.size == 1 => + callConstructor(values.head, ctor, args) + case _ => report.warning("[Internal error] unexpected constructor call, meth = " + ctor + ", this = " + value + Trace.show, Trace.position) Bottom @@ -999,12 +979,13 @@ class Objects(using Context @constructorOnly): */ def select(value: Value, field: Symbol, receiver: Type, needResolve: Boolean = true): Contextual[Value] = log("select " + field.show + ", this = " + value.show, printer, (_: Value).show) { value.filterClass(field.owner) match - case Top => - report.warning("Value is unknown to the checker due to widening. " + Trace.show, Trace.position) - Bottom case UnknownValue => reportWarningForUnknownValue("Using unknown value. " + Trace.show, Trace.position) + case arr: OfArray => + report.warning("[Internal error] unexpected tree in selecting an array, array = " + arr.show + Trace.show, Trace.position) + Bottom + case v @ SafeValue(_) => if v.typeSymbol != defn.NullClass then // selection on Null is sensible on AST level; no warning for it @@ -1024,7 +1005,7 @@ class Objects(using Context @constructorOnly): 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) + given Scope = Env.emptyEnv(target.owner.asInstanceOf[ClassSymbol].primaryConstructor) if target.hasSource then val rhs = target.defTree.asInstanceOf[ValDef].rhs eval(rhs, ref, target.owner.asClass, cacheResult = true) @@ -1034,11 +1015,10 @@ class Objects(using Context @constructorOnly): def isNextFieldOfColonColon: Boolean = ref.klass == defn.ConsClass && target.name.toString == "next" if target.isMutableVarOrAccessor && !isNextFieldOfColonColon then if ref.hasVar(target) then - val addr = ref.varAddr(target) - if addr.owner == State.currentObject then - Heap.read(addr) + if ref.owner == State.currentObject then + ref.varValue(target) else - errorReadOtherStaticObject(State.currentObject, addr) + errorReadOtherStaticObject(State.currentObject, ref) Bottom else if ref.isObjectRef && ref.klass.hasSource then report.warning("Access uninitialized field " + field.show + ". " + Trace.show, Trace.position) @@ -1068,10 +1048,6 @@ class Objects(using Context @constructorOnly): report.warning("[Internal error] unexpected tree in selecting a function, fun = " + fun.code.show + Trace.show, fun.code) Bottom - case arr: OfArray => - report.warning("[Internal error] unexpected tree in selecting an array, array = " + arr.show + Trace.show, Trace.position) - Bottom - case Bottom => Bottom case ValueSet(values) => @@ -1087,8 +1063,6 @@ class Objects(using Context @constructorOnly): */ 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.filterClass(field.owner) match - case Top => - report.warning("Value is unknown to the checker due to widening. " + Trace.show, Trace.position) case UnknownValue => val _ = reportWarningForUnknownValue("Assigning to unknown value. " + Trace.show, Trace.position) case p: Package => @@ -1106,11 +1080,10 @@ class Objects(using Context @constructorOnly): case ref: Ref => if ref.hasVar(field) then - val addr = ref.varAddr(field) - if addr.owner != State.currentObject then - errorMutateOtherStaticObject(State.currentObject, addr) + if ref.owner != State.currentObject then + errorMutateOtherStaticObject(State.currentObject, ref) else - Heap.writeJoin(addr, rhs) + Heap.writeJoinVal(ref, field, rhs) else report.warning("Mutating a field before its initialization: " + field.show + ". " + Trace.show, Trace.position) end match @@ -1137,7 +1110,7 @@ class Objects(using Context @constructorOnly): case UnknownValue => reportWarningForUnknownValue("Instantiating when outer is unknown. " + Trace.show, Trace.position) - case outer: (Ref | Top.type | Package) => + case outer: (Ref | Package) => if klass == defn.ArrayClass then args.head.tree.tpe match case ConstantType(Constants.Constant(0)) => @@ -1145,26 +1118,29 @@ class Objects(using Context @constructorOnly): Bottom case _ => val arr = OfArray(State.currentObject, summon[Regions.Data]) - Heap.writeJoin(arr.addr, Bottom) arr else // Widen the outer to finitize the domain. Arguments already widened in `evalArgs`. - val (outerWidened, envWidened) = + val envWidened: ScopeSet = outer match case Package(_) => // For top-level classes - (outer, Env.NoEnv) - case thisV : ThisValue => - if klass.owner.isClass then - if klass.owner.is(Flags.Package) then - report.warning("[Internal error] top-level class should have `Package` as outer, class = " + klass.show + ", outer = " + outer.show + ", " + Trace.show, Trace.position) - (Bottom, Env.NoEnv) - else - (thisV.widenThisValue(1), Env.NoEnv) + Env.NoEnv + case outer : ThisValue => + if klass.owner.is(Flags.Package) then + report.warning("[Internal error] top-level class should have `Package` as outer, class = " + klass.show + ", outer = " + outer.show + ", " + Trace.show, Trace.position) + Env.NoEnv else - // klass.enclosingMethod returns its primary constructor - Env.resolveEnvByOwner(klass.owner.enclosingMethod, thisV, summon[Env.Data]).getOrElse(UnknownValue -> Env.NoEnv) + val outerCls = klass.owner.enclosingClass.asClass + // When `klass` is directly nested in `outerCls`, `outerCls`.enclosingMethod returns its primary constructor + if klass.owner.enclosingMethod == outerCls.primaryConstructor then + // Don't use the parameter `outer` as the outer value, but uses `outerCls.this` + // This eliminates infinite outer chain caused by inner classes extending outer classes. + // See `inner-extends-outer.scala` + resolveThis(outerCls, outer).toScopeSet + else + Env.resolveEnvByMethod(klass.owner.enclosingMethod, outer, summon[Scope])._2 - val instance = OfClass(klass, outerWidened, ctor, args.map(_.value), envWidened) + val instance = OfClass(klass, envWidened, ctor) callConstructor(instance, ctor, args) case ValueSet(values) => @@ -1178,9 +1154,7 @@ class Objects(using Context @constructorOnly): */ def initLocal(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.writeJoin(addr, value) + Env.setLocalVar(sym, value) else Env.setLocalVal(sym, value) } @@ -1192,46 +1166,42 @@ class Objects(using Context @constructorOnly): */ def readLocal(thisV: ThisValue, sym: Symbol): Contextual[Value] = log("reading local " + sym.show, printer, (_: Value).show) { def isByNameParam(sym: Symbol) = sym.is(Flags.Param) && sym.info.isInstanceOf[ExprType] + def evalByNameParam(value: Value): Contextual[Value] = value match + case fun: Fun => + given Scope = Env.ofByName(sym, fun.scope) + eval(fun.code, fun.thisV, fun.klass) + case UnknownValue => + reportWarningForUnknownValue("Calling on unknown value. " + Trace.show, Trace.position) + case Bottom => Bottom + case ValueSet(values) if values.size == 1 => + evalByNameParam(values.head) + case _: ValueSet | _: Ref | _: OfArray | _: Package | SafeValue(_) => + report.warning("[Internal error] Unexpected by-name value " + value.show + ". " + Trace.show, Trace.position) + Bottom + end evalByNameParam + // Can't use enclosingMethod here because values defined in a by-name closure will have the wrong enclosingMethod, // since our phase is before elimByName. - Env.resolveEnvByValue(sym, thisV, summon[Env.Data]) match - case Some(thisV -> env) => + Env.resolveEnvByValue(sym, thisV, summon[Scope]) match + case Some(thisV -> scopeSet) => if sym.is(Flags.Mutable) then // Assume forward reference check is doing a good job - given Env.Data = env - Env.getVar(sym) match - case Some(addr) => - if addr.owner == State.currentObject then - Heap.read(addr) - else - errorReadOtherStaticObject(State.currentObject, addr) - Bottom - end if - case _ => - // Only vals can be lazy - report.warning("[Internal error] Variable not found " + sym.show + "\nenv = " + env.show + ". " + Trace.show, Trace.position) + val scopesOwnedByOthers = scopeSet.scopes.filter(_.owner != State.currentObject) + if scopesOwnedByOthers.isEmpty then + scopeSet.lookupSymbol(sym) + else + errorReadOtherStaticObject(State.currentObject, scopesOwnedByOthers.head) Bottom + end if else - given Env.Data = env if sym.is(Flags.Lazy) then val rhs = sym.defTree.asInstanceOf[ValDef].rhs eval(rhs, thisV, sym.enclosingClass.asClass, cacheResult = true) else // Assume forward reference check is doing a good job - val value = Env.valValue(sym) + val value = scopeSet.lookupSymbol(sym) if isByNameParam(sym) then - value match - case fun: Fun => - given Env.Data = Env.ofByName(sym, fun.env) - eval(fun.code, fun.thisV, fun.klass) - case UnknownValue => - reportWarningForUnknownValue("Calling on unknown value. " + Trace.show, Trace.position) - case Top => - report.warning("Calling on value lost due to widening. " + Trace.show, Trace.position) - Bottom - case _: ValueSet | _: Ref | _: OfArray | _: Package | SafeValue(_) => - report.warning("[Internal error] Unexpected by-name value " + value.show + ". " + Trace.show, Trace.position) - Bottom + evalByNameParam(value) else value @@ -1253,17 +1223,13 @@ class Objects(using Context @constructorOnly): assert(sym.is(Flags.Mutable), "Writing to immutable variable " + sym.show) // Can't use enclosingMethod here because values defined in a by-name closure will have the wrong enclosingMethod, // since our phase is before elimByName. - Env.resolveEnvByValue(sym, thisV, summon[Env.Data]) match - case Some(thisV -> env) => - given Env.Data = env - Env.getVar(sym) match - case Some(addr) => - if addr.owner != State.currentObject then - errorMutateOtherStaticObject(State.currentObject, addr) - else - Heap.writeJoin(addr, value) - case _ => - report.warning("[Internal error] Variable not found " + sym.show + "\nenv = " + env.show + ". " + Trace.show, Trace.position) + Env.resolveEnvByValue(sym, thisV, summon[Scope]) match + case Some(thisV -> scopeSet) => + val scopesOwnedByOthers = scopeSet.scopes.filter(_.owner != State.currentObject) + if !scopesOwnedByOthers.isEmpty then + errorMutateOtherStaticObject(State.currentObject, scopesOwnedByOthers.head) + else + scopeSet.scopes.foreach(Heap.writeJoinVal(_, sym, value)) case _ => report.warning("Assigning to variables in outer scope. " + Trace.show, Trace.position) @@ -1274,7 +1240,7 @@ class Objects(using Context @constructorOnly): // -------------------------------- algorithm -------------------------------- /** Check an individual object */ - private def accessObject(classSym: ClassSymbol)(using Context, State.Data, Trace): ObjectRef = log("accessing " + classSym.show, printer, (_: Value).show) { + private def accessObject(classSym: ClassSymbol)(using Context, State.Data, Trace, Heap.MutableData): ObjectRef = log("accessing " + classSym.show, printer, (_: Value).show) { if classSym.hasSource then State.checkObjectAccess(classSym) else @@ -1285,6 +1251,7 @@ class Objects(using Context @constructorOnly): def checkClasses(classes: List[ClassSymbol])(using Context): Unit = given State.Data = new State.Data given Trace = Trace.empty + given Heap.MutableData = Heap.empty // TODO: do garbage collection on the heap for classSym <- classes if classSym.isStaticObject @@ -1393,7 +1360,7 @@ class Objects(using Context @constructorOnly): case TermRef(NoPrefix, _) => // resolve this for the local method val enclosingClass = id.symbol.owner.enclosingClass.asClass - val thisValue2 = extendTrace(ref) { resolveThis(enclosingClass, thisV, klass) } + val thisValue2 = extendTrace(ref) { resolveThis(enclosingClass, thisV) } // 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, _) => @@ -1410,7 +1377,7 @@ class Objects(using Context @constructorOnly): case OuterSelectName(_, _) => val current = qualifier.tpe.classSymbol val target = expr.tpe.widenSingleton.classSymbol.asClass - withTrace(trace2) { resolveThis(target, qual, current.asClass) } + withTrace(trace2) { resolveThis(target, qual) } case _ => withTrace(trace2) { select(qual, expr.symbol, receiver = qualifier.tpe) } @@ -1444,18 +1411,17 @@ class Objects(using Context @constructorOnly): extendTrace(id) { evalType(prefix, thisV, klass) } val value = eval(rhs, thisV, klass) - val widened = widenEscapedValue(value, rhs) if isLocal then - writeLocal(thisV, lhs.symbol, widened) + writeLocal(thisV, lhs.symbol, value) else - withTrace(trace2) { assign(receiver, lhs.symbol, widened, rhs.tpe) } + withTrace(trace2) { assign(receiver, lhs.symbol, value, rhs.tpe) } case closureDef(ddef) => - Fun(ddef, thisV, klass, summon[Env.Data]) + Fun(ddef, thisV, klass, summon[Scope]) case PolyFun(ddef) => - Fun(ddef, thisV, klass, summon[Env.Data]) + Fun(ddef, thisV, klass, summon[Scope]) case Block(stats, expr) => evalExprs(stats, thisV, klass) @@ -1499,7 +1465,7 @@ class Objects(using Context @constructorOnly): val module = defn.getWrapVarargsArrayModule.moduleClass.asClass val args = evalArgs(elems.map(Arg.apply), thisV, klass) val arr = OfArray(State.currentObject, summon[Regions.Data]) - Heap.writeJoin(arr.addr, args.map(_.value).join) + arr.writeElement(args.map(_.value).join) call(ObjectRef(module), meth, List(ArgInfo(arr, summon[Trace], EmptyTree)), module.typeRef, NoType) case Inlined(call, bindings, expansion) => @@ -1577,6 +1543,7 @@ class Objects(using Context @constructorOnly): case bind @ Bind(_, pat) => val (tpe, value) = evalPattern(scrutinee, pat) + initLocal(bind.symbol, value) (tpe, value) @@ -1803,54 +1770,23 @@ class Objects(using Context @constructorOnly): accessObject(sym.moduleClass.asClass) else - resolveThis(tref.classSymbol.asClass, thisV, klass) + resolveThis(tref.classSymbol.asClass, thisV) case _ => throw new Exception("unexpected type: " + tp + ", Trace:\n" + Trace.show) } - /** Widen the escaped value (a method argument or rhs of an assignment) - * - * The default widening is 1 for most values, 2 for function values. - * User-specified widening annotations are repected. - */ - def widenEscapedValue(value: Value, annotatedTree: Tree): Contextual[Value] = - def parseAnnotation: Option[Int] = - annotatedTree.tpe.getAnnotation(defn.InitWidenAnnot).flatMap: 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) - None - else - Some(height) - case arg => - report.warning("The argument should be a constant integer value", arg) - None - end parseAnnotation - - parseAnnotation match - case Some(i) => - value.widen(i) - - case None => - if value.isInstanceOf[Fun] - then value.widen(2) - else value.widen(1) - /** Evaluate arguments of methods and constructors */ def evalArgs(args: List[Arg], thisV: ThisValue, 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]) + Fun(arg.tree, thisV, klass, summon[Scope]) else eval(arg.tree, thisV, klass) - val widened = widenEscapedValue(res, arg.tree) - argInfos += ArgInfo(widened, trace.add(arg.tree), arg.tree) + argInfos += ArgInfo(res, trace.add(arg.tree), arg.tree) } argInfos.toList @@ -1869,9 +1805,7 @@ class Objects(using Context @constructorOnly): 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.writeJoin(addr, value) + thisV.initVar(acc, value) else thisV.initVal(acc, value) printer.println(acc.show + " initialized with " + value) @@ -1884,7 +1818,17 @@ class Objects(using Context @constructorOnly): val cls = tref.classSymbol.asClass // update outer for super class val res = outerValue(tref, thisV, klass) - thisV.initOuter(cls, res) + res match { + case ref: Ref => thisV.initOuter(cls, ScopeSet(Set(ref))) + case vs: ValueSet if vs.isRefSet => + thisV.initOuter(cls, vs.toScopeSet) + case _: Package => + thisV.initOuter(cls, Env.NoEnv) + case _ => + val error = "[Internal error] Invalid outer value, cls = " + cls + ", value = " + res + Trace.show + report.warning(error, Trace.position) + return + } // follow constructor if cls.hasSource then @@ -1895,7 +1839,7 @@ class Objects(using Context @constructorOnly): } // parents - def initParent(parent: Tree, tasks: Tasks) = + def initParent(parent: Tree, tasks: Tasks) = // TODO: store the parent objects and resolve `p.this` for parent classes `p` parent match case tree @ Block(stats, NewExpr(tref, New(tpt), ctor, argss)) => // can happen evalExprs(stats, thisV, klass) @@ -1964,9 +1908,7 @@ class Objects(using Context @constructorOnly): val sym = vdef.symbol val res = if (allowList.contains(sym)) Bottom else eval(vdef.rhs, thisV, klass) if sym.is(Flags.Mutable) then - val addr = Heap.fieldVarAddr(summon[Regions.Data], sym, State.currentObject) - thisV.initVar(sym, addr) - Heap.writeJoin(addr, res) + thisV.initVar(sym, res) else thisV.initVal(sym, res) @@ -1980,42 +1922,52 @@ class Objects(using Context @constructorOnly): } - /** Resolve C.this that appear in `klass` + /** Resolve C.this by recursively searching through the outer chain + * @param target The class symbol for `C` for which `C.this` is to be resolved. + * @param scopeSet The scopes as the starting point. + */ + def resolveThisRecur(target: ClassSymbol, scopeSet: ScopeSet): Contextual[ValueSet] = + if scopeSet == Env.NoEnv then + Bottom + else + val head = scopeSet.scopes.head + if head.isInstanceOf[Ref] then + val klass = head.asInstanceOf[Ref].klass + assert(scopeSet.scopes.forall(_.asInstanceOf[Ref].klass == klass), "Multiple possible outer class?") + if klass == target then + scopeSet.toValueSet + else + resolveThisRecur(target, scopeSet.outers) + else + resolveThisRecur(target, scopeSet.outers) + + /** Resolve C.this that appear in `D.this` * * @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 thisV The value for `D.this`. * @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 - Package(target) // TODO: What is the semantics for package.this? + def resolveThis(target: ClassSymbol, thisV: Value, elideObjectAccess: Boolean = false): Contextual[ValueSet] = log("resolveThis target = " + target.show + ", this = " + thisV.show, printer, (_: Value).show) { + if target.is(Flags.Package) then + val error = "[Internal error] target cannot be packages, target = " + target + Trace.show + report.warning(error, Trace.position) + Bottom else if target.isStaticObject then val res = ObjectRef(target.moduleClass.asClass) - if elideObjectAccess then res - else accessObject(target) + if elideObjectAccess then ValueSet(Set(res)) + else ValueSet(Set(accessObject(target))) else thisV match case Bottom => Bottom - case UnknownValue => UnknownValue - case Top => Top 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 ValueSet(values) => - values.map(ref => resolveThis(target, ref, klass)).join - case _: Fun | _ : OfArray | _: Package | SafeValue(_) => - report.warning("[Internal error] unexpected thisV = " + thisV + ", target = " + target.show + ", klass = " + klass.show + Trace.show, Trace.position) + resolveThisRecur(target, ScopeSet(Set(ref))) + case vs: ValueSet if vs.isRefSet => + resolveThisRecur(target, vs.toScopeSet) + case _ => + report.warning("[Internal error] unexpected thisV = " + thisV + ", target = " + target.show + Trace.show, Trace.position) Bottom } @@ -2029,7 +1981,7 @@ class Objects(using Context @constructorOnly): val cls = tref.classSymbol.asClass if tref.prefix == NoPrefix then val enclosing = cls.owner.lexicallyEnclosingClass.asClass - resolveThis(enclosing, thisV, klass, elideObjectAccess = cls.isStatic) + resolveThis(enclosing, thisV, elideObjectAccess = cls.isStatic) else if cls.isAllOf(Flags.JavaInterface) then Bottom else evalType(tref.prefix, thisV, klass, elideObjectAccess = cls.isStatic) @@ -2040,25 +1992,25 @@ class Objects(using Context @constructorOnly): else "" val mutateErrorSet: mutable.Set[(ClassSymbol, ClassSymbol)] = mutable.Set.empty - def errorMutateOtherStaticObject(currentObj: ClassSymbol, addr: Heap.Addr)(using Trace, Context) = - val otherObj = addr.owner - val addr_trace = addr.getTrace + def errorMutateOtherStaticObject(currentObj: ClassSymbol, scope: Scope)(using Trace, Context) = + val otherObj = scope.owner + val scope_trace = scope.getTrace if mutateErrorSet.add((currentObj, otherObj)) then 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. " + Trace.show + - printTraceWhenMultiple(addr_trace) + printTraceWhenMultiple(scope_trace) report.warning(msg, Trace.position) val readErrorSet: mutable.Set[(ClassSymbol, ClassSymbol)] = mutable.Set.empty - def errorReadOtherStaticObject(currentObj: ClassSymbol, addr: Heap.Addr)(using Trace, Context) = - val otherObj = addr.owner - val addr_trace = addr.getTrace + def errorReadOtherStaticObject(currentObj: ClassSymbol, scope: Scope)(using Trace, Context) = + val otherObj = scope.owner + val scope_trace = scope.getTrace if readErrorSet.add((currentObj, otherObj)) then 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. " + Trace.show + - printTraceWhenMultiple(addr_trace) + printTraceWhenMultiple(scope_trace) report.warning(msg, Trace.position) diff --git a/compiler/test/dotc/neg-init-global-scala2-library-tasty.excludelist b/compiler/test/dotc/neg-init-global-scala2-library-tasty.excludelist index 18a665e0119b..8c2a1d6ce5f4 100644 --- a/compiler/test/dotc/neg-init-global-scala2-library-tasty.excludelist +++ b/compiler/test/dotc/neg-init-global-scala2-library-tasty.excludelist @@ -15,4 +15,6 @@ global-list.scala t5366.scala mutable-read7.scala t9115.scala -Color.scala \ No newline at end of file +Color.scala +unapplySeq-implicit-arg2.scala +unapplySeq-implicit-arg3.scala \ No newline at end of file diff --git a/tests/init-global/pos/cyclic-param.scala b/tests/init-global/pos/cyclic-param.scala new file mode 100644 index 000000000000..af9ce29d2e53 --- /dev/null +++ b/tests/init-global/pos/cyclic-param.scala @@ -0,0 +1,7 @@ +class B {} + +class C(c: B) extends B + +object O: + def f(param: B): Int = f(new C(param)) + val a = f(new B) \ No newline at end of file diff --git a/tests/init-global/pos/get-local-val-from-other-owner.scala b/tests/init-global/pos/get-local-val-from-other-owner.scala new file mode 100644 index 000000000000..f16c4989895e --- /dev/null +++ b/tests/init-global/pos/get-local-val-from-other-owner.scala @@ -0,0 +1,9 @@ +object O: + val a = m(2) + def m(f: Int) = + val a = f + () => a + +object O2: + val func = O.a + val a = func() \ No newline at end of file diff --git a/tests/init-global/pos/global-recursion3.scala b/tests/init-global/pos/global-recursion3.scala new file mode 100644 index 000000000000..84fee697d5e8 --- /dev/null +++ b/tests/init-global/pos/global-recursion3.scala @@ -0,0 +1,4 @@ +class A(val a: A) +object B: + val a: A = loop().a + def loop(): A = new A(loop()) \ No newline at end of file diff --git a/tests/init-global/pos/inner-extends-outer.scala b/tests/init-global/pos/inner-extends-outer.scala new file mode 100644 index 000000000000..f353f66c5796 --- /dev/null +++ b/tests/init-global/pos/inner-extends-outer.scala @@ -0,0 +1,14 @@ +class Outer { + val f = 5 + class Inner extends Outer { + val g = Outer.this.f + } +} + +object O { + def foo(i: Outer): Unit = + val i2 = new i.Inner // i2.outer should always be OfClass(Outer) + foo(i2) + + foo(new Outer) +} diff --git a/tests/init-global/pos/local-class.scala b/tests/init-global/pos/local-class.scala new file mode 100644 index 000000000000..0c5e89c7b63b --- /dev/null +++ b/tests/init-global/pos/local-class.scala @@ -0,0 +1,22 @@ +class Outer { + def foo = { + val y = 5 + class C { + val x = y + } + class D { + new C + } + + new D + } + + foo + + val n = 10 // warn +} + +object O { + val c = new Outer + val d: Object = c.foo +} \ No newline at end of file diff --git a/tests/init-global/warn/widen.scala b/tests/init-global/pos/lookup-outer.scala similarity index 91% rename from tests/init-global/warn/widen.scala rename to tests/init-global/pos/lookup-outer.scala index 157434a0f3e4..390030968e33 100644 --- a/tests/init-global/warn/widen.scala +++ b/tests/init-global/pos/lookup-outer.scala @@ -10,7 +10,7 @@ object O: def bar(t: T) = { class A { class B { - t.foo() // warn + t.foo() } val b = new B diff --git a/tests/init-global/warn/global-cycle6.check b/tests/init-global/warn/global-cycle6.check index b6454a5d2cce..68f07bad32e9 100644 --- a/tests/init-global/warn/global-cycle6.check +++ b/tests/init-global/warn/global-cycle6.check @@ -8,12 +8,8 @@ | │ ^ | ├── object B { [ global-cycle6.scala:8 ] | │ ^ - | ├── val a = new A.Inner [ global-cycle6.scala:9 ] - | │ ^^^^^^^^^^^ - | ├── class Inner { [ global-cycle6.scala:3 ] - | │ ^ - | └── println(n) // warn [ global-cycle6.scala:4 ] - | ^ + | └── val a = new A.Inner [ global-cycle6.scala:9 ] + | ^^^^^^^^^^^ -- Warning: tests/init-global/warn/global-cycle6.scala:4:12 ------------------------------------------------------------ 4 | println(n) // warn | ^ @@ -26,3 +22,15 @@ | │ ^ | └── println(n) // warn [ global-cycle6.scala:4 ] | ^ +-- Warning: tests/init-global/warn/global-cycle6.scala:14:9 ------------------------------------------------------------ +14 | object A { // warn + | ^ + | Cyclic initialization: object A -> object B -> object A. Calling trace: + | ├── object A { // warn [ global-cycle6.scala:14 ] + | │ ^ + | ├── val n: Int = B.m [ global-cycle6.scala:15 ] + | │ ^ + | ├── object B { [ global-cycle6.scala:21 ] + | │ ^ + | └── val a = new A.Inner [ global-cycle6.scala:22 ] + | ^^^^^^^^^^^ diff --git a/tests/init-global/warn/global-cycle6.scala b/tests/init-global/warn/global-cycle6.scala index 030e08d37117..de7a2b910d62 100644 --- a/tests/init-global/warn/global-cycle6.scala +++ b/tests/init-global/warn/global-cycle6.scala @@ -11,7 +11,7 @@ object B { } object O { - object A { + object A { // warn val n: Int = B.m class Inner { val x: Int = 4 diff --git a/tests/init-global/warn/global-irrelevance3.check b/tests/init-global/warn/global-irrelevance3.check index dba945224f97..9b6f8c539bf7 100644 --- a/tests/init-global/warn/global-irrelevance3.check +++ b/tests/init-global/warn/global-irrelevance3.check @@ -9,3 +9,8 @@ |│ ^^^^^^^ |└── (() => x) // warn [ global-irrelevance3.scala:9 ] | ^ + |The mutable state is created through: + |├── object A: [ global-irrelevance3.scala:1 ] + |│ ^ + |└── val p: Pair = foo() [ global-irrelevance3.scala:3 ] + | ^^^^^ diff --git a/tests/init-global/warn/global-irrelevance4.check b/tests/init-global/warn/global-irrelevance4.check index 7253e5b39688..1862ae3e1986 100644 --- a/tests/init-global/warn/global-irrelevance4.check +++ b/tests/init-global/warn/global-irrelevance4.check @@ -9,3 +9,8 @@ | │ ^^^^^^^^^ | └── (y => x = y), // warn [ global-irrelevance4.scala:8 ] | ^^^^^^^^^^ + | The mutable state is created through: + | ├── object A: [ global-irrelevance4.scala:1 ] + | │ ^ + | └── val p: Pair = foo() [ global-irrelevance4.scala:3 ] + | ^^^^^ diff --git a/tests/init-global/warn/mutable-array.check b/tests/init-global/warn/mutable-array.check index 1bc9146ceb4d..b2faa897162f 100644 --- a/tests/init-global/warn/mutable-array.check +++ b/tests/init-global/warn/mutable-array.check @@ -10,7 +10,5 @@ |The mutable state is created through: |├── object A: [ mutable-array.scala:1 ] |│ ^ - |├── val box: Box = new Box(0) [ mutable-array.scala:3 ] - |│ ^^^^^^^^^^ - |└── class Box(var value: Int) [ mutable-array.scala:2 ] - | ^^^^^^^^^^^^^^^^^^^^^^^^^ + |└── val box: Box = new Box(0) [ mutable-array.scala:3 ] + | ^^^^^^^^^^ diff --git a/tests/init-global/warn/mutable-read1.check b/tests/init-global/warn/mutable-read1.check index 4862b4399de4..0327203208f2 100644 --- a/tests/init-global/warn/mutable-read1.check +++ b/tests/init-global/warn/mutable-read1.check @@ -10,7 +10,5 @@ |The mutable state is created through: |├── object A: [ mutable-read1.scala:3 ] |│ ^ - |├── val box: Box = new Box(4) [ mutable-read1.scala:4 ] - |│ ^^^^^^^^^^ - |└── class Box(var value: Int) [ mutable-read1.scala:1 ] - | ^^^^^^^^^^^^^^^^^^^^^^^^^ + |└── val box: Box = new Box(4) [ mutable-read1.scala:4 ] + | ^^^^^^^^^^ diff --git a/tests/init-global/warn/mutable-read2.check b/tests/init-global/warn/mutable-read2.check index 7339e76672ef..368137ea6ba3 100644 --- a/tests/init-global/warn/mutable-read2.check +++ b/tests/init-global/warn/mutable-read2.check @@ -10,7 +10,5 @@ |The mutable state is created through: |├── object A: [ mutable-read2.scala:1 ] |│ ^ - |├── val box: Box = new Box(0) [ mutable-read2.scala:5 ] - |│ ^^^^^^^^^^ - |└── class Box(var value: Int) { [ mutable-read2.scala:2 ] - | ^ + |└── val box: Box = new Box(0) [ mutable-read2.scala:5 ] + | ^^^^^^^^^^ diff --git a/tests/init-global/warn/mutable-read3.check b/tests/init-global/warn/mutable-read3.check index ecaaac65994b..6ebc71818665 100644 --- a/tests/init-global/warn/mutable-read3.check +++ b/tests/init-global/warn/mutable-read3.check @@ -10,7 +10,5 @@ |The mutable state is created through: |├── object A: [ mutable-read3.scala:1 ] |│ ^ - |├── val box: Box = new Box(0) [ mutable-read3.scala:3 ] - |│ ^^^^^^^^^^ - |└── class Box(var value: Int) [ mutable-read3.scala:2 ] - | ^^^^^^^^^^^^^^^^^^^^^^^^^ + |└── val box: Box = new Box(0) [ mutable-read3.scala:3 ] + | ^^^^^^^^^^ diff --git a/tests/init-global/warn/mutable-read4.check b/tests/init-global/warn/mutable-read4.check index e66fd8235a61..c77249b90a89 100644 --- a/tests/init-global/warn/mutable-read4.check +++ b/tests/init-global/warn/mutable-read4.check @@ -10,7 +10,5 @@ |The mutable state is created through: |├── object A: [ mutable-read4.scala:3 ] |│ ^ - |├── val box: Box = new Box(4) [ mutable-read4.scala:4 ] - |│ ^^^^^^^^^^ - |└── class Box(var value: Int) [ mutable-read4.scala:1 ] - | ^^^^^^^^^^^^^^^^^^^^^^^^^ + |└── val box: Box = new Box(4) [ mutable-read4.scala:4 ] + | ^^^^^^^^^^ diff --git a/tests/init-global/warn/mutable-read6.check b/tests/init-global/warn/mutable-read6.check index 7a4798456acb..e6417b3eb327 100644 --- a/tests/init-global/warn/mutable-read6.check +++ b/tests/init-global/warn/mutable-read6.check @@ -12,7 +12,5 @@ |The mutable state is created through: |├── object Contexts: [ mutable-read6.scala:3 ] |│ ^ - |├── val NoContext: Context = new Context [ mutable-read6.scala:4 ] - |│ ^^^^^^^^^^^ - |└── class Context: [ mutable-read6.scala:5 ] - | ^ + |└── val NoContext: Context = new Context [ mutable-read6.scala:4 ] + | ^^^^^^^^^^^ diff --git a/tests/init-global/warn/unsoundness.check b/tests/init-global/warn/unsoundness.check new file mode 100644 index 000000000000..da27dc45ac1d --- /dev/null +++ b/tests/init-global/warn/unsoundness.check @@ -0,0 +1,20 @@ +-- Warning: tests/init-global/warn/unsoundness.scala:12:6 -------------------------------------------------------------- +12 | O.x // warn + | ^^^ + | Access uninitialized field value x. Calling trace: + | ├── object O: [ unsoundness.scala:15 ] + | │ ^ + | ├── f(if m > 5 then Box(A(3)) else Box(B(4))) [ unsoundness.scala:17 ] + | │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + | ├── def f(a: Box[Base[Int]]): Unit = [ unsoundness.scala:21 ] + | │ ^ + | ├── h(a.value) [ unsoundness.scala:22 ] + | │ ^^^^^^^^^^ + | ├── def h(a: Base[Int]): Unit = [ unsoundness.scala:24 ] + | │ ^ + | ├── a.update(10) [ unsoundness.scala:25 ] + | │ ^^^^^^^^^^^^ + | ├── def update(n: T) = [ unsoundness.scala:11 ] + | │ ^ + | └── O.x // warn [ unsoundness.scala:12 ] + | ^^^ diff --git a/tests/init-global/warn/unsoundness.scala b/tests/init-global/warn/unsoundness.scala new file mode 100644 index 000000000000..882c7e4d8e80 --- /dev/null +++ b/tests/init-global/warn/unsoundness.scala @@ -0,0 +1,25 @@ +class Box[T](val value: T) + +abstract class Base[T]: + def update(n: T): Unit + +class A[T](var a: T) extends Base[T]: + def update(n: T) = + a = n + +class B[T](var b: T) extends Base[T]: + def update(n: T) = + O.x // warn + b = n + +object O: + val m: Int = 3 + f(if m > 5 then Box(A(3)) else Box(B(4))) + + val x: Int = 10 + + def f(a: Box[Base[Int]]): Unit = + h(a.value) + + def h(a: Base[Int]): Unit = + a.update(10) \ No newline at end of file diff --git a/tests/init-global/warn/widen.check b/tests/init-global/warn/widen.check deleted file mode 100644 index b3191d023110..000000000000 --- a/tests/init-global/warn/widen.check +++ /dev/null @@ -1,20 +0,0 @@ --- Warning: tests/init-global/warn/widen.scala:13:13 ------------------------------------------------------------------- -13 | t.foo() // warn - | ^^^^^^^ - | Value is unknown to the checker due to widening. Calling trace: - | ├── object O: [ widen.scala:9 ] - | │ ^ - | ├── val a = bar(new C) [ widen.scala:20 ] - | │ ^^^^^^^^^^ - | ├── def bar(t: T) = { [ widen.scala:10 ] - | │ ^ - | ├── new A [ widen.scala:18 ] - | │ ^^^^^ - | ├── class A { [ widen.scala:11 ] - | │ ^ - | ├── val b = new B [ widen.scala:16 ] - | │ ^^^^^ - | ├── class B { [ widen.scala:12 ] - | │ ^ - | └── t.foo() // warn [ widen.scala:13 ] - | ^^^^^^^