From 637a5f6306448ddee63a06d121b661b2b936d89f Mon Sep 17 00:00:00 2001 From: EnzeXing Date: Thu, 21 Nov 2024 21:35:16 -0500 Subject: [PATCH 01/10] Refactor pattern matching, skipping cases when safe to do so --- .../tools/dotc/transform/init/Objects.scala | 56 +++++++++++++------ 1 file changed, 38 insertions(+), 18 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/transform/init/Objects.scala b/compiler/src/dotty/tools/dotc/transform/init/Objects.scala index c12139e508ee..be7fafba196f 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Objects.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Objects.scala @@ -644,6 +644,12 @@ class Objects(using Context @constructorOnly): case (ValueSet(values), b : ValueElement) => ValueSet(values + b) case (a : ValueElement, b : ValueElement) => ValueSet(ListSet(a, b)) + 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)) + case (a: Ref, b: Ref) if a.equals(b) => Bottom + case _ => a + def widen(height: Int)(using Context): Value = if height == 0 then Cold else @@ -1386,11 +1392,6 @@ class Objects(using Context @constructorOnly): def getMemberMethod(receiver: Type, name: TermName, tp: Type): Denotation = receiver.member(name).suchThat(receiver.memberInfo(_) <:< tp) - def evalCase(caseDef: CaseDef): Value = - evalPattern(scrutinee, caseDef.pat) - eval(caseDef.guard, thisV, klass) - eval(caseDef.body, thisV, klass) - /** Abstract evaluation of patterns. * * It augments the local environment for bound pattern variables. As symbols are globally @@ -1398,17 +1399,18 @@ class Objects(using Context @constructorOnly): * * Currently, we assume all cases are reachable, thus all patterns are assumed to match. */ - def evalPattern(scrutinee: Value, pat: Tree): Value = log("match " + scrutinee.show + " against " + pat.show, printer, (_: Value).show): + def evalPattern(scrutinee: Value, pat: Tree): (Type, Value) = log("match " + scrutinee.show + " against " + pat.show, printer, (_: (Type, Value))._2.show): val trace2 = Trace.trace.add(pat) pat match case Alternative(pats) => - for pat <- pats do evalPattern(scrutinee, pat) - scrutinee + val (types, values) = pats.map(evalPattern(scrutinee, _)).unzip() + val orType = types.fold(defn.NothingType)(OrType(_, _, false)) + (orType, values.join) case bind @ Bind(_, pat) => - val value = evalPattern(scrutinee, pat) + val (tpe, value) = evalPattern(scrutinee, pat) initLocal(bind.symbol, value) - scrutinee + (tpe, value) case UnApply(fun, implicits, pats) => given Trace = trace2 @@ -1417,6 +1419,10 @@ class Objects(using Context @constructorOnly): val funRef = fun1.tpe.asInstanceOf[TermRef] val unapplyResTp = funRef.widen.finalResultType + val receiverType = fun1 match + case ident: Ident => funRef.prefix + case select: Select => select.qualifier.tpe + val receiver = fun1 match case ident: Ident => evalType(funRef.prefix, thisV, klass) @@ -1505,17 +1511,18 @@ class Objects(using Context @constructorOnly): end if end if end if - scrutinee + (receiverType, scrutinee.filterType(receiverType)) case Ident(nme.WILDCARD) | Ident(nme.WILDCARD_STAR) => - scrutinee + (defn.ThrowableType, scrutinee) - case Typed(pat, _) => - evalPattern(scrutinee, pat) + case Typed(pat, typeTree) => + val (_, value) = evalPattern(scrutinee.filterType(typeTree.tpe), pat) + (typeTree.tpe, value) case tree => // For all other trees, the semantics is normal. - eval(tree, thisV, klass) + (defn.ThrowableType, eval(tree, thisV, klass)) end evalPattern @@ -1539,12 +1546,12 @@ class Objects(using Context @constructorOnly): if isWildcardStarArgList(pats) then if pats.size == 1 then // call .toSeq - val toSeqDenot = getMemberMethod(scrutineeType, nme.toSeq, toSeqType(elemType)) + val toSeqDenot = scrutineeType.member(nme.toSeq).suchThat(_.info.isParameterless) val toSeqRes = call(scrutinee, toSeqDenot.symbol, Nil, scrutineeType, superType = NoType, needResolve = true) evalPattern(toSeqRes, pats.head) else // call .drop - val dropDenot = getMemberMethod(scrutineeType, nme.drop, dropType(elemType)) + val dropDenot = getMemberMethod(scrutineeType, nme.drop, applyType(elemType)) val dropRes = call(scrutinee, dropDenot.symbol, ArgInfo(Bottom, summon[Trace], EmptyTree) :: Nil, scrutineeType, superType = NoType, needResolve = true) for pat <- pats.init do evalPattern(applyRes, pat) evalPattern(dropRes, pats.last) @@ -1555,8 +1562,21 @@ class Objects(using Context @constructorOnly): end if end evalSeqPatterns + def canSkipCase(remainingScrutinee: Value, catchValue: Value) = + (remainingScrutinee == Bottom && scrutinee != Bottom) || + (catchValue == Bottom && remainingScrutinee != Bottom) - cases.map(evalCase).join + var remainingScrutinee = scrutinee + val caseResults: mutable.ArrayBuffer[Value] = mutable.ArrayBuffer() + for caseDef <- cases do + val (tpe, value) = evalPattern(remainingScrutinee, caseDef.pat) + eval(caseDef.guard, thisV, klass) + if !canSkipCase(remainingScrutinee, value) then + caseResults.addOne(eval(caseDef.body, thisV, klass)) + if catchesAllOf(caseDef, tpe) then + remainingScrutinee = remainingScrutinee.remove(value) + + caseResults.join end patternMatch /** Handle semantics of leaf nodes From 7db959e133a71840ee1c587f12a12ea010ae0459 Mon Sep 17 00:00:00 2001 From: EnzeXing Date: Sat, 14 Dec 2024 12:39:28 -0500 Subject: [PATCH 02/10] Add BaseOrUnknownValue --- .../tools/dotc/transform/init/Objects.scala | 71 +++++++++++++------ 1 file changed, 49 insertions(+), 22 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/transform/init/Objects.scala b/compiler/src/dotty/tools/dotc/transform/init/Objects.scala index be7fafba196f..98fa1f52818a 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.* @@ -91,6 +92,7 @@ class Objects(using Context @constructorOnly): * ve ::= ObjectRef(class) // global object * | OfClass(class, vs[outer], ctor, args, env) // instance of a class * | OfArray(object[owner], regions) + * | BaseOrUnknownValue // Int, String, etc., and values without source * | Fun(..., env) // value elements that can be contained in ValueSet * vs ::= ValueSet(ve) // set of abstract values * Bottom ::= ValueSet(Empty) @@ -233,6 +235,11 @@ class Objects(using Context @constructorOnly): case class ValueSet(values: ListSet[ValueElement]) extends Value: def show(using Context) = values.map(_.show).mkString("[", ",", "]") + // Represents common base values like Int, String, etc. + // and also values loaded without source + case object BaseOrUnknownValue extends ValueElement: + def show(using Context): String = "BaseOrUnknownValue" + /** A cold alias which should not be used during initialization. * * Cold is not ValueElement since RefSet containing Cold is equivalent to Cold @@ -678,12 +685,15 @@ class Objects(using Context @constructorOnly): if baseClasses.isEmpty then a else filterClass(baseClasses.head) // could have called ClassSymbol, but it does not handle OrType and AndType + // Filter the value according to a class symbol, and only leaves the sub-values + // which could represent an object of the given class def filterClass(sym: Symbol)(using Context): Value = if !sym.isClass then a else val klass = sym.asClass a match case Cold => Cold + case BaseOrUnknownValue => BaseOrUnknownValue 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 @@ -716,6 +726,13 @@ class Objects(using Context @constructorOnly): case Bottom => Bottom + // Bottom arguments mean unreachable call + case _ if args.map(_.value).contains(Bottom) => + Bottom + + case BaseOrUnknownValue => + BaseOrUnknownValue + case arr: OfArray => val target = resolve(defn.ArrayClass, meth) @@ -734,7 +751,7 @@ class Objects(using Context @constructorOnly): Bottom else // Array.length is OK - Bottom + BaseOrUnknownValue case ref: Ref => val isLocal = !meth.owner.isClass @@ -755,7 +772,7 @@ class Objects(using Context @constructorOnly): arr else if target.equals(defn.Predef_classOf) then // Predef.classOf is a stub method in tasty and is replaced in backend - Bottom + BaseOrUnknownValue else if target.hasSource then val cls = target.owner.enclosingClass.asClass val ddef = target.defTree.asInstanceOf[DefDef] @@ -778,7 +795,7 @@ class Objects(using Context @constructorOnly): } } else - Bottom + BaseOrUnknownValue else if target.exists then select(ref, target, receiver, needResolve = false) else @@ -846,7 +863,7 @@ class Objects(using Context @constructorOnly): } else // no source code available - Bottom + BaseOrUnknownValue case _ => report.warning("[Internal error] unexpected constructor call, meth = " + ctor + ", this = " + value + Trace.show, Trace.position) @@ -866,6 +883,9 @@ class Objects(using Context @constructorOnly): report.warning("Using cold alias", Trace.position) Bottom + case BaseOrUnknownValue => + BaseOrUnknownValue + case ref: Ref => val target = if needResolve then resolve(ref.klass, field) else field if target.is(Flags.Lazy) then @@ -874,7 +894,7 @@ class Objects(using Context @constructorOnly): val rhs = target.defTree.asInstanceOf[ValDef].rhs eval(rhs, ref, target.owner.asClass, cacheResult = true) else - Bottom + BaseOrUnknownValue else if target.exists then def isNextFieldOfColonColon: Boolean = ref.klass == defn.ConsClass && target.name.toString == "next" if target.isOneOf(Flags.Mutable) && !isNextFieldOfColonColon then @@ -890,7 +910,7 @@ class Objects(using Context @constructorOnly): Bottom else // initialization error, reported by the initialization checker - Bottom + BaseOrUnknownValue else if ref.hasVal(target) then ref.valValue(target) else if ref.isObjectRef && ref.klass.hasSource then @@ -898,16 +918,16 @@ class Objects(using Context @constructorOnly): Bottom else // initialization error, reported by the initialization checker - Bottom + BaseOrUnknownValue 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 + // This is possible due to incorrect type cast or accessing standard library objects + // See tests/init/pos/Type.scala / tests/init/warn/unapplySeq-implicit-arg2.scala + BaseOrUnknownValue case fun: Fun => report.warning("[Internal error] unexpected tree in selecting a function, fun = " + fun.code.show + Trace.show, fun.code) @@ -917,7 +937,7 @@ class Objects(using Context @constructorOnly): report.warning("[Internal error] unexpected tree in selecting an array, array = " + arr.show + Trace.show, Trace.position) Bottom - case Bottom => + case Bottom => // TODO: add a value for packages? if field.isStaticObject then accessObject(field.moduleClass.asClass) else Bottom @@ -943,7 +963,7 @@ class Objects(using Context @constructorOnly): case Cold => report.warning("Assigning to cold aliases is forbidden. " + Trace.show, Trace.position) - case Bottom => + case BaseOrUnknownValue | Bottom => case ValueSet(values) => values.foreach(ref => assign(ref, field, rhs, rhsTyp)) @@ -978,6 +998,9 @@ class Objects(using Context @constructorOnly): report.warning("[Internal error] unexpected outer in instantiating a class, outer = " + outer.show + ", class = " + klass.show + ", " + Trace.show, Trace.position) Bottom + case BaseOrUnknownValue => + BaseOrUnknownValue + case outer: (Ref | Cold.type | Bottom.type) => if klass == defn.ArrayClass then args.head.tree.tpe match @@ -1068,6 +1091,7 @@ class Objects(using Context @constructorOnly): case Cold => report.warning("Calling cold by-name alias. " + Trace.show, Trace.position) Bottom + case BaseOrUnknownValue => BaseOrUnknownValue case _: ValueSet | _: Ref | _: OfArray => report.warning("[Internal error] Unexpected by-name value " + value.show + ". " + Trace.show, Trace.position) Bottom @@ -1257,7 +1281,7 @@ class Objects(using Context @constructorOnly): evalType(expr.tpe, thisV, klass) case Literal(_) => - Bottom + BaseOrUnknownValue case Typed(expr, tpt) => if tpt.tpe.hasAnnotation(defn.UncheckedAnnot) then @@ -1511,7 +1535,9 @@ class Objects(using Context @constructorOnly): end if end if end if - (receiverType, scrutinee.filterType(receiverType)) + // TODO: receiverType is the companion object type, not the class itself; + // cannot filter scritunee by this type + (receiverType, scrutinee) case Ident(nme.WILDCARD) | Ident(nme.WILDCARD_STAR) => (defn.ThrowableType, scrutinee) @@ -1533,7 +1559,7 @@ class Objects(using Context @constructorOnly): // call .lengthCompare or .length val lengthCompareDenot = getMemberMethod(scrutineeType, nme.lengthCompare, lengthCompareType) if lengthCompareDenot.exists then - call(scrutinee, lengthCompareDenot.symbol, ArgInfo(Bottom, summon[Trace], EmptyTree) :: Nil, scrutineeType, superType = NoType, needResolve = true) + call(scrutinee, lengthCompareDenot.symbol, ArgInfo(BaseOrUnknownValue, summon[Trace], EmptyTree) :: Nil, scrutineeType, superType = NoType, needResolve = true) else val lengthDenot = getMemberMethod(scrutineeType, nme.length, lengthType) call(scrutinee, lengthDenot.symbol, Nil, scrutineeType, superType = NoType, needResolve = true) @@ -1541,18 +1567,18 @@ class Objects(using Context @constructorOnly): // call .apply val applyDenot = getMemberMethod(scrutineeType, nme.apply, applyType(elemType)) - val applyRes = call(scrutinee, applyDenot.symbol, ArgInfo(Bottom, summon[Trace], EmptyTree) :: Nil, scrutineeType, superType = NoType, needResolve = true) + val applyRes = call(scrutinee, applyDenot.symbol, ArgInfo(BaseOrUnknownValue, summon[Trace], EmptyTree) :: Nil, scrutineeType, superType = NoType, needResolve = true) if isWildcardStarArgList(pats) then if pats.size == 1 then // call .toSeq - val toSeqDenot = scrutineeType.member(nme.toSeq).suchThat(_.info.isParameterless) + val toSeqDenot = getMemberMethod(scrutineeType, nme.toSeq, toSeqType(elemType)) val toSeqRes = call(scrutinee, toSeqDenot.symbol, Nil, scrutineeType, superType = NoType, needResolve = true) evalPattern(toSeqRes, pats.head) else // call .drop - val dropDenot = getMemberMethod(scrutineeType, nme.drop, applyType(elemType)) - val dropRes = call(scrutinee, dropDenot.symbol, ArgInfo(Bottom, summon[Trace], EmptyTree) :: Nil, scrutineeType, superType = NoType, needResolve = true) + val dropDenot = getMemberMethod(scrutineeType, nme.drop, dropType(elemType)) + val dropRes = call(scrutinee, dropDenot.symbol, ArgInfo(BaseOrUnknownValue, summon[Trace], EmptyTree) :: Nil, scrutineeType, superType = NoType, needResolve = true) for pat <- pats.init do evalPattern(applyRes, pat) evalPattern(dropRes, pats.last) end if @@ -1575,7 +1601,7 @@ class Objects(using Context @constructorOnly): caseResults.addOne(eval(caseDef.body, thisV, klass)) if catchesAllOf(caseDef, tpe) then remainingScrutinee = remainingScrutinee.remove(value) - + caseResults.join end patternMatch @@ -1594,12 +1620,12 @@ class Objects(using Context @constructorOnly): def evalType(tp: Type, thisV: ThisValue, klass: ClassSymbol, elideObjectAccess: Boolean = false): Contextual[Value] = log("evaluating " + tp.show, printer, (_: Value).show) { tp match case _: ConstantType => - Bottom + BaseOrUnknownValue case tmref: TermRef if tmref.prefix == NoPrefix => val sym = tmref.symbol if sym.is(Flags.Package) then - Bottom + Bottom // TODO: package value? 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 @@ -1833,6 +1859,7 @@ class Objects(using Context @constructorOnly): else thisV match case Bottom => Bottom + case BaseOrUnknownValue => BaseOrUnknownValue case Cold => Cold case ref: Ref => val outerCls = klass.owner.lexicallyEnclosingClass.asClass From 44ed0b373faeb40d9a9afac2e96a77b915866210 Mon Sep 17 00:00:00 2001 From: EnzeXing Date: Sun, 29 Dec 2024 21:00:28 -0500 Subject: [PATCH 03/10] Refactor abstract domain; combining Cold and Unknown Value; adding Package Value --- .../tools/dotc/transform/init/Objects.scala | 160 ++++++++++-------- 1 file changed, 89 insertions(+), 71 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/transform/init/Objects.scala b/compiler/src/dotty/tools/dotc/transform/init/Objects.scala index 98fa1f52818a..d0aee85f9abd 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Objects.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Objects.scala @@ -92,13 +92,13 @@ class Objects(using Context @constructorOnly): * ve ::= ObjectRef(class) // global object * | OfClass(class, vs[outer], ctor, args, env) // instance of a class * | OfArray(object[owner], regions) - * | BaseOrUnknownValue // Int, String, etc., and values without source * | Fun(..., env) // value elements that can be contained in ValueSet + * | BaseValue // Int, String, etc. * vs ::= ValueSet(ve) // set of abstract values * Bottom ::= ValueSet(Empty) - * val ::= ve | Cold | vs // all possible abstract values in domain + * val ::= ve | 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 | Cold // possible values for 'this' + * ThisValue ::= Ref | UnknownValue // 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 @@ -227,6 +227,11 @@ class Objects(using Context @constructorOnly): 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 + ")" + /** Represents common base values like Int, String, etc. + */ + case object BaseValue extends ValueElement: + def show(using Context): String = "BaseValue" + /** * Represents a set of values * @@ -235,22 +240,24 @@ class Objects(using Context @constructorOnly): case class ValueSet(values: ListSet[ValueElement]) extends Value: def show(using Context) = values.map(_.show).mkString("[", ",", "]") - // Represents common base values like Int, String, etc. - // and also values loaded without source - case object BaseOrUnknownValue extends ValueElement: - def show(using Context): String = "BaseOrUnknownValue" + case class Package(packageSym: Symbol) extends Value: + def show(using Context): String = "Package(" + packageSym.show + ")" - /** A cold alias which should not be used during initialization. + /** Represents values unknown to the checker, such as values loaded without source * - * Cold is not ValueElement since RefSet containing Cold is equivalent to Cold + * This is the top of the abstract domain lattice, which should not + * be used during initialization. + * + * UnknownValue is not ValueElement since RefSet containing UnknownValue + * is equivalent to UnknownValue */ - case object Cold extends Value: - def show(using Context) = "Cold" + case object UnknownValue extends Value: + def show(using Context): String = "UnknownValue" val Bottom = ValueSet(ListSet.empty) /** Possible types for 'this' */ - type ThisValue = Ref | Cold.type + type ThisValue = Ref | UnknownValue.type /** Checking state */ object State: @@ -642,8 +649,10 @@ class Objects(using Context @constructorOnly): extension (a: Value) def join(b: Value): Value = (a, b) match - case (Cold, _) => Cold - case (_, Cold) => Cold + case (UnknownValue, _) => UnknownValue + case (_, UnknownValue) => UnknownValue + case (Package(_), _) => UnknownValue // should not happen + case (_, Package(_)) => UnknownValue case (Bottom, b) => b case (a, Bottom) => a case (ValueSet(values1), ValueSet(values2)) => ValueSet(values1 ++ values2) @@ -658,7 +667,7 @@ class Objects(using Context @constructorOnly): case _ => a def widen(height: Int)(using Context): Value = - if height == 0 then Cold + if height == 0 then UnknownValue else a match case Bottom => Bottom @@ -692,16 +701,17 @@ class Objects(using Context @constructorOnly): else val klass = sym.asClass a match - case Cold => Cold - case BaseOrUnknownValue => BaseOrUnknownValue + case UnknownValue => UnknownValue + case Package(_) => a + case BaseValue => BaseValue 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: Ref | Cold.type) - def widenRefOrCold(height : Int)(using Context) : Ref | Cold.type = value.widen(height).asInstanceOf[ThisValue] + extension (value: Ref | UnknownValue.type) + def widenRefOrCold(height : Int)(using Context) : Ref | UnknownValue.type = value.widen(height).asInstanceOf[ThisValue] extension (values: Iterable[Value]) def join: Value = if values.isEmpty then Bottom else values.reduce { (v1, v2) => v1.join(v2) } @@ -719,10 +729,17 @@ 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 Cold => - report.warning("Using cold alias. " + Trace.show, Trace.position) + case UnknownValue => // TODO: This ensures soundness but emits extra warnings. Add an option to turn off warnings here + report.warning("Using unknown value. " + Trace.show, Trace.position) Bottom + case Package(packageSym) => + report.warning("[Internal error] Unexpected call on package = " + value.show + ", meth = " + meth.show + Trace.show, Trace.position) + Bottom + + case BaseValue => // TODO: This ensures soundness but emits extra warnings. Add an option to return BaseValue here + UnknownValue + case Bottom => Bottom @@ -730,9 +747,6 @@ class Objects(using Context @constructorOnly): case _ if args.map(_.value).contains(Bottom) => Bottom - case BaseOrUnknownValue => - BaseOrUnknownValue - case arr: OfArray => val target = resolve(defn.ArrayClass, meth) @@ -751,7 +765,7 @@ class Objects(using Context @constructorOnly): Bottom else // Array.length is OK - BaseOrUnknownValue + BaseValue case ref: Ref => val isLocal = !meth.owner.isClass @@ -772,7 +786,7 @@ class Objects(using Context @constructorOnly): arr else if target.equals(defn.Predef_classOf) then // Predef.classOf is a stub method in tasty and is replaced in backend - BaseOrUnknownValue + UnknownValue else if target.hasSource then val cls = target.owner.enclosingClass.asClass val ddef = target.defTree.asInstanceOf[DefDef] @@ -782,7 +796,7 @@ class Objects(using Context @constructorOnly): if meth.owner.isClass then (ref, Env.NoEnv) else - Env.resolveEnvByOwner(meth.owner.enclosingMethod, ref, summon[Env.Data]).getOrElse(Cold -> Env.NoEnv) + Env.resolveEnvByOwner(meth.owner.enclosingMethod, ref, summon[Env.Data]).getOrElse(UnknownValue -> Env.NoEnv) val env2 = Env.ofDefDef(ddef, args.map(_.value), outerEnv) extendTrace(ddef) { @@ -795,7 +809,7 @@ class Objects(using Context @constructorOnly): } } else - BaseOrUnknownValue + UnknownValue else if target.exists then select(ref, target, receiver, needResolve = false) else @@ -824,7 +838,7 @@ class Objects(using Context @constructorOnly): else // In future, we will have Tasty for stdlib classes and can abstractly interpret that Tasty. // For now, return `Cold` to ensure soundness and trigger a warning. - Cold + UnknownValue end if end if @@ -863,7 +877,7 @@ class Objects(using Context @constructorOnly): } else // no source code available - BaseOrUnknownValue + UnknownValue case _ => report.warning("[Internal error] unexpected constructor call, meth = " + ctor + ", this = " + value + Trace.show, Trace.position) @@ -879,12 +893,21 @@ 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 Cold => - report.warning("Using cold alias", Trace.position) + case UnknownValue => + report.warning("Using unknown value", Trace.position) Bottom - case BaseOrUnknownValue => - BaseOrUnknownValue + case BaseValue => + UnknownValue + + case Package(packageSym) => + if field.isStaticObject then + accessObject(field.moduleClass.asClass) + else if field.is(Flags.Package) then + Package(field) + else + report.warning("[Internal error] Unexpected selection on package " + packageSym.show + ", field = " + field.show + Trace.show, Trace.position) + Bottom case ref: Ref => val target = if needResolve then resolve(ref.klass, field) else field @@ -894,7 +917,7 @@ class Objects(using Context @constructorOnly): val rhs = target.defTree.asInstanceOf[ValDef].rhs eval(rhs, ref, target.owner.asClass, cacheResult = true) else - BaseOrUnknownValue + UnknownValue else if target.exists then def isNextFieldOfColonColon: Boolean = ref.klass == defn.ConsClass && target.name.toString == "next" if target.isOneOf(Flags.Mutable) && !isNextFieldOfColonColon then @@ -910,7 +933,7 @@ class Objects(using Context @constructorOnly): Bottom else // initialization error, reported by the initialization checker - BaseOrUnknownValue + UnknownValue else if ref.hasVal(target) then ref.valValue(target) else if ref.isObjectRef && ref.klass.hasSource then @@ -918,7 +941,7 @@ class Objects(using Context @constructorOnly): Bottom else // initialization error, reported by the initialization checker - BaseOrUnknownValue + UnknownValue else if ref.klass.isSubClass(receiver.widenSingleton.classSymbol) then @@ -927,7 +950,7 @@ class Objects(using Context @constructorOnly): else // This is possible due to incorrect type cast or accessing standard library objects // See tests/init/pos/Type.scala / tests/init/warn/unapplySeq-implicit-arg2.scala - BaseOrUnknownValue + UnknownValue case fun: Fun => report.warning("[Internal error] unexpected tree in selecting a function, fun = " + fun.code.show + Trace.show, fun.code) @@ -937,9 +960,7 @@ class Objects(using Context @constructorOnly): report.warning("[Internal error] unexpected tree in selecting an array, array = " + arr.show + Trace.show, Trace.position) Bottom - case Bottom => // TODO: add a value for packages? - if field.isStaticObject then accessObject(field.moduleClass.asClass) - else Bottom + case Bottom => Bottom case ValueSet(values) => values.map(ref => select(ref, field, receiver)).join @@ -954,16 +975,15 @@ 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 p: Package => + report.warning("[Internal error] unexpected tree in assignment, package = " + p.packageSym.show + Trace.show, Trace.position) case fun: Fun => report.warning("[Internal error] unexpected tree in assignment, fun = " + fun.code.show + Trace.show, Trace.position) - case arr: OfArray => report.warning("[Internal error] unexpected tree in assignment, array = " + arr.show + " field = " + field + Trace.show, Trace.position) - case Cold => - report.warning("Assigning to cold aliases is forbidden. " + Trace.show, Trace.position) - - case BaseOrUnknownValue | Bottom => + case BaseValue | UnknownValue => + report.warning("Assigning to base or unknown value is forbidden. " + Trace.show, Trace.position) case ValueSet(values) => values.foreach(ref => assign(ref, field, rhs, rhsTyp)) @@ -994,14 +1014,14 @@ class Objects(using Context @constructorOnly): */ 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.filterClass(klass.owner) match - case _ : Fun | _: OfArray => + case _ : Fun | _: OfArray | BaseValue => report.warning("[Internal error] unexpected outer in instantiating a class, outer = " + outer.show + ", class = " + klass.show + ", " + Trace.show, Trace.position) Bottom - case BaseOrUnknownValue => - BaseOrUnknownValue + case UnknownValue => + UnknownValue - case outer: (Ref | Cold.type | Bottom.type) => + case outer: (Ref | UnknownValue.type | Package) => if klass == defn.ArrayClass then args.head.tree.tpe match case ConstantType(Constants.Constant(0)) => @@ -1015,18 +1035,18 @@ class Objects(using Context @constructorOnly): // Widen the outer to finitize the domain. Arguments already widened in `evalArgs`. val (outerWidened, envWidened) = outer match - case _ : Bottom.type => // For top-level classes - (Bottom, Env.NoEnv) - case thisV : (Ref | Cold.type) => + case Package(_) => // For top-level classes + (outer, Env.NoEnv) + case thisV : (Ref | UnknownValue.type) => if klass.owner.isClass then if klass.owner.is(Flags.Package) then - report.warning("[Internal error] top-level class should have `Bottom` as outer, class = " + klass.show + ", outer = " + outer.show + ", " + Trace.show, Trace.position) + 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.widenRefOrCold(1), Env.NoEnv) else // klass.enclosingMethod returns its primary constructor - Env.resolveEnvByOwner(klass.owner.enclosingMethod, thisV, summon[Env.Data]).getOrElse(Cold -> Env.NoEnv) + Env.resolveEnvByOwner(klass.owner.enclosingMethod, thisV, summon[Env.Data]).getOrElse(UnknownValue -> Env.NoEnv) val instance = OfClass(klass, outerWidened, ctor, args.map(_.value), envWidened) callConstructor(instance, ctor, args) @@ -1088,11 +1108,10 @@ class Objects(using Context @constructorOnly): case fun: Fun => given Env.Data = Env.ofByName(sym, fun.env) eval(fun.code, fun.thisV, fun.klass) - case Cold => - report.warning("Calling cold by-name alias. " + Trace.show, Trace.position) + case UnknownValue => + report.warning("Calling on unknown value. " + Trace.show, Trace.position) Bottom - case BaseOrUnknownValue => BaseOrUnknownValue - case _: ValueSet | _: Ref | _: OfArray => + case _: ValueSet | _: Ref | _: OfArray | _: Package | BaseValue => report.warning("[Internal error] Unexpected by-name value " + value.show + ". " + Trace.show, Trace.position) Bottom else @@ -1103,7 +1122,7 @@ class Objects(using Context @constructorOnly): report.warning("Calling cold by-name alias. " + Trace.show, Trace.position) Bottom else - Cold + UnknownValue } /** Handle local variable assignmenbt, `x = e`. @@ -1281,7 +1300,7 @@ class Objects(using Context @constructorOnly): evalType(expr.tpe, thisV, klass) case Literal(_) => - BaseOrUnknownValue + BaseValue case Typed(expr, tpt) => if tpt.tpe.hasAnnotation(defn.UncheckedAnnot) then @@ -1559,7 +1578,7 @@ class Objects(using Context @constructorOnly): // call .lengthCompare or .length val lengthCompareDenot = getMemberMethod(scrutineeType, nme.lengthCompare, lengthCompareType) if lengthCompareDenot.exists then - call(scrutinee, lengthCompareDenot.symbol, ArgInfo(BaseOrUnknownValue, summon[Trace], EmptyTree) :: Nil, scrutineeType, superType = NoType, needResolve = true) + call(scrutinee, lengthCompareDenot.symbol, ArgInfo(UnknownValue, summon[Trace], EmptyTree) :: Nil, scrutineeType, superType = NoType, needResolve = true) else val lengthDenot = getMemberMethod(scrutineeType, nme.length, lengthType) call(scrutinee, lengthDenot.symbol, Nil, scrutineeType, superType = NoType, needResolve = true) @@ -1567,7 +1586,7 @@ class Objects(using Context @constructorOnly): // call .apply val applyDenot = getMemberMethod(scrutineeType, nme.apply, applyType(elemType)) - val applyRes = call(scrutinee, applyDenot.symbol, ArgInfo(BaseOrUnknownValue, summon[Trace], EmptyTree) :: Nil, scrutineeType, superType = NoType, needResolve = true) + val applyRes = call(scrutinee, applyDenot.symbol, ArgInfo(BaseValue, summon[Trace], EmptyTree) :: Nil, scrutineeType, superType = NoType, needResolve = true) if isWildcardStarArgList(pats) then if pats.size == 1 then @@ -1578,7 +1597,7 @@ class Objects(using Context @constructorOnly): else // call .drop val dropDenot = getMemberMethod(scrutineeType, nme.drop, dropType(elemType)) - val dropRes = call(scrutinee, dropDenot.symbol, ArgInfo(BaseOrUnknownValue, summon[Trace], EmptyTree) :: Nil, scrutineeType, superType = NoType, needResolve = true) + val dropRes = call(scrutinee, dropDenot.symbol, ArgInfo(BaseValue, summon[Trace], EmptyTree) :: Nil, scrutineeType, superType = NoType, needResolve = true) for pat <- pats.init do evalPattern(applyRes, pat) evalPattern(dropRes, pats.last) end if @@ -1620,12 +1639,12 @@ class Objects(using Context @constructorOnly): def evalType(tp: Type, thisV: ThisValue, klass: ClassSymbol, elideObjectAccess: Boolean = false): Contextual[Value] = log("evaluating " + tp.show, printer, (_: Value).show) { tp match case _: ConstantType => - BaseOrUnknownValue + BaseValue case tmref: TermRef if tmref.prefix == NoPrefix => val sym = tmref.symbol if sym.is(Flags.Package) then - Bottom // TODO: package value? + Package(sym) 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 @@ -1651,7 +1670,7 @@ class Objects(using Context @constructorOnly): case tp @ ThisType(tref) => val sym = tref.symbol if sym.is(Flags.Package) then - Bottom + Package(sym) else if sym.isStaticObject && sym != klass then // The typer may use ThisType to refer to an object outside its definition. if elideObjectAccess then @@ -1851,7 +1870,7 @@ class Objects(using Context @constructorOnly): if target == klass then thisV else if target.is(Flags.Package) then - Bottom + Package(target) // TODO: What is the semantics for package.this? else if target.isStaticObject then val res = ObjectRef(target.moduleClass.asClass) if elideObjectAccess then res @@ -1859,8 +1878,7 @@ class Objects(using Context @constructorOnly): else thisV match case Bottom => Bottom - case BaseOrUnknownValue => BaseOrUnknownValue - case Cold => Cold + case UnknownValue => UnknownValue case ref: Ref => val outerCls = klass.owner.lexicallyEnclosingClass.asClass if !ref.hasOuter(klass) then @@ -1871,7 +1889,7 @@ class Objects(using Context @constructorOnly): resolveThis(target, ref.outerValue(klass), outerCls) case ValueSet(values) => values.map(ref => resolveThis(target, ref, klass)).join - case _: Fun | _ : OfArray => + case _: Fun | _ : OfArray | _: Package | BaseValue => report.warning("[Internal error] unexpected thisV = " + thisV + ", target = " + target.show + ", klass = " + klass.show + Trace.show, Trace.position) Bottom } From bf18e953b1cf0a1e9b3aec8dcc84685c4ed0eaa2 Mon Sep 17 00:00:00 2001 From: EnzeXing Date: Sun, 12 Jan 2025 16:32:02 -0500 Subject: [PATCH 04/10] Add option to ignore using unknown value warning --- compiler/src/dotty/tools/dotc/Compiler.scala | 2 +- .../tools/dotc/config/ScalaSettings.scala | 2 +- .../src/dotty/tools/dotc/core/Symbols.scala | 2 +- .../tools/dotc/transform/init/Checker.scala | 4 +-- .../tools/dotc/transform/init/Objects.scala | 34 +++++++++++++------ .../dotty/tools/dotc/CompilationTests.scala | 4 +-- 6 files changed, 31 insertions(+), 17 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/Compiler.scala b/compiler/src/dotty/tools/dotc/Compiler.scala index 6aab7d54d59e..71eb29ad1063 100644 --- a/compiler/src/dotty/tools/dotc/Compiler.scala +++ b/compiler/src/dotty/tools/dotc/Compiler.scala @@ -171,7 +171,7 @@ class Compiler { val rctx = if ctx.settings.Xsemanticdb.value then ctx.addMode(Mode.ReadPositions) - else if ctx.settings.YcheckInitGlobal.value then + else if !ctx.settings.YcheckInitGlobal.isDefault then ctx.addMode(Mode.ReadPositions) else ctx diff --git a/compiler/src/dotty/tools/dotc/config/ScalaSettings.scala b/compiler/src/dotty/tools/dotc/config/ScalaSettings.scala index c6c0ab47de52..28b848d6d5b2 100644 --- a/compiler/src/dotty/tools/dotc/config/ScalaSettings.scala +++ b/compiler/src/dotty/tools/dotc/config/ScalaSettings.scala @@ -446,7 +446,7 @@ private sealed trait YSettings: val YnoKindPolymorphism: Setting[Boolean] = BooleanSetting(ForkSetting, "Yno-kind-polymorphism", "Disable kind polymorphism.") val YexplicitNulls: Setting[Boolean] = BooleanSetting(ForkSetting, "Yexplicit-nulls", "Make reference types non-nullable. Nullable types can be expressed with unions: e.g. String|Null.") val YnoFlexibleTypes: Setting[Boolean] = BooleanSetting(ForkSetting, "Yno-flexible-types", "Disable turning nullable Java return types and parameter types into flexible types, which behave like abstract types with a nullable lower bound and non-nullable upper bound.") - val YcheckInitGlobal: Setting[Boolean] = BooleanSetting(ForkSetting, "Ysafe-init-global", "Check safe initialization of global objects.") + val YcheckInitGlobal: Setting[String] = ChoiceSetting(ForkSetting, "Ysafe-init-global", "[report-unknown, ignore-unknown]", "Check safe initialization of global objects.", List("report-unknown", "ignore-unknown", "off"), "off") val YrequireTargetName: Setting[Boolean] = BooleanSetting(ForkSetting, "Yrequire-targetName", "Warn if an operator is defined without a @targetName annotation.") val YrecheckTest: Setting[Boolean] = BooleanSetting(ForkSetting, "Yrecheck-test", "Run basic rechecking (internal test only).") val YccDebug: Setting[Boolean] = BooleanSetting(ForkSetting, "Ycc-debug", "Used in conjunction with captureChecking language import, debug info for captured references.") diff --git a/compiler/src/dotty/tools/dotc/core/Symbols.scala b/compiler/src/dotty/tools/dotc/core/Symbols.scala index 7de75e371752..b53e103bfab0 100644 --- a/compiler/src/dotty/tools/dotc/core/Symbols.scala +++ b/compiler/src/dotty/tools/dotc/core/Symbols.scala @@ -85,7 +85,7 @@ object Symbols extends SymUtils { denot.owner.isTerm || // no risk of leaking memory after a run for these denot.isOneOf(InlineOrProxy) || // need to keep inline info ctx.settings.Whas.checkInit || // initialization check - ctx.settings.YcheckInitGlobal.value + !ctx.settings.YcheckInitGlobal.isDefault /** The last denotation of this symbol */ private var lastDenot: SymDenotation = uninitialized diff --git a/compiler/src/dotty/tools/dotc/transform/init/Checker.scala b/compiler/src/dotty/tools/dotc/transform/init/Checker.scala index 4d5c467cf4fe..b8afabf1efc7 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Checker.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Checker.scala @@ -29,7 +29,7 @@ class Checker extends Phase: override val runsAfter = Set(Pickler.name) override def isEnabled(using Context): Boolean = - super.isEnabled && (ctx.settings.Whas.checkInit || ctx.settings.YcheckInitGlobal.value) + super.isEnabled && (ctx.settings.Whas.checkInit || !ctx.settings.YcheckInitGlobal.isDefault) def traverse(traverser: InitTreeTraverser)(using Context): Boolean = monitor(phaseName): val unit = ctx.compilationUnit @@ -53,7 +53,7 @@ class Checker extends Phase: if ctx.settings.Whas.checkInit then Semantic.checkClasses(classes)(using checkCtx) - if ctx.settings.YcheckInitGlobal.value then + if !ctx.settings.YcheckInitGlobal.isDefault then val obj = new Objects obj.checkClasses(classes)(using checkCtx) } diff --git a/compiler/src/dotty/tools/dotc/transform/init/Objects.scala b/compiler/src/dotty/tools/dotc/transform/init/Objects.scala index d0aee85f9abd..527708a9cf69 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Objects.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Objects.scala @@ -718,6 +718,11 @@ class Objects(using Context @constructorOnly): def widen(height: Int): Contextual[List[Value]] = values.map(_.widen(height)).toList + /** Check if the checker option reports warnings about unknown code + */ + def reportUnknown(using context: Context): Boolean = + context.settings.YcheckInitGlobal.value == "report-unknown" + /** Handle method calls `e.m(args)`. * * @param value The value for the receiver. @@ -727,18 +732,21 @@ class Objects(using Context @constructorOnly): * @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 + ", this = " + value.show + ", args = " + args.map(_.value.show), printer, (_: Value).show) { + 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(_.tree.show), printer, (_: Value).show) { value.filterClass(meth.owner) match - case UnknownValue => // TODO: This ensures soundness but emits extra warnings. Add an option to turn off warnings here - report.warning("Using unknown value. " + Trace.show, Trace.position) - Bottom + case UnknownValue => + if reportUnknown then + report.warning("Using unknown value. " + Trace.show, Trace.position) + Bottom + else + UnknownValue case Package(packageSym) => report.warning("[Internal error] Unexpected call on package = " + value.show + ", meth = " + meth.show + Trace.show, Trace.position) Bottom - case BaseValue => // TODO: This ensures soundness but emits extra warnings. Add an option to return BaseValue here - UnknownValue + case BaseValue => + if reportUnknown then UnknownValue else BaseValue case Bottom => Bottom @@ -786,7 +794,10 @@ class Objects(using Context @constructorOnly): arr else if target.equals(defn.Predef_classOf) then // Predef.classOf is a stub method in tasty and is replaced in backend - UnknownValue + BaseValue + else if target.equals(defn.ClassTagModule_apply) then + // ClassTag and other reflection related values are considered safe + BaseValue else if target.hasSource then val cls = target.owner.enclosingClass.asClass val ddef = target.defTree.asInstanceOf[DefDef] @@ -894,11 +905,14 @@ 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 UnknownValue => - report.warning("Using unknown value", Trace.position) - Bottom + if reportUnknown then + report.warning("Using unknown value. " + Trace.show, Trace.position) + Bottom + else + UnknownValue case BaseValue => - UnknownValue + if reportUnknown then UnknownValue else BaseValue case Package(packageSym) => if field.isStaticObject then diff --git a/compiler/test/dotty/tools/dotc/CompilationTests.scala b/compiler/test/dotty/tools/dotc/CompilationTests.scala index cc1ce5a0145e..b35ecd0983c3 100644 --- a/compiler/test/dotty/tools/dotc/CompilationTests.scala +++ b/compiler/test/dotty/tools/dotc/CompilationTests.scala @@ -231,8 +231,8 @@ class CompilationTests { // initialization tests @Test def checkInitGlobal: Unit = { implicit val testGroup: TestGroup = TestGroup("checkInitGlobal") - compileFilesInDir("tests/init-global/warn", defaultOptions.and("-Ysafe-init-global"), FileFilter.exclude(TestSources.negInitGlobalScala2LibraryTastyExcludelisted)).checkWarnings() - compileFilesInDir("tests/init-global/pos", defaultOptions.and("-Ysafe-init-global", "-Xfatal-warnings"), FileFilter.exclude(TestSources.posInitGlobalScala2LibraryTastyExcludelisted)).checkCompile() + compileFilesInDir("tests/init-global/warn", defaultOptions.and("-Ysafe-init-global:ignore-unknown"), FileFilter.exclude(TestSources.negInitGlobalScala2LibraryTastyExcludelisted)).checkWarnings() + compileFilesInDir("tests/init-global/pos", defaultOptions.and("-Ysafe-init-global:ignore-unknown", "-Xfatal-warnings"), FileFilter.exclude(TestSources.posInitGlobalScala2LibraryTastyExcludelisted)).checkCompile() } // initialization tests From f51f91b83482839f97185a4e6febfc2aaa23370e Mon Sep 17 00:00:00 2001 From: EnzeXing Date: Fri, 17 Jan 2025 20:55:32 -0500 Subject: [PATCH 05/10] Rename BaseValue to SafeValue --- .../tools/dotc/transform/init/Objects.scala | 39 ++++++++++--------- 1 file changed, 20 insertions(+), 19 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/transform/init/Objects.scala b/compiler/src/dotty/tools/dotc/transform/init/Objects.scala index 527708a9cf69..978216bc12eb 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Objects.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Objects.scala @@ -93,7 +93,7 @@ class Objects(using Context @constructorOnly): * | 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 - * | BaseValue // Int, String, etc. + * | SafeValue // values on which method calls and fields won't cause warnings. Int, String, etc. * vs ::= ValueSet(ve) // set of abstract values * Bottom ::= ValueSet(Empty) * val ::= ve | UnknownValue | vs | Package // all possible abstract values in domain @@ -229,8 +229,9 @@ class Objects(using Context @constructorOnly): /** Represents common base values like Int, String, etc. */ - case object BaseValue extends ValueElement: - def show(using Context): String = "BaseValue" + case object SafeValue extends ValueElement: + val safeTypes = defn.ScalaNumericValueTypeList ++ List(defn.UnitType, defn.BooleanType, defn.StringType) + def show(using Context): String = "SafeValue" /** * Represents a set of values @@ -703,7 +704,7 @@ class Objects(using Context @constructorOnly): a match case UnknownValue => UnknownValue case Package(_) => a - case BaseValue => BaseValue + case SafeValue => SafeValue 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 @@ -745,8 +746,8 @@ class Objects(using Context @constructorOnly): report.warning("[Internal error] Unexpected call on package = " + value.show + ", meth = " + meth.show + Trace.show, Trace.position) Bottom - case BaseValue => - if reportUnknown then UnknownValue else BaseValue + case SafeValue => + SafeValue // Check return type, if not safe, try to analyze body, 1.until(2).map(i => UninitializedObject) case Bottom => Bottom @@ -773,7 +774,7 @@ class Objects(using Context @constructorOnly): Bottom else // Array.length is OK - BaseValue + SafeValue case ref: Ref => val isLocal = !meth.owner.isClass @@ -794,10 +795,10 @@ class Objects(using Context @constructorOnly): arr else if target.equals(defn.Predef_classOf) then // Predef.classOf is a stub method in tasty and is replaced in backend - BaseValue + SafeValue else if target.equals(defn.ClassTagModule_apply) then // ClassTag and other reflection related values are considered safe - BaseValue + SafeValue else if target.hasSource then val cls = target.owner.enclosingClass.asClass val ddef = target.defTree.asInstanceOf[DefDef] @@ -911,8 +912,8 @@ class Objects(using Context @constructorOnly): else UnknownValue - case BaseValue => - if reportUnknown then UnknownValue else BaseValue + case SafeValue => + SafeValue case Package(packageSym) => if field.isStaticObject then @@ -996,7 +997,7 @@ class Objects(using Context @constructorOnly): case arr: OfArray => report.warning("[Internal error] unexpected tree in assignment, array = " + arr.show + " field = " + field + Trace.show, Trace.position) - case BaseValue | UnknownValue => + case SafeValue | UnknownValue => report.warning("Assigning to base or unknown value is forbidden. " + Trace.show, Trace.position) case ValueSet(values) => @@ -1028,7 +1029,7 @@ class Objects(using Context @constructorOnly): */ 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.filterClass(klass.owner) match - case _ : Fun | _: OfArray | BaseValue => + case _ : Fun | _: OfArray | SafeValue => report.warning("[Internal error] unexpected outer in instantiating a class, outer = " + outer.show + ", class = " + klass.show + ", " + Trace.show, Trace.position) Bottom @@ -1125,7 +1126,7 @@ class Objects(using Context @constructorOnly): case UnknownValue => report.warning("Calling on unknown value. " + Trace.show, Trace.position) Bottom - case _: ValueSet | _: Ref | _: OfArray | _: Package | BaseValue => + case _: ValueSet | _: Ref | _: OfArray | _: Package | SafeValue => report.warning("[Internal error] Unexpected by-name value " + value.show + ". " + Trace.show, Trace.position) Bottom else @@ -1314,7 +1315,7 @@ class Objects(using Context @constructorOnly): evalType(expr.tpe, thisV, klass) case Literal(_) => - BaseValue + SafeValue case Typed(expr, tpt) => if tpt.tpe.hasAnnotation(defn.UncheckedAnnot) then @@ -1600,7 +1601,7 @@ class Objects(using Context @constructorOnly): // call .apply val applyDenot = getMemberMethod(scrutineeType, nme.apply, applyType(elemType)) - val applyRes = call(scrutinee, applyDenot.symbol, ArgInfo(BaseValue, summon[Trace], EmptyTree) :: Nil, scrutineeType, superType = NoType, needResolve = true) + val applyRes = call(scrutinee, applyDenot.symbol, ArgInfo(SafeValue, summon[Trace], EmptyTree) :: Nil, scrutineeType, superType = NoType, needResolve = true) if isWildcardStarArgList(pats) then if pats.size == 1 then @@ -1611,7 +1612,7 @@ class Objects(using Context @constructorOnly): else // call .drop val dropDenot = getMemberMethod(scrutineeType, nme.drop, dropType(elemType)) - val dropRes = call(scrutinee, dropDenot.symbol, ArgInfo(BaseValue, summon[Trace], EmptyTree) :: Nil, scrutineeType, superType = NoType, needResolve = true) + val dropRes = call(scrutinee, dropDenot.symbol, ArgInfo(SafeValue, summon[Trace], EmptyTree) :: Nil, scrutineeType, superType = NoType, needResolve = true) for pat <- pats.init do evalPattern(applyRes, pat) evalPattern(dropRes, pats.last) end if @@ -1653,7 +1654,7 @@ class Objects(using Context @constructorOnly): def evalType(tp: Type, thisV: ThisValue, klass: ClassSymbol, elideObjectAccess: Boolean = false): Contextual[Value] = log("evaluating " + tp.show, printer, (_: Value).show) { tp match case _: ConstantType => - BaseValue + SafeValue case tmref: TermRef if tmref.prefix == NoPrefix => val sym = tmref.symbol @@ -1903,7 +1904,7 @@ class Objects(using Context @constructorOnly): resolveThis(target, ref.outerValue(klass), outerCls) case ValueSet(values) => values.map(ref => resolveThis(target, ref, klass)).join - case _: Fun | _ : OfArray | _: Package | BaseValue => + case _: Fun | _ : OfArray | _: Package | SafeValue => report.warning("[Internal error] unexpected thisV = " + thisV + ", target = " + target.show + ", klass = " + klass.show + Trace.show, Trace.position) Bottom } From fdcb455b8da18a9df9c7b9668c754beb52c2d1ef Mon Sep 17 00:00:00 2001 From: EnzeXing Date: Wed, 5 Feb 2025 12:18:04 -0500 Subject: [PATCH 06/10] Adding semantics for calling on SafeValue and evaluating SeqLiteral --- .../tools/dotc/transform/init/Objects.scala | 96 +++++++++++++------ .../pos/packageObjectStringInterpolator.scala | 16 ++++ 2 files changed, 82 insertions(+), 30 deletions(-) create mode 100644 tests/init-global/pos/packageObjectStringInterpolator.scala diff --git a/compiler/src/dotty/tools/dotc/transform/init/Objects.scala b/compiler/src/dotty/tools/dotc/transform/init/Objects.scala index 978216bc12eb..1257905cc6c9 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Objects.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Objects.scala @@ -227,11 +227,19 @@ class Objects(using Context @constructorOnly): 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 + ")" - /** Represents common base values like Int, String, etc. + /** + * Represents common base values like Int, String, etc. + * Assumption: all methods calls on such values should be pure (no side effects) */ - case object SafeValue extends ValueElement: - val safeTypes = defn.ScalaNumericValueTypeList ++ List(defn.UnitType, defn.BooleanType, defn.StringType) - def show(using Context): String = "SafeValue" + case class SafeValue(tpe: Type) extends ValueElement: + // tpe could be a AppliedType(java.lang.Class, T) + val baseType = if tpe.isInstanceOf[AppliedType] then tpe.asInstanceOf[AppliedType].underlying else tpe + assert(baseType.isInstanceOf[TypeRef] && SafeValue.safeTypes.contains(baseType), "Invalid creation of SafeValue! Type = " + tpe) + val typeref = baseType.asInstanceOf[TypeRef] + def show(using Context): String = "SafeValue of type " + tpe + + object SafeValue: + val safeTypes = defn.ScalaNumericValueTypeList ++ List(defn.UnitType, defn.BooleanType, defn.StringType, defn.NullType, defn.ClassClass.typeRef) /** * Represents a set of values @@ -704,7 +712,7 @@ class Objects(using Context @constructorOnly): a match case UnknownValue => UnknownValue case Package(_) => a - case SafeValue => SafeValue + case SafeValue(_) => a 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 @@ -733,7 +741,7 @@ class Objects(using Context @constructorOnly): * @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 + ", this = " + value.show + ", args = " + args.map(_.tree.show), printer, (_: Value).show) { + 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 UnknownValue => if reportUnknown then @@ -743,11 +751,33 @@ class Objects(using Context @constructorOnly): UnknownValue case Package(packageSym) => - report.warning("[Internal error] Unexpected call on package = " + value.show + ", meth = " + meth.show + Trace.show, Trace.position) - Bottom - - case SafeValue => - SafeValue // Check return type, if not safe, try to analyze body, 1.until(2).map(i => UninitializedObject) + // calls on packages are unexpected. However the typer might mistakenly + // set the receiver to be a package instead of package object. + // See packageObjectStringInterpolator.scala + if !meth.owner.denot.isPackageObject then + report.warning("[Internal error] Unexpected call on package = " + value.show + ", meth = " + meth.show + Trace.show, Trace.position) + Bottom + else + // Method call on package object instead + val packageObj = accessObject(meth.owner.moduleClass.asClass) + call(packageObj, meth, args, receiver, superType, needResolve) + + case v @ SafeValue(tpe) => + // Assume such method is pure. Check return type, only try to analyze body if return type is not safe + val target = resolve(v.typeref.symbol.asClass, meth) + if !target.hasSource then + UnknownValue + else + val ddef = target.defTree.asInstanceOf[DefDef] + val returnType = ddef.tpt.tpe + if SafeValue.safeTypes.contains(returnType) then + // since method is pure and return type is safe, no need to analyze method body + SafeValue(returnType) + else + val cls = target.owner.enclosingClass.asClass + // convert SafeType to an OfClass before analyzing method body + val ref = OfClass(cls, Bottom, NoSymbol, Nil, Env.NoEnv) + call(ref, meth, args, receiver, superType, needResolve) case Bottom => Bottom @@ -774,7 +804,7 @@ class Objects(using Context @constructorOnly): Bottom else // Array.length is OK - SafeValue + SafeValue(defn.IntType) case ref: Ref => val isLocal = !meth.owner.isClass @@ -795,10 +825,10 @@ class Objects(using Context @constructorOnly): arr else if target.equals(defn.Predef_classOf) then // Predef.classOf is a stub method in tasty and is replaced in backend - SafeValue + UnknownValue else if target.equals(defn.ClassTagModule_apply) then - // ClassTag and other reflection related values are considered safe - SafeValue + // ClassTag and other reflection related values are not analyzed + UnknownValue else if target.hasSource then val cls = target.owner.enclosingClass.asClass val ddef = target.defTree.asInstanceOf[DefDef] @@ -886,6 +916,7 @@ class Objects(using Context @constructorOnly): Returns.installHandler(ctor) eval(ddef.rhs, ref, cls, cacheResult = true) Returns.popHandler(ctor) + value } else // no source code available @@ -912,8 +943,9 @@ class Objects(using Context @constructorOnly): else UnknownValue - case SafeValue => - SafeValue + case v @ SafeValue(_) => + report.warning("[Internal error] Unexpected selection on safe value " + v.show + ", field = " + field.show + Trace.show, Trace.position) + Bottom case Package(packageSym) => if field.isStaticObject then @@ -997,7 +1029,7 @@ class Objects(using Context @constructorOnly): case arr: OfArray => report.warning("[Internal error] unexpected tree in assignment, array = " + arr.show + " field = " + field + Trace.show, Trace.position) - case SafeValue | UnknownValue => + case SafeValue(_) | UnknownValue => report.warning("Assigning to base or unknown value is forbidden. " + Trace.show, Trace.position) case ValueSet(values) => @@ -1029,7 +1061,7 @@ class Objects(using Context @constructorOnly): */ 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.filterClass(klass.owner) match - case _ : Fun | _: OfArray | SafeValue => + case _ : Fun | _: OfArray | SafeValue(_) => report.warning("[Internal error] unexpected outer in instantiating a class, outer = " + outer.show + ", class = " + klass.show + ", " + Trace.show, Trace.position) Bottom @@ -1126,7 +1158,7 @@ class Objects(using Context @constructorOnly): case UnknownValue => report.warning("Calling on unknown value. " + Trace.show, Trace.position) Bottom - case _: ValueSet | _: Ref | _: OfArray | _: Package | SafeValue => + case _: ValueSet | _: Ref | _: OfArray | _: Package | SafeValue(_) => report.warning("[Internal error] Unexpected by-name value " + value.show + ". " + Trace.show, Trace.position) Bottom else @@ -1314,8 +1346,8 @@ class Objects(using Context @constructorOnly): case _: This => evalType(expr.tpe, thisV, klass) - case Literal(_) => - SafeValue + case Literal(const) => + SafeValue(const.tpe) case Typed(expr, tpt) => if tpt.tpe.hasAnnotation(defn.UncheckedAnnot) then @@ -1390,7 +1422,12 @@ class Objects(using Context @constructorOnly): res case SeqLiteral(elems, elemtpt) => - evalExprs(elems, thisV, klass).join + // Obtain the output Seq from SeqLiteral tree by calling respective wrapArrayMethod + val wrapArrayMethodName = ast.tpd.wrapArrayMethodName(elemtpt.tpe) + val meth = defn.getWrapVarargsArrayModule.requiredMethod(wrapArrayMethodName) + val module = defn.getWrapVarargsArrayModule.moduleClass.asClass + val args = evalArgs(elems.map(Arg.apply), thisV, klass) + call(ObjectRef(module), meth, args, module.typeRef, NoType) case Inlined(call, bindings, expansion) => evalExprs(bindings, thisV, klass) @@ -1601,7 +1638,7 @@ class Objects(using Context @constructorOnly): // call .apply val applyDenot = getMemberMethod(scrutineeType, nme.apply, applyType(elemType)) - val applyRes = call(scrutinee, applyDenot.symbol, ArgInfo(SafeValue, summon[Trace], EmptyTree) :: Nil, scrutineeType, superType = NoType, needResolve = true) + val applyRes = call(scrutinee, applyDenot.symbol, ArgInfo(SafeValue(defn.IntType), summon[Trace], EmptyTree) :: Nil, scrutineeType, superType = NoType, needResolve = true) if isWildcardStarArgList(pats) then if pats.size == 1 then @@ -1612,7 +1649,7 @@ class Objects(using Context @constructorOnly): else // call .drop val dropDenot = getMemberMethod(scrutineeType, nme.drop, dropType(elemType)) - val dropRes = call(scrutinee, dropDenot.symbol, ArgInfo(SafeValue, summon[Trace], EmptyTree) :: Nil, scrutineeType, superType = NoType, needResolve = true) + val dropRes = call(scrutinee, dropDenot.symbol, ArgInfo(SafeValue(defn.IntType), summon[Trace], EmptyTree) :: Nil, scrutineeType, superType = NoType, needResolve = true) for pat <- pats.init do evalPattern(applyRes, pat) evalPattern(dropRes, pats.last) end if @@ -1623,8 +1660,7 @@ class Objects(using Context @constructorOnly): end evalSeqPatterns def canSkipCase(remainingScrutinee: Value, catchValue: Value) = - (remainingScrutinee == Bottom && scrutinee != Bottom) || - (catchValue == Bottom && remainingScrutinee != Bottom) + remainingScrutinee == Bottom || catchValue == Bottom var remainingScrutinee = scrutinee val caseResults: mutable.ArrayBuffer[Value] = mutable.ArrayBuffer() @@ -1653,8 +1689,8 @@ class Objects(using Context @constructorOnly): */ def evalType(tp: Type, thisV: ThisValue, klass: ClassSymbol, elideObjectAccess: Boolean = false): Contextual[Value] = log("evaluating " + tp.show, printer, (_: Value).show) { tp match - case _: ConstantType => - SafeValue + case consttpe: ConstantType => + SafeValue(consttpe.underlying) case tmref: TermRef if tmref.prefix == NoPrefix => val sym = tmref.symbol @@ -1904,7 +1940,7 @@ class Objects(using Context @constructorOnly): resolveThis(target, ref.outerValue(klass), outerCls) case ValueSet(values) => values.map(ref => resolveThis(target, ref, klass)).join - case _: Fun | _ : OfArray | _: Package | SafeValue => + case _: Fun | _ : OfArray | _: Package | SafeValue(_) => report.warning("[Internal error] unexpected thisV = " + thisV + ", target = " + target.show + ", klass = " + klass.show + Trace.show, Trace.position) Bottom } diff --git a/tests/init-global/pos/packageObjectStringInterpolator.scala b/tests/init-global/pos/packageObjectStringInterpolator.scala new file mode 100644 index 000000000000..21b16c81269f --- /dev/null +++ b/tests/init-global/pos/packageObjectStringInterpolator.scala @@ -0,0 +1,16 @@ +package p +package object a { + val b = 10 + implicit class CI(s: StringContext) { + def ci(args: Any*) = 10 + } +} + +import p.a._ + +object A: + val f = b // p.a(ObjectRef(p.a)).b + def foo(s: String): String = s + val f1 = ci"a" // => p.a(Package(p).select(a)).CI(StringContext"a").ci() + + From 7ee07265542c68a8092a4eab1d70b537da5e5f79 Mon Sep 17 00:00:00 2001 From: EnzeXing Date: Tue, 11 Feb 2025 20:12:09 -0500 Subject: [PATCH 07/10] Modify test --- compiler/src/dotty/tools/dotc/transform/init/Objects.scala | 7 +++---- tests/init-global/warn/mutable-array.check | 6 +++--- tests/init-global/warn/mutable-array.scala | 3 ++- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/transform/init/Objects.scala b/compiler/src/dotty/tools/dotc/transform/init/Objects.scala index 1257905cc6c9..455b5b2d76e2 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Objects.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Objects.scala @@ -826,9 +826,6 @@ class Objects(using Context @constructorOnly): else if target.equals(defn.Predef_classOf) then // Predef.classOf is a stub method in tasty and is replaced in backend UnknownValue - else if target.equals(defn.ClassTagModule_apply) then - // ClassTag and other reflection related values are not analyzed - UnknownValue else if target.hasSource then val cls = target.owner.enclosingClass.asClass val ddef = target.defTree.asInstanceOf[DefDef] @@ -1427,7 +1424,9 @@ class Objects(using Context @constructorOnly): val meth = defn.getWrapVarargsArrayModule.requiredMethod(wrapArrayMethodName) val module = defn.getWrapVarargsArrayModule.moduleClass.asClass val args = evalArgs(elems.map(Arg.apply), thisV, klass) - call(ObjectRef(module), meth, args, module.typeRef, NoType) + val arr = OfArray(State.currentObject, summon[Regions.Data]) + Heap.writeJoin(arr.addr, args.map(_.value).join) + call(ObjectRef(module), meth, List(ArgInfo(arr, summon[Trace], EmptyTree)), module.typeRef, NoType) case Inlined(call, bindings, expansion) => evalExprs(bindings, thisV, klass) diff --git a/tests/init-global/warn/mutable-array.check b/tests/init-global/warn/mutable-array.check index 7618f3470433..1bc9146ceb4d 100644 --- a/tests/init-global/warn/mutable-array.check +++ b/tests/init-global/warn/mutable-array.check @@ -1,11 +1,11 @@ --- Warning: tests/init-global/warn/mutable-array.scala:8:19 ------------------------------------------------------------ -8 | val x: Int = box.value // warn +-- Warning: tests/init-global/warn/mutable-array.scala:9:19 ------------------------------------------------------------ +9 | val x: Int = box.value // warn | ^^^^^^^^^ |Reading mutable state of object A during initialization of object B. |Reading mutable state of other static objects is forbidden as it breaks initialization-time irrelevance. Calling trace: |├── object B: [ mutable-array.scala:5 ] |│ ^ - |└── val x: Int = box.value // warn [ mutable-array.scala:8 ] + |└── val x: Int = box.value // warn [ mutable-array.scala:9 ] | ^^^^^^^^^ |The mutable state is created through: |├── object A: [ mutable-array.scala:1 ] diff --git a/tests/init-global/warn/mutable-array.scala b/tests/init-global/warn/mutable-array.scala index a0ea2ea0f465..43556225a670 100644 --- a/tests/init-global/warn/mutable-array.scala +++ b/tests/init-global/warn/mutable-array.scala @@ -3,6 +3,7 @@ object A: val box: Box = new Box(0) object B: - val boxes: Array[A.Box] = Array(A.box) + val boxes = new Array[A.Box](2) + boxes(0) = A.box val box: A.Box = boxes(0) val x: Int = box.value // warn From 6b5d6a4bd593fad934ec2594a17c5905b61159a1 Mon Sep 17 00:00:00 2001 From: EnzeXing Date: Mon, 24 Feb 2025 23:29:15 -0500 Subject: [PATCH 08/10] Address comments; add TopWidenedValue --- .../tools/dotc/transform/init/Objects.scala | 127 +++++++++++------- 1 file changed, 82 insertions(+), 45 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/transform/init/Objects.scala b/compiler/src/dotty/tools/dotc/transform/init/Objects.scala index 455b5b2d76e2..71f40144b6b8 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Objects.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Objects.scala @@ -93,10 +93,11 @@ class Objects(using Context @constructorOnly): * | 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 - * | SafeValue // values on which method calls and fields won't cause warnings. Int, String, etc. + * | SafeValue // values on which method calls and field accesses won't cause warnings. Int, String, etc. + * | UnknownValue * vs ::= ValueSet(ve) // set of abstract values * Bottom ::= ValueSet(Empty) - * val ::= ve | UnknownValue | vs | Package // all possible abstract values in domain + * val ::= ve | TopWidenedValue | vs | Package // all possible abstract values in domain * Ref ::= ObjectRef | OfClass // values that represent a reference to some (global or instance) object * ThisValue ::= Ref | UnknownValue // possible values for 'this' * @@ -190,7 +191,7 @@ class Objects(using Context @constructorOnly): def show(using Context) = val valFields = vals.map(_.show + " -> " + _.show) - "OfClass(" + klass.show + ", outer = " + outer + ", args = " + args.map(_.show) + ", vals = " + valFields + ")" + "OfClass(" + klass.show + ", outer = " + outer + ", args = " + args.map(_.show) + " env = " + env.show + ", vals = " + valFields + ")" object OfClass: def apply( @@ -229,17 +230,24 @@ class Objects(using Context @constructorOnly): /** * Represents common base values like Int, String, etc. - * Assumption: all methods calls on such values should be pure (no side effects) + * Assumption: all methods calls on such values should not trigger initialization of global objects + * or read/write mutable fields */ case class SafeValue(tpe: Type) extends ValueElement: // tpe could be a AppliedType(java.lang.Class, T) val baseType = if tpe.isInstanceOf[AppliedType] then tpe.asInstanceOf[AppliedType].underlying else tpe - assert(baseType.isInstanceOf[TypeRef] && SafeValue.safeTypes.contains(baseType), "Invalid creation of SafeValue! Type = " + tpe) - val typeref = baseType.asInstanceOf[TypeRef] - def show(using Context): String = "SafeValue of type " + tpe + assert(baseType.isInstanceOf[TypeRef], "Invalid creation of SafeValue! Type = " + tpe) + val typeSymbol = baseType.asInstanceOf[TypeRef].symbol + assert(SafeValue.safeTypeSymbols.contains(typeSymbol), "Invalid creation of SafeValue! Type = " + tpe) + def show(using Context): String = "SafeValue of " + typeSymbol.show + override def equals(that: Any): Boolean = + that.isInstanceOf[SafeValue] && that.asInstanceOf[SafeValue].typeSymbol == typeSymbol object SafeValue: - val safeTypes = defn.ScalaNumericValueTypeList ++ List(defn.UnitType, defn.BooleanType, defn.StringType, defn.NullType, defn.ClassClass.typeRef) + val safeTypeSymbols = + (defn.ScalaNumericValueTypeList ++ + List(defn.UnitType, defn.BooleanType, defn.StringType.asInstanceOf[TypeRef], defn.NullType, defn.ClassClass.typeRef)) + .map(_.symbol) /** * Represents a set of values @@ -253,20 +261,26 @@ class Objects(using Context @constructorOnly): def show(using Context): String = "Package(" + packageSym.show + ")" /** Represents values unknown to the checker, such as values loaded without source + */ + case object UnknownValue extends ValueElement: + 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. * - * UnknownValue is not ValueElement since RefSet containing UnknownValue - * is equivalent to UnknownValue - */ - case object UnknownValue extends Value: - def show(using Context): String = "UnknownValue" + * TopWidenedValue is not ValueElement since RefSet containing TopWidenedValue + * is equivalent to TopWidenedValue + */ + + case object TopWidenedValue extends Value: + def show(using Context): String = "TopWidenedValue" val Bottom = ValueSet(ListSet.empty) /** Possible types for 'this' */ - type ThisValue = Ref | UnknownValue.type + type ThisValue = Ref | TopWidenedValue.type /** Checking state */ object State: @@ -658,8 +672,8 @@ class Objects(using Context @constructorOnly): extension (a: Value) def join(b: Value): Value = (a, b) match - case (UnknownValue, _) => UnknownValue - case (_, UnknownValue) => UnknownValue + case (TopWidenedValue, _) => TopWidenedValue + case (_, TopWidenedValue) => TopWidenedValue case (Package(_), _) => UnknownValue // should not happen case (_, Package(_)) => UnknownValue case (Bottom, b) => b @@ -675,8 +689,8 @@ class Objects(using Context @constructorOnly): case (a: Ref, b: Ref) if a.equals(b) => Bottom case _ => a - def widen(height: Int)(using Context): Value = - if height == 0 then UnknownValue + def widen(height: Int)(using Context): Value = log("widening value " + a.show + " down to height " + height, printer, (_: Value).show) { + if height == 0 then TopWidenedValue else a match case Bottom => Bottom @@ -694,6 +708,7 @@ class Objects(using Context @constructorOnly): ref.widenedCopy(outer2, args2, env2) case _ => a + } def filterType(tpe: Type)(using Context): Value = tpe match @@ -706,21 +721,24 @@ class Objects(using Context @constructorOnly): // Filter the value according to a class symbol, and only leaves the sub-values // which could represent an object of the given class def filterClass(sym: Symbol)(using Context): Value = - if !sym.isClass then a - else - val klass = sym.asClass - a match - case UnknownValue => UnknownValue - case Package(_) => a - case SafeValue(_) => a - 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: Ref | UnknownValue.type) - def widenRefOrCold(height : Int)(using Context) : Ref | UnknownValue.type = value.widen(height).asInstanceOf[ThisValue] + if !sym.isClass then a + else + val klass = sym.asClass + a match + case UnknownValue | TopWidenedValue => a + case Package(packageSym) => + if packageSym.moduleClass.equals(sym) || (klass.denot.isPackageObject && klass.owner.equals(sym)) then a else Bottom + 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 widenRefOrCold(height : Int)(using Context) : ThisValue = + assert(height > 0, "Cannot call widenRefOrCold with height 0!") + value.widen(height).asInstanceOf[ThisValue] extension (values: Iterable[Value]) def join: Value = if values.isEmpty then Bottom else values.reduce { (v1, v2) => v1.join(v2) } @@ -743,6 +761,9 @@ 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 TopWidenedValue => + report.warning("Value is unknown to the checker due to widening. " + Trace.show, Trace.position) + Bottom case UnknownValue => if reportUnknown then report.warning("Using unknown value. " + Trace.show, Trace.position) @@ -751,10 +772,12 @@ class Objects(using Context @constructorOnly): UnknownValue case Package(packageSym) => + if meth.equals(defn.throwMethod) then + Bottom // calls on packages are unexpected. However the typer might mistakenly // set the receiver to be a package instead of package object. // See packageObjectStringInterpolator.scala - if !meth.owner.denot.isPackageObject then + else if !meth.owner.denot.isPackageObject then report.warning("[Internal error] Unexpected call on package = " + value.show + ", meth = " + meth.show + Trace.show, Trace.position) Bottom else @@ -764,13 +787,13 @@ class Objects(using Context @constructorOnly): case v @ SafeValue(tpe) => // Assume such method is pure. Check return type, only try to analyze body if return type is not safe - val target = resolve(v.typeref.symbol.asClass, meth) + val target = resolve(v.typeSymbol.asClass, meth) if !target.hasSource then UnknownValue else val ddef = target.defTree.asInstanceOf[DefDef] val returnType = ddef.tpt.tpe - if SafeValue.safeTypes.contains(returnType) then + if SafeValue.safeTypeSymbols.contains(returnType.typeSymbol) then // since method is pure and return type is safe, no need to analyze method body SafeValue(returnType) else @@ -835,7 +858,7 @@ class Objects(using Context @constructorOnly): if meth.owner.isClass then (ref, Env.NoEnv) else - Env.resolveEnvByOwner(meth.owner.enclosingMethod, ref, summon[Env.Data]).getOrElse(UnknownValue -> Env.NoEnv) + Env.resolveEnvByOwner(meth.owner.enclosingMethod, ref, summon[Env.Data]).getOrElse(TopWidenedValue -> Env.NoEnv) val env2 = Env.ofDefDef(ddef, args.map(_.value), outerEnv) extendTrace(ddef) { @@ -933,6 +956,9 @@ 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 TopWidenedValue => + report.warning("Value is unknown to the checker due to widening. " + Trace.show, Trace.position) + Bottom case UnknownValue => if reportUnknown then report.warning("Using unknown value. " + Trace.show, Trace.position) @@ -977,7 +1003,7 @@ class Objects(using Context @constructorOnly): Bottom else // initialization error, reported by the initialization checker - UnknownValue + Bottom else if ref.hasVal(target) then ref.valValue(target) else if ref.isObjectRef && ref.klass.hasSource then @@ -985,7 +1011,7 @@ class Objects(using Context @constructorOnly): Bottom else // initialization error, reported by the initialization checker - UnknownValue + Bottom else if ref.klass.isSubClass(receiver.widenSingleton.classSymbol) then @@ -1019,6 +1045,12 @@ 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 TopWidenedValue => + report.warning("Value is unknown to the checker due to widening. " + Trace.show, Trace.position) + case UnknownValue => + if reportUnknown then + report.warning("Assigning to unknown value. " + Trace.show, Trace.position) + end if case p: Package => report.warning("[Internal error] unexpected tree in assignment, package = " + p.packageSym.show + Trace.show, Trace.position) case fun: Fun => @@ -1026,8 +1058,8 @@ class Objects(using Context @constructorOnly): case arr: OfArray => report.warning("[Internal error] unexpected tree in assignment, array = " + arr.show + " field = " + field + Trace.show, Trace.position) - case SafeValue(_) | UnknownValue => - report.warning("Assigning to base or unknown value is forbidden. " + Trace.show, Trace.position) + case SafeValue(_) => + report.warning("Assigning to base value is forbidden. " + Trace.show, Trace.position) case ValueSet(values) => values.foreach(ref => assign(ref, field, rhs, rhsTyp)) @@ -1063,9 +1095,13 @@ class Objects(using Context @constructorOnly): Bottom case UnknownValue => - UnknownValue + if reportUnknown then + report.warning("Instantiating when outer is unknown. " + Trace.show, Trace.position) + Bottom + else + UnknownValue - case outer: (Ref | UnknownValue.type | Package) => + case outer: (Ref | TopWidenedValue.type | Package) => if klass == defn.ArrayClass then args.head.tree.tpe match case ConstantType(Constants.Constant(0)) => @@ -1081,7 +1117,7 @@ class Objects(using Context @constructorOnly): outer match case Package(_) => // For top-level classes (outer, Env.NoEnv) - case thisV : (Ref | UnknownValue.type) => + 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) @@ -1152,7 +1188,7 @@ class Objects(using Context @constructorOnly): case fun: Fun => given Env.Data = Env.ofByName(sym, fun.env) eval(fun.code, fun.thisV, fun.klass) - case UnknownValue => + case UnknownValue | TopWidenedValue => report.warning("Calling on unknown value. " + Trace.show, Trace.position) Bottom case _: ValueSet | _: Ref | _: OfArray | _: Package | SafeValue(_) => @@ -1929,6 +1965,7 @@ class Objects(using Context @constructorOnly): thisV match case Bottom => Bottom case UnknownValue => UnknownValue + case TopWidenedValue => TopWidenedValue case ref: Ref => val outerCls = klass.owner.lexicallyEnclosingClass.asClass if !ref.hasOuter(klass) then From e72efedd469d69d0e21accd1a37276f9cdfda0f5 Mon Sep 17 00:00:00 2001 From: EnzeXing Date: Tue, 25 Feb 2025 23:30:20 -0500 Subject: [PATCH 09/10] Reorganize tests --- compiler/src/dotty/tools/dotc/Compiler.scala | 2 +- .../tools/dotc/config/ScalaSettings.scala | 2 +- .../src/dotty/tools/dotc/core/Symbols.scala | 2 +- .../tools/dotc/transform/init/Checker.scala | 4 ++-- .../tools/dotc/transform/init/Objects.scala | 9 ++++++--- ...it-global-scala2-library-tasty.excludelist | 3 --- .../dotty/tools/dotc/CompilationTests.scala | 8 ++++++-- tests/init-global/pos-tasty/simple.scala | 3 +++ tests/init-global/pos/arithmetic.scala | 3 +++ .../{warn => warn-tasty}/patmat.check | 0 .../{warn => warn-tasty}/patmat.scala | 0 .../unapplySeq-implicit-arg.check | 2 +- .../unapplySeq-implicit-arg.scala | 0 .../unapplySeq-implicit-arg2.check | 2 +- .../unapplySeq-implicit-arg2.scala | 0 .../unapplySeq-implicit-arg3.check | 2 +- .../unapplySeq-implicit-arg3.scala | 0 tests/init-global/warn/widen.check | 20 +++++++++++++++++++ tests/init-global/warn/widen.scala | 20 +++++++++++++++++++ 19 files changed, 66 insertions(+), 16 deletions(-) create mode 100644 tests/init-global/pos-tasty/simple.scala create mode 100644 tests/init-global/pos/arithmetic.scala rename tests/init-global/{warn => warn-tasty}/patmat.check (100%) rename tests/init-global/{warn => warn-tasty}/patmat.scala (100%) rename tests/init-global/{warn => warn-tasty}/unapplySeq-implicit-arg.check (77%) rename tests/init-global/{warn => warn-tasty}/unapplySeq-implicit-arg.scala (100%) rename tests/init-global/{warn => warn-tasty}/unapplySeq-implicit-arg2.check (80%) rename tests/init-global/{warn => warn-tasty}/unapplySeq-implicit-arg2.scala (100%) rename tests/init-global/{warn => warn-tasty}/unapplySeq-implicit-arg3.check (84%) rename tests/init-global/{warn => warn-tasty}/unapplySeq-implicit-arg3.scala (100%) create mode 100644 tests/init-global/warn/widen.check create mode 100644 tests/init-global/warn/widen.scala diff --git a/compiler/src/dotty/tools/dotc/Compiler.scala b/compiler/src/dotty/tools/dotc/Compiler.scala index 71eb29ad1063..6aab7d54d59e 100644 --- a/compiler/src/dotty/tools/dotc/Compiler.scala +++ b/compiler/src/dotty/tools/dotc/Compiler.scala @@ -171,7 +171,7 @@ class Compiler { val rctx = if ctx.settings.Xsemanticdb.value then ctx.addMode(Mode.ReadPositions) - else if !ctx.settings.YcheckInitGlobal.isDefault then + else if ctx.settings.YcheckInitGlobal.value then ctx.addMode(Mode.ReadPositions) else ctx diff --git a/compiler/src/dotty/tools/dotc/config/ScalaSettings.scala b/compiler/src/dotty/tools/dotc/config/ScalaSettings.scala index 28b848d6d5b2..c6c0ab47de52 100644 --- a/compiler/src/dotty/tools/dotc/config/ScalaSettings.scala +++ b/compiler/src/dotty/tools/dotc/config/ScalaSettings.scala @@ -446,7 +446,7 @@ private sealed trait YSettings: val YnoKindPolymorphism: Setting[Boolean] = BooleanSetting(ForkSetting, "Yno-kind-polymorphism", "Disable kind polymorphism.") val YexplicitNulls: Setting[Boolean] = BooleanSetting(ForkSetting, "Yexplicit-nulls", "Make reference types non-nullable. Nullable types can be expressed with unions: e.g. String|Null.") val YnoFlexibleTypes: Setting[Boolean] = BooleanSetting(ForkSetting, "Yno-flexible-types", "Disable turning nullable Java return types and parameter types into flexible types, which behave like abstract types with a nullable lower bound and non-nullable upper bound.") - val YcheckInitGlobal: Setting[String] = ChoiceSetting(ForkSetting, "Ysafe-init-global", "[report-unknown, ignore-unknown]", "Check safe initialization of global objects.", List("report-unknown", "ignore-unknown", "off"), "off") + val YcheckInitGlobal: Setting[Boolean] = BooleanSetting(ForkSetting, "Ysafe-init-global", "Check safe initialization of global objects.") val YrequireTargetName: Setting[Boolean] = BooleanSetting(ForkSetting, "Yrequire-targetName", "Warn if an operator is defined without a @targetName annotation.") val YrecheckTest: Setting[Boolean] = BooleanSetting(ForkSetting, "Yrecheck-test", "Run basic rechecking (internal test only).") val YccDebug: Setting[Boolean] = BooleanSetting(ForkSetting, "Ycc-debug", "Used in conjunction with captureChecking language import, debug info for captured references.") diff --git a/compiler/src/dotty/tools/dotc/core/Symbols.scala b/compiler/src/dotty/tools/dotc/core/Symbols.scala index b53e103bfab0..7de75e371752 100644 --- a/compiler/src/dotty/tools/dotc/core/Symbols.scala +++ b/compiler/src/dotty/tools/dotc/core/Symbols.scala @@ -85,7 +85,7 @@ object Symbols extends SymUtils { denot.owner.isTerm || // no risk of leaking memory after a run for these denot.isOneOf(InlineOrProxy) || // need to keep inline info ctx.settings.Whas.checkInit || // initialization check - !ctx.settings.YcheckInitGlobal.isDefault + ctx.settings.YcheckInitGlobal.value /** The last denotation of this symbol */ private var lastDenot: SymDenotation = uninitialized diff --git a/compiler/src/dotty/tools/dotc/transform/init/Checker.scala b/compiler/src/dotty/tools/dotc/transform/init/Checker.scala index b8afabf1efc7..4d5c467cf4fe 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Checker.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Checker.scala @@ -29,7 +29,7 @@ class Checker extends Phase: override val runsAfter = Set(Pickler.name) override def isEnabled(using Context): Boolean = - super.isEnabled && (ctx.settings.Whas.checkInit || !ctx.settings.YcheckInitGlobal.isDefault) + super.isEnabled && (ctx.settings.Whas.checkInit || ctx.settings.YcheckInitGlobal.value) def traverse(traverser: InitTreeTraverser)(using Context): Boolean = monitor(phaseName): val unit = ctx.compilationUnit @@ -53,7 +53,7 @@ class Checker extends Phase: if ctx.settings.Whas.checkInit then Semantic.checkClasses(classes)(using checkCtx) - if !ctx.settings.YcheckInitGlobal.isDefault then + if ctx.settings.YcheckInitGlobal.value then val obj = new Objects obj.checkClasses(classes)(using checkCtx) } diff --git a/compiler/src/dotty/tools/dotc/transform/init/Objects.scala b/compiler/src/dotty/tools/dotc/transform/init/Objects.scala index 71f40144b6b8..18cc813d1290 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Objects.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Objects.scala @@ -747,8 +747,8 @@ class Objects(using Context @constructorOnly): /** Check if the checker option reports warnings about unknown code */ - def reportUnknown(using context: Context): Boolean = - context.settings.YcheckInitGlobal.value == "report-unknown" + val reportUnknown: Boolean = false + /** Handle method calls `e.m(args)`. * @@ -967,7 +967,10 @@ class Objects(using Context @constructorOnly): UnknownValue case v @ SafeValue(_) => - report.warning("[Internal error] Unexpected selection on safe value " + v.show + ", field = " + field.show + Trace.show, Trace.position) + if v.typeSymbol != defn.NullClass then + // selection on Null is sensible on AST level; no warning for it + report.warning("[Internal error] Unexpected selection on safe value " + v.show + ", field = " + field.show + ". " + Trace.show, Trace.position) + end if Bottom case Package(packageSym) => 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 03b020db64d9..18a665e0119b 100644 --- a/compiler/test/dotc/neg-init-global-scala2-library-tasty.excludelist +++ b/compiler/test/dotc/neg-init-global-scala2-library-tasty.excludelist @@ -1,9 +1,6 @@ ## See #18882 patmat.scala t9312.scala -unapplySeq-implicit-arg.scala -unapplySeq-implicit-arg2.scala -unapplySeq-implicit-arg3.scala ScalaCheck.scala mutable-read8.scala TypeCast.scala diff --git a/compiler/test/dotty/tools/dotc/CompilationTests.scala b/compiler/test/dotty/tools/dotc/CompilationTests.scala index b35ecd0983c3..c780944378eb 100644 --- a/compiler/test/dotty/tools/dotc/CompilationTests.scala +++ b/compiler/test/dotty/tools/dotc/CompilationTests.scala @@ -231,8 +231,12 @@ class CompilationTests { // initialization tests @Test def checkInitGlobal: Unit = { implicit val testGroup: TestGroup = TestGroup("checkInitGlobal") - compileFilesInDir("tests/init-global/warn", defaultOptions.and("-Ysafe-init-global:ignore-unknown"), FileFilter.exclude(TestSources.negInitGlobalScala2LibraryTastyExcludelisted)).checkWarnings() - compileFilesInDir("tests/init-global/pos", defaultOptions.and("-Ysafe-init-global:ignore-unknown", "-Xfatal-warnings"), FileFilter.exclude(TestSources.posInitGlobalScala2LibraryTastyExcludelisted)).checkCompile() + compileFilesInDir("tests/init-global/warn", defaultOptions.and("-Ysafe-init-global"), FileFilter.exclude(TestSources.negInitGlobalScala2LibraryTastyExcludelisted)).checkWarnings() + compileFilesInDir("tests/init-global/pos", defaultOptions.and("-Ysafe-init-global", "-Xfatal-warnings"), FileFilter.exclude(TestSources.posInitGlobalScala2LibraryTastyExcludelisted)).checkCompile() + if Properties.usingScalaLibraryTasty then + compileFilesInDir("tests/init-global/warn-tasty", defaultOptions.and("-Ysafe-init-global"), FileFilter.exclude(TestSources.negInitGlobalScala2LibraryTastyExcludelisted)).checkWarnings() + compileFilesInDir("tests/init-global/pos-tasty", defaultOptions.and("-Ysafe-init-global", "-Xfatal-warnings"), FileFilter.exclude(TestSources.posInitGlobalScala2LibraryTastyExcludelisted)).checkCompile() + end if } // initialization tests diff --git a/tests/init-global/pos-tasty/simple.scala b/tests/init-global/pos-tasty/simple.scala new file mode 100644 index 000000000000..b3bd10e7ea42 --- /dev/null +++ b/tests/init-global/pos-tasty/simple.scala @@ -0,0 +1,3 @@ +object O: + val a: Int = 5 + val b = a.toDouble \ No newline at end of file diff --git a/tests/init-global/pos/arithmetic.scala b/tests/init-global/pos/arithmetic.scala new file mode 100644 index 000000000000..c9a3d74faccb --- /dev/null +++ b/tests/init-global/pos/arithmetic.scala @@ -0,0 +1,3 @@ +object A: + val a = f(10) + def f(x: Int) = x * 2 + 5 \ No newline at end of file diff --git a/tests/init-global/warn/patmat.check b/tests/init-global/warn-tasty/patmat.check similarity index 100% rename from tests/init-global/warn/patmat.check rename to tests/init-global/warn-tasty/patmat.check diff --git a/tests/init-global/warn/patmat.scala b/tests/init-global/warn-tasty/patmat.scala similarity index 100% rename from tests/init-global/warn/patmat.scala rename to tests/init-global/warn-tasty/patmat.scala diff --git a/tests/init-global/warn/unapplySeq-implicit-arg.check b/tests/init-global/warn-tasty/unapplySeq-implicit-arg.check similarity index 77% rename from tests/init-global/warn/unapplySeq-implicit-arg.check rename to tests/init-global/warn-tasty/unapplySeq-implicit-arg.check index ec08187f058f..92bd3871c5d2 100644 --- a/tests/init-global/warn/unapplySeq-implicit-arg.check +++ b/tests/init-global/warn-tasty/unapplySeq-implicit-arg.check @@ -1,4 +1,4 @@ --- Warning: tests/init-global/warn/unapplySeq-implicit-arg.scala:11:20 ------------------------------------------------- +-- Warning: tests/init-global/warn-tasty/unapplySeq-implicit-arg.scala:11:20 ------------------------------------------- 11 | val i2: Int = Seq(i2) match // warn | ^^ | Access uninitialized field value i2. Calling trace: diff --git a/tests/init-global/warn/unapplySeq-implicit-arg.scala b/tests/init-global/warn-tasty/unapplySeq-implicit-arg.scala similarity index 100% rename from tests/init-global/warn/unapplySeq-implicit-arg.scala rename to tests/init-global/warn-tasty/unapplySeq-implicit-arg.scala diff --git a/tests/init-global/warn/unapplySeq-implicit-arg2.check b/tests/init-global/warn-tasty/unapplySeq-implicit-arg2.check similarity index 80% rename from tests/init-global/warn/unapplySeq-implicit-arg2.check rename to tests/init-global/warn-tasty/unapplySeq-implicit-arg2.check index e75b66495d4b..07a9bd97001c 100644 --- a/tests/init-global/warn/unapplySeq-implicit-arg2.check +++ b/tests/init-global/warn-tasty/unapplySeq-implicit-arg2.check @@ -1,4 +1,4 @@ --- Warning: tests/init-global/warn/unapplySeq-implicit-arg2.scala:4:9 -------------------------------------------------- +-- Warning: tests/init-global/warn-tasty/unapplySeq-implicit-arg2.scala:4:9 -------------------------------------------- 4 | Some(i1 +: seqi) // warn | ^^ |Access uninitialized field value i1. Calling trace: diff --git a/tests/init-global/warn/unapplySeq-implicit-arg2.scala b/tests/init-global/warn-tasty/unapplySeq-implicit-arg2.scala similarity index 100% rename from tests/init-global/warn/unapplySeq-implicit-arg2.scala rename to tests/init-global/warn-tasty/unapplySeq-implicit-arg2.scala diff --git a/tests/init-global/warn/unapplySeq-implicit-arg3.check b/tests/init-global/warn-tasty/unapplySeq-implicit-arg3.check similarity index 84% rename from tests/init-global/warn/unapplySeq-implicit-arg3.check rename to tests/init-global/warn-tasty/unapplySeq-implicit-arg3.check index 7674298a3665..ca8e4ed83f8d 100644 --- a/tests/init-global/warn/unapplySeq-implicit-arg3.check +++ b/tests/init-global/warn-tasty/unapplySeq-implicit-arg3.check @@ -1,4 +1,4 @@ --- Warning: tests/init-global/warn/unapplySeq-implicit-arg3.scala:3:27 ------------------------------------------------- +-- Warning: tests/init-global/warn-tasty/unapplySeq-implicit-arg3.scala:3:27 ------------------------------------------- 3 | def m(seq: Seq[Int]) = i1 +: seq // warn | ^^ |Access uninitialized field value i1. Calling trace: diff --git a/tests/init-global/warn/unapplySeq-implicit-arg3.scala b/tests/init-global/warn-tasty/unapplySeq-implicit-arg3.scala similarity index 100% rename from tests/init-global/warn/unapplySeq-implicit-arg3.scala rename to tests/init-global/warn-tasty/unapplySeq-implicit-arg3.scala diff --git a/tests/init-global/warn/widen.check b/tests/init-global/warn/widen.check new file mode 100644 index 000000000000..b3191d023110 --- /dev/null +++ b/tests/init-global/warn/widen.check @@ -0,0 +1,20 @@ +-- 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 ] + | ^^^^^^^ diff --git a/tests/init-global/warn/widen.scala b/tests/init-global/warn/widen.scala new file mode 100644 index 000000000000..157434a0f3e4 --- /dev/null +++ b/tests/init-global/warn/widen.scala @@ -0,0 +1,20 @@ +trait T { + def foo(): Unit +} + +class C extends T { + def foo(): Unit = println("Calling foo on an instance of C!") +} + +object O: + def bar(t: T) = { + class A { + class B { + t.foo() // warn + } + + val b = new B + } + new A + } + val a = bar(new C) \ No newline at end of file From 773880f6ba43e358c9629b35422561c8249a2937 Mon Sep 17 00:00:00 2001 From: EnzeXing Date: Mon, 3 Mar 2025 22:51:11 -0500 Subject: [PATCH 10/10] Address comments --- .../tools/dotc/transform/init/Objects.scala | 209 ++++++++++-------- .../dotty/tools/dotc/CompilationTests.scala | 2 +- .../{pos => pos-tasty}/array-size-zero.scala | 0 tests/init-global/pos-tasty/simple.scala | 3 - tests/init-global/pos/arithmetic.scala | 2 + 5 files changed, 125 insertions(+), 91 deletions(-) rename tests/init-global/{pos => pos-tasty}/array-size-zero.scala (100%) delete mode 100644 tests/init-global/pos-tasty/simple.scala diff --git a/compiler/src/dotty/tools/dotc/transform/init/Objects.scala b/compiler/src/dotty/tools/dotc/transform/init/Objects.scala index 18cc813d1290..b5d70eeb16b2 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Objects.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Objects.scala @@ -18,7 +18,6 @@ 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.* @@ -30,6 +29,7 @@ import scala.collection.mutable import scala.annotation.tailrec import scala.annotation.constructorOnly import dotty.tools.dotc.core.Flags.AbstractOrTrait +import dotty.tools.dotc.util.SrcPos /** Check initialization safety of static objects * @@ -55,10 +55,10 @@ import dotty.tools.dotc.core.Flags.AbstractOrTrait * 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. + * 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. * * 3. It is inter-procedural and flow-sensitive. * @@ -94,12 +94,11 @@ class Objects(using Context @constructorOnly): * | OfArray(object[owner], regions) * | Fun(..., env) // 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 * vs ::= ValueSet(ve) // set of abstract values * Bottom ::= ValueSet(Empty) - * val ::= ve | TopWidenedValue | vs | Package // all possible abstract values in domain + * 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 | UnknownValue // possible values for 'this' + * 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 @@ -233,36 +232,59 @@ class Objects(using Context @constructorOnly): * Assumption: all methods calls on such values should not trigger initialization of global objects * or read/write mutable fields */ - case class SafeValue(tpe: Type) extends ValueElement: - // tpe could be a AppliedType(java.lang.Class, T) - val baseType = if tpe.isInstanceOf[AppliedType] then tpe.asInstanceOf[AppliedType].underlying else tpe - assert(baseType.isInstanceOf[TypeRef], "Invalid creation of SafeValue! Type = " + tpe) - val typeSymbol = baseType.asInstanceOf[TypeRef].symbol - assert(SafeValue.safeTypeSymbols.contains(typeSymbol), "Invalid creation of SafeValue! Type = " + tpe) + case class SafeValue(typeSymbol: Symbol) extends ValueElement: + assert(SafeValue.safeTypeSymbols.contains(typeSymbol), "Invalid creation of SafeValue! Type = " + typeSymbol) def show(using Context): String = "SafeValue of " + typeSymbol.show - override def equals(that: Any): Boolean = - that.isInstanceOf[SafeValue] && that.asInstanceOf[SafeValue].typeSymbol == typeSymbol object SafeValue: val safeTypeSymbols = + defn.StringClass :: (defn.ScalaNumericValueTypeList ++ - List(defn.UnitType, defn.BooleanType, defn.StringType.asInstanceOf[TypeRef], defn.NullType, defn.ClassClass.typeRef)) + List(defn.UnitType, defn.BooleanType, defn.NullType, defn.ClassClass.typeRef)) .map(_.symbol) + def getSafeTypeSymbol(tpe: Type): Option[Symbol] = + val baseType = if tpe.isInstanceOf[AppliedType] then tpe.asInstanceOf[AppliedType].underlying else tpe + if baseType.isInstanceOf[TypeRef] then + val typeRef = baseType.asInstanceOf[TypeRef] + val typeSymbol = typeRef.symbol + val typeAlias = typeRef.translucentSuperType + if safeTypeSymbols.contains(typeSymbol) then + Some(typeSymbol) + else if typeAlias.isInstanceOf[TypeRef] && typeAlias.asInstanceOf[TypeRef].symbol == defn.StringClass then + // Special case, type scala.Predef.String = java.lang.String + Some(defn.StringClass) + else None + else + None + + def apply(tpe: Type): SafeValue = + // tpe could be a AppliedType(java.lang.Class, T) + val typeSymbol = getSafeTypeSymbol(tpe) + assert(typeSymbol.isDefined, "Invalid creation of SafeValue with type " + tpe) + new SafeValue(typeSymbol.get) + /** * Represents a set of values * * It comes from `if` expressions. */ - case class ValueSet(values: ListSet[ValueElement]) extends Value: + case class ValueSet(values: Set[ValueElement]) extends Value: def show(using Context) = values.map(_.show).mkString("[", ",", "]") - case class Package(packageSym: Symbol) extends Value: - def show(using Context): String = "Package(" + packageSym.show + ")" + case class Package(packageModuleClass: ClassSymbol) extends Value: + def show(using Context): String = "Package(" + packageModuleClass.show + ")" + + object Package: + def apply(packageSym: Symbol): Package = + 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 ValueElement: + case object UnknownValue extends Value: def show(using Context): String = "UnknownValue" /** Represents values lost due to widening @@ -270,17 +292,17 @@ class Objects(using Context @constructorOnly): * This is the top of the abstract domain lattice, which should not * be used during initialization. * - * TopWidenedValue is not ValueElement since RefSet containing TopWidenedValue - * is equivalent to TopWidenedValue + * Top is not ValueElement since RefSet containing Top + * is equivalent to Top */ - case object TopWidenedValue extends Value: - def show(using Context): String = "TopWidenedValue" + case object Top extends Value: + def show(using Context): String = "Top" val Bottom = ValueSet(ListSet.empty) /** Possible types for 'this' */ - type ThisValue = Ref | TopWidenedValue.type + type ThisValue = Ref | Top.type /** Checking state */ object State: @@ -671,26 +693,30 @@ class Objects(using Context @constructorOnly): 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 (TopWidenedValue, _) => TopWidenedValue - case (_, TopWidenedValue) => TopWidenedValue - case (Package(_), _) => UnknownValue // should not happen - case (_, Package(_)) => UnknownValue + 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(ListSet(a, b)) + case (a : ValueElement, b : ValueElement) => ValueSet(Set(a, b)) + case _ => Bottom 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)) case (a: Ref, b: Ref) if a.equals(b) => Bottom + case (a: SafeValue, b: SafeValue) if a == b => Bottom + 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 TopWidenedValue + if height == 0 then Top else a match case Bottom => Bottom @@ -699,7 +725,7 @@ class Objects(using Context @constructorOnly): values.map(ref => ref.widen(height)).join case Fun(code, thisV, klass, env) => - Fun(code, thisV.widenRefOrCold(height), klass, env.widen(height - 1)) + Fun(code, thisV.widenThisValue(height), klass, env.widen(height - 1)) case ref @ OfClass(klass, outer, _, args, env) => val outer2 = outer.widen(height - 1) @@ -725,9 +751,11 @@ class Objects(using Context @constructorOnly): else val klass = sym.asClass a match - case UnknownValue | TopWidenedValue => a - case Package(packageSym) => - if packageSym.moduleClass.equals(sym) || (klass.denot.isPackageObject && klass.owner.equals(sym)) then a else Bottom + case UnknownValue | Top => a + case Package(packageModuleClass) => + // the typer might mistakenly set the receiver to be a package instead of package object. + // See pos/packageObjectStringInterpolator.scala + if packageModuleClass == klass || (klass.denot.isPackageObject && klass.owner == packageModuleClass) then a else Bottom 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 @@ -736,8 +764,8 @@ class Objects(using Context @constructorOnly): if klass.isOneOf(AbstractOrTrait) && klass.baseClasses.exists(defn.isFunctionClass) then fun else Bottom extension (value: ThisValue) - def widenRefOrCold(height : Int)(using Context) : ThisValue = - assert(height > 0, "Cannot call widenRefOrCold with height 0!") + def widenThisValue(height : Int)(using Context) : ThisValue = + assert(height > 0, "Cannot call widenThisValue with height 0!") value.widen(height).asInstanceOf[ThisValue] extension (values: Iterable[Value]) @@ -749,6 +777,12 @@ class Objects(using Context @constructorOnly): */ val reportUnknown: Boolean = false + def reportWarningForUnknownValue(msg: => String, pos: SrcPos)(using Context): Value = + if reportUnknown then + report.warning(msg, pos) + Bottom + else + UnknownValue /** Handle method calls `e.m(args)`. * @@ -761,42 +795,51 @@ 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 TopWidenedValue => + case Top => report.warning("Value is unknown to the checker due to widening. " + Trace.show, Trace.position) Bottom case UnknownValue => - if reportUnknown then - report.warning("Using unknown value. " + Trace.show, Trace.position) - Bottom - else - UnknownValue + reportWarningForUnknownValue("Using unknown value. " + Trace.show, Trace.position) - case Package(packageSym) => + case Package(packageModuleClass) => if meth.equals(defn.throwMethod) then Bottom - // calls on packages are unexpected. However the typer might mistakenly - // set the receiver to be a package instead of package object. - // See packageObjectStringInterpolator.scala - else if !meth.owner.denot.isPackageObject then - report.warning("[Internal error] Unexpected call on package = " + value.show + ", meth = " + meth.show + Trace.show, Trace.position) - Bottom - else + else if meth.owner.denot.isPackageObject then + // calls on packages are unexpected. However the typer might mistakenly + // set the receiver to be a package instead of package object. + // See packageObjectStringInterpolator.scala // Method call on package object instead val packageObj = accessObject(meth.owner.moduleClass.asClass) call(packageObj, meth, args, receiver, superType, needResolve) + else + report.warning("[Internal error] Unexpected call on package = " + value.show + ", meth = " + meth.show + Trace.show, Trace.position) + Bottom - case v @ SafeValue(tpe) => - // Assume such method is pure. Check return type, only try to analyze body if return type is not safe - val target = resolve(v.typeSymbol.asClass, meth) - if !target.hasSource then - UnknownValue + case v @ SafeValue(_) => + if v.typeSymbol == defn.NullClass then + // call on Null is sensible on AST level but not in practice + Bottom else - val ddef = target.defTree.asInstanceOf[DefDef] - val returnType = ddef.tpt.tpe - if SafeValue.safeTypeSymbols.contains(returnType.typeSymbol) then + // Assume such method is pure. Check return type, only try to analyze body if return type is not safe + val target = resolve(v.typeSymbol.asClass, meth) + val targetType = target.denot.info + assert(targetType.isInstanceOf[ExprType] || targetType.isInstanceOf[MethodType], + "Unexpected type! Receiver = " + v.show + ", meth = " + target + ", type = " + targetType) + val returnType = + if targetType.isInstanceOf[ExprType] then + // corresponds to parameterless method like `def meth: ExprType[T]` + // See pos/toDouble.scala + targetType.asInstanceOf[ExprType].resType + else + targetType.asInstanceOf[MethodType].resType + val typeSymbol = SafeValue.getSafeTypeSymbol(returnType) + if typeSymbol.isDefined then // since method is pure and return type is safe, no need to analyze method body - SafeValue(returnType) + SafeValue(typeSymbol.get) + else if !target.hasSource then + UnknownValue else + 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) @@ -858,7 +901,7 @@ class Objects(using Context @constructorOnly): if meth.owner.isClass then (ref, Env.NoEnv) else - Env.resolveEnvByOwner(meth.owner.enclosingMethod, ref, summon[Env.Data]).getOrElse(TopWidenedValue -> Env.NoEnv) + Env.resolveEnvByOwner(meth.owner.enclosingMethod, ref, summon[Env.Data]).getOrElse(Top -> Env.NoEnv) val env2 = Env.ofDefDef(ddef, args.map(_.value), outerEnv) extendTrace(ddef) { @@ -899,7 +942,7 @@ class Objects(using Context @constructorOnly): value else // In future, we will have Tasty for stdlib classes and can abstractly interpret that Tasty. - // For now, return `Cold` to ensure soundness and trigger a warning. + // For now, return `UnknownValue` to ensure soundness and trigger a warning when reportUnknown = true. UnknownValue end if end if @@ -956,15 +999,11 @@ 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 TopWidenedValue => + case Top => report.warning("Value is unknown to the checker due to widening. " + Trace.show, Trace.position) Bottom case UnknownValue => - if reportUnknown then - report.warning("Using unknown value. " + Trace.show, Trace.position) - Bottom - else - UnknownValue + reportWarningForUnknownValue("Using unknown value. " + Trace.show, Trace.position) case v @ SafeValue(_) => if v.typeSymbol != defn.NullClass then @@ -973,13 +1012,13 @@ class Objects(using Context @constructorOnly): end if Bottom - case Package(packageSym) => + case Package(packageModuleClass) => if field.isStaticObject then accessObject(field.moduleClass.asClass) else if field.is(Flags.Package) then Package(field) else - report.warning("[Internal error] Unexpected selection on package " + packageSym.show + ", field = " + field.show + Trace.show, Trace.position) + report.warning("[Internal error] Unexpected selection on package " + packageModuleClass.show + ", field = " + field.show + Trace.show, Trace.position) Bottom case ref: Ref => @@ -1048,14 +1087,12 @@ 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 TopWidenedValue => + case Top => report.warning("Value is unknown to the checker due to widening. " + Trace.show, Trace.position) case UnknownValue => - if reportUnknown then - report.warning("Assigning to unknown value. " + Trace.show, Trace.position) - end if + val _ = reportWarningForUnknownValue("Assigning to unknown value. " + Trace.show, Trace.position) case p: Package => - report.warning("[Internal error] unexpected tree in assignment, package = " + p.packageSym.show + Trace.show, Trace.position) + report.warning("[Internal error] unexpected tree in assignment, package = " + p.show + Trace.show, Trace.position) case fun: Fun => report.warning("[Internal error] unexpected tree in assignment, fun = " + fun.code.show + Trace.show, Trace.position) case arr: OfArray => @@ -1098,13 +1135,9 @@ class Objects(using Context @constructorOnly): Bottom case UnknownValue => - if reportUnknown then - report.warning("Instantiating when outer is unknown. " + Trace.show, Trace.position) - Bottom - else - UnknownValue + reportWarningForUnknownValue("Instantiating when outer is unknown. " + Trace.show, Trace.position) - case outer: (Ref | TopWidenedValue.type | Package) => + case outer: (Ref | Top.type | Package) => if klass == defn.ArrayClass then args.head.tree.tpe match case ConstantType(Constants.Constant(0)) => @@ -1126,7 +1159,7 @@ class Objects(using Context @constructorOnly): 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.widenRefOrCold(1), Env.NoEnv) + (thisV.widenThisValue(1), Env.NoEnv) else // klass.enclosingMethod returns its primary constructor Env.resolveEnvByOwner(klass.owner.enclosingMethod, thisV, summon[Env.Data]).getOrElse(UnknownValue -> Env.NoEnv) @@ -1191,8 +1224,10 @@ class Objects(using Context @constructorOnly): case fun: Fun => given Env.Data = Env.ofByName(sym, fun.env) eval(fun.code, fun.thisV, fun.klass) - case UnknownValue | TopWidenedValue => - report.warning("Calling on unknown value. " + Trace.show, Trace.position) + 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) @@ -1645,7 +1680,7 @@ class Objects(using Context @constructorOnly): end if end if // TODO: receiverType is the companion object type, not the class itself; - // cannot filter scritunee by this type + // cannot filter scrutinee by this type (receiverType, scrutinee) case Ident(nme.WILDCARD) | Ident(nme.WILDCARD_STAR) => @@ -1968,7 +2003,7 @@ class Objects(using Context @constructorOnly): thisV match case Bottom => Bottom case UnknownValue => UnknownValue - case TopWidenedValue => TopWidenedValue + case Top => Top case ref: Ref => val outerCls = klass.owner.lexicallyEnclosingClass.asClass if !ref.hasOuter(klass) then diff --git a/compiler/test/dotty/tools/dotc/CompilationTests.scala b/compiler/test/dotty/tools/dotc/CompilationTests.scala index c780944378eb..55d26130343b 100644 --- a/compiler/test/dotty/tools/dotc/CompilationTests.scala +++ b/compiler/test/dotty/tools/dotc/CompilationTests.scala @@ -233,7 +233,7 @@ class CompilationTests { implicit val testGroup: TestGroup = TestGroup("checkInitGlobal") compileFilesInDir("tests/init-global/warn", defaultOptions.and("-Ysafe-init-global"), FileFilter.exclude(TestSources.negInitGlobalScala2LibraryTastyExcludelisted)).checkWarnings() compileFilesInDir("tests/init-global/pos", defaultOptions.and("-Ysafe-init-global", "-Xfatal-warnings"), FileFilter.exclude(TestSources.posInitGlobalScala2LibraryTastyExcludelisted)).checkCompile() - if Properties.usingScalaLibraryTasty then + if Properties.usingScalaLibraryTasty && !Properties.usingScalaLibraryCCTasty then compileFilesInDir("tests/init-global/warn-tasty", defaultOptions.and("-Ysafe-init-global"), FileFilter.exclude(TestSources.negInitGlobalScala2LibraryTastyExcludelisted)).checkWarnings() compileFilesInDir("tests/init-global/pos-tasty", defaultOptions.and("-Ysafe-init-global", "-Xfatal-warnings"), FileFilter.exclude(TestSources.posInitGlobalScala2LibraryTastyExcludelisted)).checkCompile() end if diff --git a/tests/init-global/pos/array-size-zero.scala b/tests/init-global/pos-tasty/array-size-zero.scala similarity index 100% rename from tests/init-global/pos/array-size-zero.scala rename to tests/init-global/pos-tasty/array-size-zero.scala diff --git a/tests/init-global/pos-tasty/simple.scala b/tests/init-global/pos-tasty/simple.scala deleted file mode 100644 index b3bd10e7ea42..000000000000 --- a/tests/init-global/pos-tasty/simple.scala +++ /dev/null @@ -1,3 +0,0 @@ -object O: - val a: Int = 5 - val b = a.toDouble \ No newline at end of file diff --git a/tests/init-global/pos/arithmetic.scala b/tests/init-global/pos/arithmetic.scala index c9a3d74faccb..0b4c9e3bb850 100644 --- a/tests/init-global/pos/arithmetic.scala +++ b/tests/init-global/pos/arithmetic.scala @@ -1,3 +1,5 @@ object A: val a = f(10) + val b = -a + val c = b.toDouble def f(x: Int) = x * 2 + 5 \ No newline at end of file