diff --git a/compiler/src/dotty/tools/dotc/transform/init/Objects.scala b/compiler/src/dotty/tools/dotc/transform/init/Objects.scala index c12139e508ee..b5d70eeb16b2 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Objects.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Objects.scala @@ -29,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 * @@ -54,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. * @@ -92,11 +93,12 @@ 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 field accesses won't cause warnings. 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 | 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 | Cold // 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 @@ -188,7 +190,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( @@ -225,25 +227,82 @@ 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. + * Assumption: all methods calls on such values should not trigger initialization of global objects + * or read/write mutable fields + */ + 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 + + object SafeValue: + val safeTypeSymbols = + defn.StringClass :: + (defn.ScalaNumericValueTypeList ++ + 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("[", ",", "]") - /** A cold alias which should not be used during initialization. - * - * Cold is not ValueElement since RefSet containing Cold is equivalent to Cold + 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 Cold extends Value: - def show(using Context) = "Cold" + case object UnknownValue extends Value: + def show(using Context): String = "UnknownValue" + + /** Represents values lost due to widening + * + * This is the top of the abstract domain lattice, which should not + * be used during initialization. + * + * Top is not ValueElement since RefSet containing Top + * is equivalent to Top + */ + + case object Top extends Value: + def show(using Context): String = "Top" val Bottom = ValueSet(ListSet.empty) /** Possible types for 'this' */ - type ThisValue = Ref | Cold.type + type ThisValue = Ref | Top.type /** Checking state */ object State: @@ -634,18 +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 (Cold, _) => Cold - case (_, Cold) => Cold + 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)) - - def widen(height: Int)(using Context): Value = - if height == 0 then Cold + 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 Top else a match case Bottom => Bottom @@ -654,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) @@ -663,6 +734,7 @@ class Objects(using Context @constructorOnly): ref.widenedCopy(outer2, args2, env2) case _ => a + } def filterType(tpe: Type)(using Context): Value = tpe match @@ -672,26 +744,46 @@ 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 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] + if !sym.isClass then a + else + val klass = sym.asClass + a match + 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 + case arr: OfArray => if defn.ArrayClass.isSubClass(klass) then arr else Bottom + case fun: Fun => + if klass.isOneOf(AbstractOrTrait) && klass.baseClasses.exists(defn.isFunctionClass) then fun else Bottom + + extension (value: ThisValue) + def widenThisValue(height : Int)(using Context) : ThisValue = + assert(height > 0, "Cannot call widenThisValue with height 0!") + value.widen(height).asInstanceOf[ThisValue] extension (values: Iterable[Value]) def join: Value = if values.isEmpty then Bottom else values.reduce { (v1, v2) => v1.join(v2) } def widen(height: Int): Contextual[List[Value]] = values.map(_.widen(height)).toList + /** Check if the checker option reports warnings about unknown code + */ + 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)`. * * @param value The value for the receiver. @@ -703,13 +795,63 @@ 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 Top => + report.warning("Value is unknown to the checker due to widening. " + Trace.show, Trace.position) Bottom + case UnknownValue => + reportWarningForUnknownValue("Using unknown value. " + Trace.show, Trace.position) + + case Package(packageModuleClass) => + if meth.equals(defn.throwMethod) then + Bottom + 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(_) => + if v.typeSymbol == defn.NullClass then + // call on Null is sensible on AST level but not in practice + Bottom + else + // 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(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) + call(ref, meth, args, receiver, superType, needResolve) case Bottom => Bottom + // Bottom arguments mean unreachable call + case _ if args.map(_.value).contains(Bottom) => + Bottom + case arr: OfArray => val target = resolve(defn.ArrayClass, meth) @@ -728,7 +870,7 @@ class Objects(using Context @constructorOnly): Bottom else // Array.length is OK - Bottom + SafeValue(defn.IntType) case ref: Ref => val isLocal = !meth.owner.isClass @@ -749,7 +891,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 + UnknownValue else if target.hasSource then val cls = target.owner.enclosingClass.asClass val ddef = target.defTree.asInstanceOf[DefDef] @@ -759,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(Cold -> 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) { @@ -772,7 +914,7 @@ class Objects(using Context @constructorOnly): } } else - Bottom + UnknownValue else if target.exists then select(ref, target, receiver, needResolve = false) else @@ -800,8 +942,8 @@ 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. - Cold + // For now, return `UnknownValue` to ensure soundness and trigger a warning when reportUnknown = true. + UnknownValue end if end if @@ -837,10 +979,11 @@ class Objects(using Context @constructorOnly): Returns.installHandler(ctor) eval(ddef.rhs, ref, cls, cacheResult = true) Returns.popHandler(ctor) + value } else // no source code available - Bottom + UnknownValue case _ => report.warning("[Internal error] unexpected constructor call, meth = " + ctor + ", this = " + value + Trace.show, Trace.position) @@ -856,9 +999,27 @@ 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 Top => + report.warning("Value is unknown to the checker due to widening. " + Trace.show, Trace.position) Bottom + case UnknownValue => + reportWarningForUnknownValue("Using unknown value. " + Trace.show, Trace.position) + + case v @ SafeValue(_) => + 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(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 " + packageModuleClass.show + ", field = " + field.show + Trace.show, Trace.position) + Bottom case ref: Ref => val target = if needResolve then resolve(ref.klass, field) else field @@ -868,7 +1029,7 @@ class Objects(using Context @constructorOnly): val rhs = target.defTree.asInstanceOf[ValDef].rhs eval(rhs, ref, target.owner.asClass, cacheResult = true) else - Bottom + UnknownValue else if target.exists then def isNextFieldOfColonColon: Boolean = ref.klass == defn.ConsClass && target.name.toString == "next" if target.isOneOf(Flags.Mutable) && !isNextFieldOfColonColon then @@ -899,9 +1060,9 @@ class Objects(using Context @constructorOnly): 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 + UnknownValue case fun: Fun => report.warning("[Internal error] unexpected tree in selecting a function, fun = " + fun.code.show + Trace.show, fun.code) @@ -911,9 +1072,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 => - if field.isStaticObject then accessObject(field.moduleClass.asClass) - else Bottom + case Bottom => Bottom case ValueSet(values) => values.map(ref => select(ref, field, receiver)).join @@ -928,16 +1087,19 @@ class Objects(using Context @constructorOnly): */ def assign(lhs: Value, field: Symbol, rhs: Value, rhsTyp: Type): Contextual[Value] = log("Assign" + field.show + " of " + lhs.show + ", rhs = " + rhs.show, printer, (_: Value).show) { lhs.filterClass(field.owner) match + case Top => + report.warning("Value is unknown to the checker due to widening. " + Trace.show, Trace.position) + case UnknownValue => + val _ = reportWarningForUnknownValue("Assigning to unknown value. " + Trace.show, Trace.position) + case p: Package => + 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 => 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 Bottom => + 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)) @@ -968,11 +1130,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 | SafeValue(_) => report.warning("[Internal error] unexpected outer in instantiating a class, outer = " + outer.show + ", class = " + klass.show + ", " + Trace.show, Trace.position) Bottom - case outer: (Ref | Cold.type | Bottom.type) => + case UnknownValue => + reportWarningForUnknownValue("Instantiating when outer is unknown. " + Trace.show, Trace.position) + + case outer: (Ref | Top.type | Package) => if klass == defn.ArrayClass then args.head.tree.tpe match case ConstantType(Constants.Constant(0)) => @@ -986,18 +1151,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 : ThisValue => 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) + (thisV.widenThisValue(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) @@ -1059,10 +1224,12 @@ 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 => + 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 => + case _: ValueSet | _: Ref | _: OfArray | _: Package | SafeValue(_) => report.warning("[Internal error] Unexpected by-name value " + value.show + ". " + Trace.show, Trace.position) Bottom else @@ -1073,7 +1240,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`. @@ -1250,8 +1417,8 @@ class Objects(using Context @constructorOnly): case _: This => evalType(expr.tpe, thisV, klass) - case Literal(_) => - Bottom + case Literal(const) => + SafeValue(const.tpe) case Typed(expr, tpt) => if tpt.tpe.hasAnnotation(defn.UncheckedAnnot) then @@ -1326,7 +1493,14 @@ 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) + 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) @@ -1386,11 +1560,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 +1567,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 +1587,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 +1679,20 @@ class Objects(using Context @constructorOnly): end if end if end if - scrutinee + // TODO: receiverType is the companion object type, not the class itself; + // cannot filter scrutinee by this type + (receiverType, scrutinee) 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 @@ -1526,7 +1703,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(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) @@ -1534,7 +1711,7 @@ 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(SafeValue(defn.IntType), summon[Trace], EmptyTree) :: Nil, scrutineeType, superType = NoType, needResolve = true) if isWildcardStarArgList(pats) then if pats.size == 1 then @@ -1545,7 +1722,7 @@ class Objects(using Context @constructorOnly): else // call .drop val dropDenot = getMemberMethod(scrutineeType, nme.drop, dropType(elemType)) - val dropRes = call(scrutinee, dropDenot.symbol, ArgInfo(Bottom, 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 @@ -1555,8 +1732,20 @@ class Objects(using Context @constructorOnly): end if end evalSeqPatterns + def canSkipCase(remainingScrutinee: Value, catchValue: Value) = + remainingScrutinee == Bottom || catchValue == Bottom + + 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) - cases.map(evalCase).join + caseResults.join end patternMatch /** Handle semantics of leaf nodes @@ -1573,13 +1762,13 @@ 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 + case consttpe: ConstantType => + SafeValue(consttpe.underlying) case tmref: TermRef if tmref.prefix == NoPrefix => val sym = tmref.symbol if sym.is(Flags.Package) then - Bottom + 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 @@ -1605,7 +1794,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 @@ -1805,7 +1994,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 @@ -1813,7 +2002,8 @@ class Objects(using Context @constructorOnly): else thisV match case Bottom => Bottom - case Cold => Cold + case UnknownValue => UnknownValue + case Top => Top case ref: Ref => val outerCls = klass.owner.lexicallyEnclosingClass.asClass if !ref.hasOuter(klass) then @@ -1824,7 +2014,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 | SafeValue(_) => report.warning("[Internal error] unexpected thisV = " + thisV + ", target = " + target.show + ", klass = " + klass.show + Trace.show, Trace.position) Bottom } 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 cc1ce5a0145e..55d26130343b 100644 --- a/compiler/test/dotty/tools/dotc/CompilationTests.scala +++ b/compiler/test/dotty/tools/dotc/CompilationTests.scala @@ -233,6 +233,10 @@ 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 && !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 } // initialization tests 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/arithmetic.scala b/tests/init-global/pos/arithmetic.scala new file mode 100644 index 000000000000..0b4c9e3bb850 --- /dev/null +++ b/tests/init-global/pos/arithmetic.scala @@ -0,0 +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 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() + + 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/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 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