From b787d3cb127c883e328506a97b105e342f51d9c1 Mon Sep 17 00:00:00 2001 From: Fengyun Liu Date: Sat, 15 Oct 2022 09:24:17 +0200 Subject: [PATCH 001/113] WIP - check global objects --- .../tools/dotc/transform/init/Objects.scala | 193 ++++++++++++++++++ 1 file changed, 193 insertions(+) create mode 100644 compiler/src/dotty/tools/dotc/transform/init/Objects.scala diff --git a/compiler/src/dotty/tools/dotc/transform/init/Objects.scala b/compiler/src/dotty/tools/dotc/transform/init/Objects.scala new file mode 100644 index 000000000000..24686d841426 --- /dev/null +++ b/compiler/src/dotty/tools/dotc/transform/init/Objects.scala @@ -0,0 +1,193 @@ +package dotty.tools.dotc +package transform +package init + +import core.* +import Contexts.* +import Symbols.* +import Types.* +import StdNames.* +import NameKinds.OuterSelectName +import NameKinds.SuperAccessorName + +import ast.tpd.* +import config.Printers.init as printer +import reporting.trace as log + +import Errors.* + +import scala.collection.mutable +import scala.annotation.tailrec + +/** Check initialization safety of static objects + * + * The problem is illustrated by the example below: + * + * class Foo(val opposite: Foo) + * case object A extends Foo(B) // A -> B + * case object B extends Foo(A) // B -> A + * + * In the code above, the initialization of object `A` depends on `B` and vice + * versa. There is no correct way to initialize the code above. The current + * check issue a warning for the code above. + * + * At the high-level, the analysis has the following characteristics: + * + * 1. It is inter-procedural and flow-insensitive. + * + * 2. It is modular with respect to separate compilation, even incremental + * compilation. + * + * 3. It is receiver-sensitive but not heap-sensitive -- fields are always + * abstracted by types. + * + * 4. If the target of a virtual method call cannot be determined by its + * receiver, the target is approximated by all methods of classes currently + * being compiled and are instantiated. This is to some extent similar to + * RTA (rapid type analysis). + * + * However, a class type is only added to the list when it leaks: + * + * - A value of the class is used as method argument. + * - A value of the class is alised to a field or variable. + */ +object Objects: + sealed abstract class Value + + /** + * Rerepsents values that are instances of the given class + * + * The parameter `klass` should be the concrete class of the value at runtime. + */ + case class OfClass(klass: ClassSymbol) extends Value + + /** + * Rerepsents values that are of the given type + */ + case class OfType(tp: Type) extends Value + + object State: + /** + * Records the instantiated types during instantiation of a static object. + * + * Functions and by-name closures are called when they leak, therefore they + * are not part of the instantiated types. + */ + class Data(allConcreteClasses: Set[ClassSymbol]): + // object -> (class, types of the class) + val instantiatedTypes = mutable.Map.empty[ClassSymbol, Map[ClassSymbol, List[Type]]] + + opaque type Rep = Data + + def init(classes: List[ClassSymbol])(using Context): Rep = + val concreteClasses = classes.filter(Semantic.isConcreteClass).toSet + new Data(concreteClasses) + + type Contextual[T] = (Context, State.Rep) ?=> T + + /** Check an individual class + * + * The class to be checked must be an instantiable concrete class. + */ + private def checkObject(classSym: ClassSymbol): Contextual[Unit] = + val tpl = classSym.defTree.asInstanceOf[TypeDef].rhs.asInstanceOf[Template] + init(tpl, OfClass(classSym), classSym) + + def checkClasses(classes: List[ClassSymbol])(using Context): Unit = + given State.Rep = State.init(classes) + + for + classSym <- classes + if classSym.is(Flags.Module) && classSym.isStaticOwner + do + checkObject(classSym) + + + /** Handles the evaluation of different expressions + * + * Note: Recursive call should go to `eval` instead of `cases`. + * + * @param expr The expression to be evaluated. + * @param thisV The value for `C.this` where `C` is represented by the parameter `klass`. + * @param klass The enclosing class where the expression `expr` is located. + */ + def eval(expr: Tree, thisV: Value, klass: ClassSymbol): Contextual[Value] = ??? + + def evalArgs(args: List[Arg], thisV: Value, klass: ClassSymbol): Contextual[List[Value]] = + val argInfos = new mutable.ArrayBuffer[ArgInfo] + args.foreach { arg => + val res = + if arg.isByName then + Fun(arg.tree, thisV, klass) + else + eval(arg.tree, thisV, klass) + + argInfos += res + } + argInfos.toList + + def init(tpl: Template, thisV: OfClass, klass: ClassSymbol): Contextual[Unit] = + def superCall(tref: TypeRef, ctor: Symbol, args: List[Value]): Unit = + val cls = tref.classSymbol.asClass + + // follow constructor + if cls.hasSource then thisV.callConstructor(ctor, args) + + // parents + def initParent(parent: Tree) = + parent match + case tree @ Block(stats, NewExpr(tref, New(tpt), ctor, argss)) => // can happen + eval(stats, thisV, klass) + val args = evalArgs(argss.flatten, thisV, klass) + superCall(tref, ctor, args) + + case tree @ NewExpr(tref, New(tpt), ctor, argss) => // extends A(args) + val args = evalArgs(argss.flatten, thisV, klass) + superCall(tref, ctor, args) + + case _ => // extends A or extends A[T] + val tref = typeRefOf(parent.tpe) + superCall(tref, tref.classSymbol.primaryConstructor, Nil) + + // see spec 5.1 about "Template Evaluation". + // https://www.scala-lang.org/files/archive/spec/2.13/05-classes-and-objects.html + if !klass.is(Flags.Trait) then + // 1. first init parent class recursively + // 2. initialize traits according to linearization order + val superParent = tpl.parents.head + val superCls = superParent.tpe.classSymbol.asClass + initParent(superParent) + + val parents = tpl.parents.tail + val mixins = klass.baseClasses.tail.takeWhile(_ != superCls) + + mixins.reverse.foreach { mixin => + parents.find(_.tpe.classSymbol == mixin) match + case Some(parent) => + initParent(parent) + case None => + // According to the language spec, if the mixin trait requires + // arguments, then the class must provide arguments to it explicitly + // in the parent list. That means we will encounter it in the Some + // branch. + // + // When a trait A extends a parameterized trait B, it cannot provide + // term arguments to B. That can only be done in a concrete class. + val tref = typeRefOf(klass.typeRef.baseType(mixin).typeConstructor) + val ctor = tref.classSymbol.primaryConstructor + if ctor.exists then + superCall(tref, ctor, Nil) + } + end if + + // class body + tpl.body.foreach { + case vdef : ValDef if !vdef.symbol.is(Flags.Lazy) && !vdef.rhs.isEmpty => + // Throw the field value away, as the analysis is not heap-sensitive + eval(vdef.rhs, thisV, klass) + + case _: MemberDef => + + case tree => + eval(tree, thisV, klass) + } From 337730b52fe801d9f5e3f5aac2fd69b846ab8336 Mon Sep 17 00:00:00 2001 From: Fengyun Liu Date: Sat, 15 Oct 2022 09:54:50 +0200 Subject: [PATCH 002/113] Add more docs --- .../tools/dotc/transform/init/Objects.scala | 27 +++++++++++++------ 1 file changed, 19 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 24686d841426..4847de947ae5 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Objects.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Objects.scala @@ -38,18 +38,24 @@ import scala.annotation.tailrec * 2. It is modular with respect to separate compilation, even incremental * compilation. * - * 3. It is receiver-sensitive but not heap-sensitive -- fields are always - * abstracted by types. + * When a value leaks the boundary of analysis, we approximate by calling + * all public methods of the type at the definition site. + * + * 3. It is receiver-sensitive but not heap-sensitive nor parameter-sensitive. + * + * Fields and parameters are always abstracted by their types. * * 4. If the target of a virtual method call cannot be determined by its * receiver, the target is approximated by all methods of classes currently - * being compiled and are instantiated. This is to some extent similar to - * RTA (rapid type analysis). + * being compiled and are instantiated. This is similar to RTA (rapid type + * analysis). * * However, a class type is only added to the list when it leaks: * * - A value of the class is used as method argument. * - A value of the class is alised to a field or variable. + * - A value of the class is returned from a method. + * */ object Objects: sealed abstract class Value @@ -66,17 +72,22 @@ object Objects: */ case class OfType(tp: Type) extends Value + /** + * Represents a lambda expression + */ + case class Fun(expr: Tree, thisV: Value, klass: ClassSymbol) extends Value + object State: /** - * Records the instantiated types during instantiation of a static object. - * - * Functions and by-name closures are called when they leak, therefore they - * are not part of the instantiated types. + * Remembers the instantiated types during instantiation of a static object. */ class Data(allConcreteClasses: Set[ClassSymbol]): // object -> (class, types of the class) val instantiatedTypes = mutable.Map.empty[ClassSymbol, Map[ClassSymbol, List[Type]]] + // object -> (fun type, fun values) + val instantiatedFuncs = mutable.Map.empty[ClassSymbol, Map[Type, List[Fun]]] + opaque type Rep = Data def init(classes: List[ClassSymbol])(using Context): Rep = From e11258197ee4429112b263da78e2b5cc48122ee9 Mon Sep 17 00:00:00 2001 From: Fengyun Liu Date: Sat, 15 Oct 2022 11:13:09 +0200 Subject: [PATCH 003/113] Add code skeleton --- .../tools/dotc/transform/init/Objects.scala | 250 +++++++++++++++++- 1 file changed, 242 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 4847de947ae5..df33fd6a3f3e 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Objects.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Objects.scala @@ -52,20 +52,22 @@ import scala.annotation.tailrec * * However, a class type is only added to the list when it leaks: * - * - A value of the class is used as method argument. - * - A value of the class is alised to a field or variable. - * - A value of the class is returned from a method. + * - A value of OfClass is used as method argument. + * - A value of OfClass is alised to a field. * + * When a value of OfClass is aliased to a local variable, the RHS will be + * re-evaluated when it is accessed (caching is used for optimization). */ object Objects: sealed abstract class Value + def show: String = toString /** - * Rerepsents values that are instances of the given class + * Rerepsents values that are instances of the specified class * - * The parameter `klass` should be the concrete class of the value at runtime. + * The `tp.classSymbol` should be the concrete class of the value at runtime. */ - case class OfClass(klass: ClassSymbol) extends Value + case class OfClass(tp: Type) extends Value /** * Rerepsents values that are of the given type @@ -94,7 +96,74 @@ object Objects: val concreteClasses = classes.filter(Semantic.isConcreteClass).toSet new Data(concreteClasses) - type Contextual[T] = (Context, State.Rep) ?=> T + type Contextual[T] = (Context, State.Rep, State.Cache) ?=> T + + object Cache: + /** Cache for expressions + * + * OfClass -> Tree -> Value + * + * The first key is the value of `this` for the expression. + */ + opaque type ExprValueCache = Map[OfClass, Map[TreeWrapper, Value]] + + /** A wrapper for trees for storage in maps based on referential equality of trees. */ + private abstract class TreeWrapper: + def tree: Tree + + override final def equals(other: Any): Boolean = + other match + case that: TreeWrapper => this.tree eq that.tree + case _ => false + + override final def hashCode = tree.hashCode + + /** The immutable wrapper is intended to be stored as key in the heap. */ + private class ImmutableTreeWrapper(val tree: Tree) extends TreeWrapper + + /** For queries on the heap, reuse the same wrapper to avoid unnecessary allocation. + * + * A `MutableTreeWrapper` is only ever used temporarily for querying a map, + * and is never inserted to the map. + */ + private class MutableTreeWrapper extends TreeWrapper: + var queryTree: Tree | Null = null + def tree: Tree = queryTree match + case tree: Tree => tree + case null => ??? + + /** Used to avoid allocation, its state does not matter */ + private given MutableTreeWrapper = new MutableTreeWrapper + + def get(value: OfClass, expr: Tree)(using cache: ExprValueCache): Option[Value] = + cache.get(value, expr) match + case None => cache.get(value, expr) + case res => res + + def cache(value: OfClass, expr: Tree, result: Value)(using cache: ExprValueCache): Option[Value] = + cache.updatedNested(value, expr, result) + extension (cache: ExprValueCache) + private def get(value: OfClass, expr: Tree)(using queryWrapper: MutableTreeWrapper): Option[Value] = + queryWrapper.queryTree = expr + cache.get(value).flatMap(_.get(queryWrapper)) + + private def removed(value: OfClass, expr: Tree)(using queryWrapper: MutableTreeWrapper) = + queryWrapper.queryTree = expr + val innerMap2 = cache(value).removed(queryWrapper) + cache.updated(value, innerMap2) + + private def updatedNested(value: OfClass, expr: Tree, result: Value): ExprValueCache = + val wrapper = new ImmutableTreeWrapper(expr) + updatedNestedWrapper(value, wrapper, result) + + private def updatedNestedWrapper(value: OfClass, wrapper: ImmutableTreeWrapper, result: Value): ExprValueCache = + val innerMap = cache.getOrElse(value, Map.empty[TreeWrapper, Value]) + val innerMap2 = innerMap.updated(wrapper, result) + cache.updated(value, innerMap2) + end extension + end Cache + + // --------------------------------------- /** Check an individual class * @@ -113,6 +182,9 @@ object Objects: do checkObject(classSym) + /** Evaluate a list of expressions */ + def evalExprs(exprs: List[Tree], thisV: Value, klass: ClassSymbol): Contextual[List[Value]] = + exprs.map { expr => eval(expr, thisV, klass) } /** Handles the evaluation of different expressions * @@ -122,8 +194,170 @@ object Objects: * @param thisV The value for `C.this` where `C` is represented by the parameter `klass`. * @param klass The enclosing class where the expression `expr` is located. */ - def eval(expr: Tree, thisV: Value, klass: ClassSymbol): Contextual[Value] = ??? + def cases(expr: Tree, thisV: Value, klass: ClassSymbol): Contextual[Value] = + expr match + case Ident(nme.WILDCARD) => + // TODO: disallow `var x: T = _` + OfType(defn.NothingType) + + case id @ Ident(name) if !id.symbol.is(Flags.Method) => + assert(name.isTermName, "type trees should not reach here") + cases(expr.tpe, thisV, klass) + + case NewExpr(tref, New(tpt), ctor, argss) => + // check args + val args = evalArgs(argss.flatten, thisV, klass) + + val cls = tref.classSymbol.asClass + val outer = outerValue(tref, thisV, klass) + outer.instantiate(cls, ctor, args) + + case Call(ref, argss) => + // check args + val args = evalArgs(argss.flatten, thisV, klass) + + ref match + case Select(supert: Super, _) => + val SuperType(thisTp, superTp) = supert.tpe: @unchecked + val thisValue2 = extendTrace(ref) { resolveThis(thisTp.classSymbol.asClass, thisV, klass) } + thisValue2.call(ref.symbol, args, thisTp, superTp) + + case Select(qual, _) => + val receiver = eval(qual, thisV, klass) + if ref.symbol.isConstructor then + receiver.callConstructor(ref.symbol, args) + else + receiver.call(ref.symbol, args, receiver = qual.tpe, superType = NoType) + + case id: Ident => + id.tpe match + case TermRef(NoPrefix, _) => + // resolve this for the local method + val enclosingClass = id.symbol.owner.enclosingClass.asClass + val thisValue2 = extendTrace(ref) { resolveThis(enclosingClass, thisV, klass) } + // local methods are not a member, but we can reuse the method `call` + thisValue2.call(id.symbol, args, receiver = NoType, superType = NoType, needResolve = false) + case TermRef(prefix, _) => + val receiver = cases(prefix, thisV, klass) + if id.symbol.isConstructor then + receiver.callConstructor(id.symbol, args) + else + receiver.call(id.symbol, args, receiver = prefix, superType = NoType) + + case Select(qualifier, name) => + val qual = eval(qualifier, thisV, klass) + + name match + case OuterSelectName(_, _) => + val current = qualifier.tpe.classSymbol + val target = expr.tpe.widenSingleton.classSymbol.asClass + resolveThis(target, qual, current.asClass) + case _ => + qual.select(expr.symbol, receiver = qualifier.tpe) + + case _: This => + cases(expr.tpe, thisV, klass) + + case Literal(_) => + OfType(defn.NothingType) + + case Typed(expr, tpt) => + if (tpt.tpe.hasAnnotation(defn.UncheckedAnnot)) + OfType(defn.NothingType) + else + eval(expr, thisV, klass) + + case NamedArg(name, arg) => + eval(arg, thisV, klass) + + case Assign(lhs, rhs) => + lhs match + case Select(qual, _) => + eval(qual, thisV, klass) + eval(rhs, thisV, klass) + case id: Ident => + eval(rhs, thisV, klass) + + case closureDef(ddef) => + Fun(ddef.rhs, thisV, klass) + + case PolyFun(body) => + Fun(body, thisV, klass) + + case Block(stats, expr) => + eval(stats, thisV, klass) + eval(expr, thisV, klass) + + case If(cond, thenp, elsep) => + eval(cond :: thenp :: elsep :: Nil, thisV, klass).join + + case Annotated(arg, annot) => + if (expr.tpe.hasAnnotation(defn.UncheckedAnnot)) OfType(defn.NothingType) + else eval(arg, thisV, klass) + + case Match(selector, cases) => + eval(selector, thisV, klass) + eval(cases.map(_.body), thisV, klass).join + + case Return(expr, from) => + eval(expr, thisV, klass) + + case WhileDo(cond, body) => + eval(cond :: body :: Nil, thisV, klass) + OfType(defn.NothingType) + + case Labeled(_, expr) => + eval(expr, thisV, klass) + + case Try(block, cases, finalizer) => + eval(block, thisV, klass) + if !finalizer.isEmpty then + eval(finalizer, thisV, klass) + evalExprs(cases.map(_.body), thisV, klass).join + + case SeqLiteral(elems, elemtpt) => + evalExprs(elems, thisV, klass).join + + case Inlined(call, bindings, expansion) => + evalExprs(bindings, thisV, klass) + eval(expansion, thisV, klass) + + case Thicket(List()) => + // possible in try/catch/finally, see tests/crash/i6914.scala + OfType(defn.NothingType) + + case vdef : ValDef => + // local val definition + eval(vdef.rhs, thisV, klass) + + case ddef : DefDef => + // local method + OfType(defn.NothingType) + + case tdef: TypeDef => + // local type definition + OfType(defn.NothingType) + + case _: Import | _: Export => + OfType(defn.NothingType) + + case _ => + report.error("[Internal error] unexpected tree" + Trace.show, expr) + OfType(expr.tpe) + + /** Handle semantics of leaf nodes + * + * For leaf nodes, their semantics is determined by their types. + * + * @param tp The type to be evaluated. + * @param thisV The value for `C.this` where `C` is represented by the parameter `klass`. + * @param klass The enclosing class where the type `tp` is located. + */ + def evalType(tp: Type, thisV: Ref, klass: ClassSymbol): Contextual[Value] = log("evaluating " + tp.show, printer, (_: Value).show) { + ??? + } + /** Evaluate arguments of methods */ def evalArgs(args: List[Arg], thisV: Value, klass: ClassSymbol): Contextual[List[Value]] = val argInfos = new mutable.ArrayBuffer[ArgInfo] args.foreach { arg => From 9604f7642ac7d4097918bfd7f9a374d648ee60e0 Mon Sep 17 00:00:00 2001 From: Fengyun Liu Date: Mon, 17 Oct 2022 18:18:40 +0200 Subject: [PATCH 004/113] More code type check --- .../tools/dotc/transform/init/Objects.scala | 154 ++++++++---------- 1 file changed, 72 insertions(+), 82 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/transform/init/Objects.scala b/compiler/src/dotty/tools/dotc/transform/init/Objects.scala index df33fd6a3f3e..0f12f2455d04 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Objects.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Objects.scala @@ -16,6 +16,10 @@ import reporting.trace as log import Errors.* +import Semantic.{ NewExpr, Call, ArgInfo, Trace, PolyFun, Arg, ByNameArg } + +import Semantic.{ typeRefOf, hasSource, extendTrace, withTrace } + import scala.collection.mutable import scala.annotation.tailrec @@ -39,7 +43,10 @@ import scala.annotation.tailrec * compilation. * * When a value leaks the boundary of analysis, we approximate by calling - * all public methods of the type at the definition site. + * all methods of the value that overridding a method outside of the + * boundary. + * + * Reflection will break soundness, thus is discouraged in programming. * * 3. It is receiver-sensitive but not heap-sensitive nor parameter-sensitive. * @@ -59,31 +66,51 @@ import scala.annotation.tailrec * re-evaluated when it is accessed (caching is used for optimization). */ object Objects: - sealed abstract class Value - def show: String = toString + + // ----------------------------- abstract domain ----------------------------- + + sealed abstract class Value: + def show(using Context): String /** * Rerepsents values that are instances of the specified class * - * The `tp.classSymbol` should be the concrete class of the value at runtime. + * `tp.classSymbol` should be the concrete class of the value at runtime. */ - case class OfClass(tp: Type) extends Value + case class OfClass(tp: Type, numInstantiations: Int) extends Value: + def show(using Context) = "OfClass(" + tp.show + ", " + numInstantiations + ")" /** * Rerepsents values that are of the given type + * + * `OfType` is just a short-cut referring to currently instantiated sub-types. + * + * Note: this value should never be an index in the cache. */ - case class OfType(tp: Type) extends Value + case class OfType(tp: Type) extends Value: + def show(using Context) = "OfType(" + tp.show + ")" /** * Represents a lambda expression */ - case class Fun(expr: Tree, thisV: Value, klass: ClassSymbol) extends Value + case class Fun(expr: Tree, thisV: OfClass, klass: ClassSymbol) extends Value: + def show(using Context) = "Fun(" + expr.show + ", " + thisV.show + ", " + klass.show + ")" + + /** A value which represents a set of addresses + * + * It comes from `if` expressions. + */ + case class RefSet(refs: List[Fun | OfClass]) extends Value: + def show(using Context) = refs.map(_.show).mkString("[", ",", "]") object State: /** * Remembers the instantiated types during instantiation of a static object. */ class Data(allConcreteClasses: Set[ClassSymbol]): + // objects under check + val checkingObjects = new mutable.ArrayBuffer[ClassSymbol] + // object -> (class, types of the class) val instantiatedTypes = mutable.Map.empty[ClassSymbol, Map[ClassSymbol, List[Type]]] @@ -92,78 +119,41 @@ object Objects: opaque type Rep = Data + def currentObject(using rep: Rep): ClassSymbol = rep.checkingObjects.last + + def instantiatedCount(using rep: Rep): Int = + val obj = currentObject + rep.instantiatedTypes(obj).size + rep.instantiatedFuncs(obj).size + def init(classes: List[ClassSymbol])(using Context): Rep = val concreteClasses = classes.filter(Semantic.isConcreteClass).toSet new Data(concreteClasses) - type Contextual[T] = (Context, State.Rep, State.Cache) ?=> T + type Contextual[T] = (Context, State.Rep, Cache.Cache, Trace) ?=> T object Cache: - /** Cache for expressions + /** Cache for method calls and lazy values * - * OfClass -> Tree -> Value - * - * The first key is the value of `this` for the expression. + * Method -> ThisValue -> ReturnValue */ - opaque type ExprValueCache = Map[OfClass, Map[TreeWrapper, Value]] - - /** A wrapper for trees for storage in maps based on referential equality of trees. */ - private abstract class TreeWrapper: - def tree: Tree + opaque type Cache = Map[Symbol, Map[OfClass, Value]] - override final def equals(other: Any): Boolean = - other match - case that: TreeWrapper => this.tree eq that.tree - case _ => false + def get(thisV: OfClass, sym: Symbol)(fun: => Value)(using cache: Cache): Value = + cache.get(sym).flatMap(_.get(thisV)) match + case None => + val res = fun + store(thisV, sym, res) + res + case Some(value) => + value - override final def hashCode = tree.hashCode + private def store(thisV: OfClass, sym: Symbol, result: Value)(using cache: Cache): Unit = + cache.updated(sym, cache.getOrElse(sym, Map.empty).updated(thisV, result)) - /** The immutable wrapper is intended to be stored as key in the heap. */ - private class ImmutableTreeWrapper(val tree: Tree) extends TreeWrapper - - /** For queries on the heap, reuse the same wrapper to avoid unnecessary allocation. - * - * A `MutableTreeWrapper` is only ever used temporarily for querying a map, - * and is never inserted to the map. - */ - private class MutableTreeWrapper extends TreeWrapper: - var queryTree: Tree | Null = null - def tree: Tree = queryTree match - case tree: Tree => tree - case null => ??? - - /** Used to avoid allocation, its state does not matter */ - private given MutableTreeWrapper = new MutableTreeWrapper - - def get(value: OfClass, expr: Tree)(using cache: ExprValueCache): Option[Value] = - cache.get(value, expr) match - case None => cache.get(value, expr) - case res => res - - def cache(value: OfClass, expr: Tree, result: Value)(using cache: ExprValueCache): Option[Value] = - cache.updatedNested(value, expr, result) - extension (cache: ExprValueCache) - private def get(value: OfClass, expr: Tree)(using queryWrapper: MutableTreeWrapper): Option[Value] = - queryWrapper.queryTree = expr - cache.get(value).flatMap(_.get(queryWrapper)) - - private def removed(value: OfClass, expr: Tree)(using queryWrapper: MutableTreeWrapper) = - queryWrapper.queryTree = expr - val innerMap2 = cache(value).removed(queryWrapper) - cache.updated(value, innerMap2) - - private def updatedNested(value: OfClass, expr: Tree, result: Value): ExprValueCache = - val wrapper = new ImmutableTreeWrapper(expr) - updatedNestedWrapper(value, wrapper, result) - - private def updatedNestedWrapper(value: OfClass, wrapper: ImmutableTreeWrapper, result: Value): ExprValueCache = - val innerMap = cache.getOrElse(value, Map.empty[TreeWrapper, Value]) - val innerMap2 = innerMap.updated(wrapper, result) - cache.updated(value, innerMap2) - end extension + def fresh(): Cache = Map.empty end Cache - // --------------------------------------- + // -------------------------------- algorithm -------------------------------- /** Check an individual class * @@ -171,10 +161,12 @@ object Objects: */ private def checkObject(classSym: ClassSymbol): Contextual[Unit] = val tpl = classSym.defTree.asInstanceOf[TypeDef].rhs.asInstanceOf[Template] - init(tpl, OfClass(classSym), classSym) + init(tpl, OfClass(classSym.typeRef, State.instantiatedCount), classSym) def checkClasses(classes: List[ClassSymbol])(using Context): Unit = given State.Rep = State.init(classes) + given Cache.Cache = Cache.fresh() + given Trace = Trace.empty for classSym <- classes @@ -183,18 +175,16 @@ object Objects: checkObject(classSym) /** Evaluate a list of expressions */ - def evalExprs(exprs: List[Tree], thisV: Value, klass: ClassSymbol): Contextual[List[Value]] = + def evalExprs(exprs: List[Tree], thisV: OfClass, klass: ClassSymbol): Contextual[List[Value]] = exprs.map { expr => eval(expr, thisV, klass) } /** Handles the evaluation of different expressions - * - * Note: Recursive call should go to `eval` instead of `cases`. * * @param expr The expression to be evaluated. * @param thisV The value for `C.this` where `C` is represented by the parameter `klass`. * @param klass The enclosing class where the expression `expr` is located. */ - def cases(expr: Tree, thisV: Value, klass: ClassSymbol): Contextual[Value] = + def eval(expr: Tree, thisV: OfClass, klass: ClassSymbol): Contextual[Value] = expr match case Ident(nme.WILDCARD) => // TODO: disallow `var x: T = _` @@ -202,7 +192,7 @@ object Objects: case id @ Ident(name) if !id.symbol.is(Flags.Method) => assert(name.isTermName, "type trees should not reach here") - cases(expr.tpe, thisV, klass) + evalType(expr.tpe, thisV, klass) case NewExpr(tref, New(tpt), ctor, argss) => // check args @@ -238,7 +228,7 @@ object Objects: // local methods are not a member, but we can reuse the method `call` thisValue2.call(id.symbol, args, receiver = NoType, superType = NoType, needResolve = false) case TermRef(prefix, _) => - val receiver = cases(prefix, thisV, klass) + val receiver = evalType(prefix, thisV, klass) if id.symbol.isConstructor then receiver.callConstructor(id.symbol, args) else @@ -256,7 +246,7 @@ object Objects: qual.select(expr.symbol, receiver = qualifier.tpe) case _: This => - cases(expr.tpe, thisV, klass) + evalType(expr.tpe, thisV, klass) case Literal(_) => OfType(defn.NothingType) @@ -285,11 +275,11 @@ object Objects: Fun(body, thisV, klass) case Block(stats, expr) => - eval(stats, thisV, klass) + evalExprs(stats, thisV, klass) eval(expr, thisV, klass) case If(cond, thenp, elsep) => - eval(cond :: thenp :: elsep :: Nil, thisV, klass).join + evalExprs(cond :: thenp :: elsep :: Nil, thisV, klass).join case Annotated(arg, annot) => if (expr.tpe.hasAnnotation(defn.UncheckedAnnot)) OfType(defn.NothingType) @@ -297,13 +287,13 @@ object Objects: case Match(selector, cases) => eval(selector, thisV, klass) - eval(cases.map(_.body), thisV, klass).join + evalExprs(cases.map(_.body), thisV, klass).join case Return(expr, from) => eval(expr, thisV, klass) case WhileDo(cond, body) => - eval(cond :: body :: Nil, thisV, klass) + evalExprs(cond :: body :: Nil, thisV, klass) OfType(defn.NothingType) case Labeled(_, expr) => @@ -353,13 +343,13 @@ object Objects: * @param thisV The value for `C.this` where `C` is represented by the parameter `klass`. * @param klass The enclosing class where the type `tp` is located. */ - def evalType(tp: Type, thisV: Ref, klass: ClassSymbol): Contextual[Value] = log("evaluating " + tp.show, printer, (_: Value).show) { + def evalType(tp: Type, thisV: OfClass, klass: ClassSymbol): Contextual[Value] = log("evaluating " + tp.show, printer, (_: Value).show) { ??? } /** Evaluate arguments of methods */ - def evalArgs(args: List[Arg], thisV: Value, klass: ClassSymbol): Contextual[List[Value]] = - val argInfos = new mutable.ArrayBuffer[ArgInfo] + def evalArgs(args: List[Arg], thisV: OfClass, klass: ClassSymbol): Contextual[List[Value]] = + val argInfos = new mutable.ArrayBuffer[Value] args.foreach { arg => val res = if arg.isByName then @@ -382,7 +372,7 @@ object Objects: def initParent(parent: Tree) = parent match case tree @ Block(stats, NewExpr(tref, New(tpt), ctor, argss)) => // can happen - eval(stats, thisV, klass) + evalExprs(stats, thisV, klass) val args = evalArgs(argss.flatten, thisV, klass) superCall(tref, ctor, args) From a3b9f1280e70bc375eda21da896aa5923c8a6674 Mon Sep 17 00:00:00 2001 From: Fengyun Liu Date: Tue, 18 Oct 2022 21:20:36 +0200 Subject: [PATCH 005/113] Add skeleton --- .../tools/dotc/transform/init/Objects.scala | 87 +++++++++++++++---- 1 file changed, 71 insertions(+), 16 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/transform/init/Objects.scala b/compiler/src/dotty/tools/dotc/transform/init/Objects.scala index 0f12f2455d04..cb461eb377c5 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Objects.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Objects.scala @@ -16,9 +16,9 @@ import reporting.trace as log import Errors.* -import Semantic.{ NewExpr, Call, ArgInfo, Trace, PolyFun, Arg, ByNameArg } +import Semantic.{ NewExpr, Call, TraceValue, Trace, PolyFun, Arg, ByNameArg } -import Semantic.{ typeRefOf, hasSource, extendTrace, withTrace } +import Semantic.{ typeRefOf, hasSource, extendTrace, withTrace, trace } import scala.collection.mutable import scala.annotation.tailrec @@ -134,7 +134,7 @@ object Objects: object Cache: /** Cache for method calls and lazy values * - * Method -> ThisValue -> ReturnValue + * Symbol -> ThisValue -> ReturnValue */ opaque type Cache = Map[Symbol, Map[OfClass, Value]] @@ -153,6 +153,24 @@ object Objects: def fresh(): Cache = Map.empty end Cache + // --------------------------- domain operations ----------------------------- + + type ArgInfo = TraceValue[Value] + + extension (a: Value) + def join(b: Value): Value = ??? + + extension (values: Seq[Value]) + def join: Value = ??? + + def call(thisV: Value, meth: Symbol, args: List[ArgInfo], receiver: Type, superType: Type, needResolve: Boolean = true): Contextual[Value] = ??? + + def callConstructor(thisV: Value, ctor: Symbol, args: List[ArgInfo]): Contextual[Value] = ??? + + def select(thisV: Value, field: Symbol, receiver: Type, needResolve: Boolean = true): Contextual[Value] = ??? + + def instantiate(outer: Value, klass: ClassSymbol, ctor: Symbol, args: List[ArgInfo]): Contextual[Value] = ??? + // -------------------------------- algorithm -------------------------------- /** Check an individual class @@ -200,7 +218,7 @@ object Objects: val cls = tref.classSymbol.asClass val outer = outerValue(tref, thisV, klass) - outer.instantiate(cls, ctor, args) + instantiate(outer, cls, ctor, args) case Call(ref, argss) => // check args @@ -210,14 +228,14 @@ object Objects: case Select(supert: Super, _) => val SuperType(thisTp, superTp) = supert.tpe: @unchecked val thisValue2 = extendTrace(ref) { resolveThis(thisTp.classSymbol.asClass, thisV, klass) } - thisValue2.call(ref.symbol, args, thisTp, superTp) + call(thisValue2, ref.symbol, args, thisTp, superTp) case Select(qual, _) => val receiver = eval(qual, thisV, klass) if ref.symbol.isConstructor then - receiver.callConstructor(ref.symbol, args) + callConstructor(receiver, ref.symbol, args) else - receiver.call(ref.symbol, args, receiver = qual.tpe, superType = NoType) + call(receiver, ref.symbol, args, receiver = qual.tpe, superType = NoType) case id: Ident => id.tpe match @@ -226,13 +244,13 @@ object Objects: val enclosingClass = id.symbol.owner.enclosingClass.asClass val thisValue2 = extendTrace(ref) { resolveThis(enclosingClass, thisV, klass) } // local methods are not a member, but we can reuse the method `call` - thisValue2.call(id.symbol, args, receiver = NoType, superType = NoType, needResolve = false) + call(thisValue2, id.symbol, args, receiver = NoType, superType = NoType, needResolve = false) case TermRef(prefix, _) => val receiver = evalType(prefix, thisV, klass) if id.symbol.isConstructor then - receiver.callConstructor(id.symbol, args) + callConstructor(receiver, id.symbol, args) else - receiver.call(id.symbol, args, receiver = prefix, superType = NoType) + call(receiver, id.symbol, args, receiver = prefix, superType = NoType) case Select(qualifier, name) => val qual = eval(qualifier, thisV, klass) @@ -243,7 +261,7 @@ object Objects: val target = expr.tpe.widenSingleton.classSymbol.asClass resolveThis(target, qual, current.asClass) case _ => - qual.select(expr.symbol, receiver = qualifier.tpe) + select(qual, expr.symbol, receiver = qualifier.tpe) case _: This => evalType(expr.tpe, thisV, klass) @@ -348,8 +366,8 @@ object Objects: } /** Evaluate arguments of methods */ - def evalArgs(args: List[Arg], thisV: OfClass, klass: ClassSymbol): Contextual[List[Value]] = - val argInfos = new mutable.ArrayBuffer[Value] + def evalArgs(args: List[Arg], thisV: OfClass, klass: ClassSymbol): Contextual[List[ArgInfo]] = + val argInfos = new mutable.ArrayBuffer[ArgInfo] args.foreach { arg => val res = if arg.isByName then @@ -357,16 +375,16 @@ object Objects: else eval(arg.tree, thisV, klass) - argInfos += res + argInfos += TraceValue(res, trace.add(arg.tree)) } argInfos.toList def init(tpl: Template, thisV: OfClass, klass: ClassSymbol): Contextual[Unit] = - def superCall(tref: TypeRef, ctor: Symbol, args: List[Value]): Unit = + def superCall(tref: TypeRef, ctor: Symbol, args: List[ArgInfo]): Unit = val cls = tref.classSymbol.asClass // follow constructor - if cls.hasSource then thisV.callConstructor(ctor, args) + if cls.hasSource then callConstructor(thisV, ctor, args) // parents def initParent(parent: Tree) = @@ -426,3 +444,40 @@ object Objects: case tree => eval(tree, thisV, klass) } + + /** Resolve C.this that appear in `klass` + * + * @param target The class symbol for `C` for which `C.this` is to be resolved. + * @param thisV The value for `D.this` where `D` is represented by the parameter `klass`. + * @param klass The enclosing class where the type `C.this` is located. + */ + def resolveThis(target: ClassSymbol, thisV: Value, klass: ClassSymbol): Contextual[Value] = + thisV match + case OfClass(tp, _) => + ??? + + case OfType(tp) => + ??? + + case RefSet(refs) => + ??? + + case _ => + report.error("[Internal error] unexpected this value " + thisV + Trace.show, Trace.position) + OfType(defn.NothingType) + + /** Compute the outer value that correspond to `tref.prefix` + * + * @param tref The type whose prefix is to be evaluated. + * @param thisV The value for `C.this` where `C` is represented by the parameter `klass`. + * @param klass The enclosing class where the type `tref` is located. + */ + def outerValue(tref: TypeRef, thisV: OfClass, klass: ClassSymbol): Contextual[Value] = + val cls = tref.classSymbol.asClass + if tref.prefix == NoPrefix then + val enclosing = cls.owner.lexicallyEnclosingClass.asClass + val outerV = resolveThis(enclosing, thisV, klass) + outerV + else + if cls.isAllOf(Flags.JavaInterface) then OfType(defn.NothingType) + else evalType(tref.prefix, thisV, klass) From ce25221a1956c66941f78365dc6074be00fac6ea Mon Sep 17 00:00:00 2001 From: Fengyun Liu Date: Thu, 5 Jan 2023 00:36:19 +0100 Subject: [PATCH 006/113] Refine abstract domain --- .../tools/dotc/transform/init/Objects.scala | 48 ++++++++----------- 1 file changed, 21 insertions(+), 27 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/transform/init/Objects.scala b/compiler/src/dotty/tools/dotc/transform/init/Objects.scala index cb461eb377c5..0a43532a47b0 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Objects.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Objects.scala @@ -33,7 +33,7 @@ import scala.annotation.tailrec * * In the code above, the initialization of object `A` depends on `B` and vice * versa. There is no correct way to initialize the code above. The current - * check issue a warning for the code above. + * checker issues a warning for the code above. * * At the high-level, the analysis has the following characteristics: * @@ -42,28 +42,14 @@ import scala.annotation.tailrec * 2. It is modular with respect to separate compilation, even incremental * compilation. * - * When a value leaks the boundary of analysis, we approximate by calling - * all methods of the value that overridding a method outside of the - * boundary. - * - * Reflection will break soundness, thus is discouraged in programming. + * When a value leaks beyond the boundary of analysis, we approximate by + * calling all methods of the value that overridding a method outside of + * the boundary. * * 3. It is receiver-sensitive but not heap-sensitive nor parameter-sensitive. * * Fields and parameters are always abstracted by their types. * - * 4. If the target of a virtual method call cannot be determined by its - * receiver, the target is approximated by all methods of classes currently - * being compiled and are instantiated. This is similar to RTA (rapid type - * analysis). - * - * However, a class type is only added to the list when it leaks: - * - * - A value of OfClass is used as method argument. - * - A value of OfClass is alised to a field. - * - * When a value of OfClass is aliased to a local variable, the RHS will be - * re-evaluated when it is accessed (caching is used for optimization). */ object Objects: @@ -72,13 +58,24 @@ object Objects: sealed abstract class Value: def show(using Context): String + /** + * Represents an external value + * + * An external value is usually an instance of a class of another project, + * therefore it may not refer to objects defined in the current project. The + * only possibility is that an internal value leaks to an external value. Such + * leaking is strictly checked by the algorithm. + */ + case object Ext extends Value: + def show(using Context) = "Ext" + /** * Rerepsents values that are instances of the specified class * * `tp.classSymbol` should be the concrete class of the value at runtime. */ - case class OfClass(tp: Type, numInstantiations: Int) extends Value: - def show(using Context) = "OfClass(" + tp.show + ", " + numInstantiations + ")" + case class OfClass(tp: Type) extends Value: + def show(using Context) = "OfClass(" + tp.show + ")" /** * Rerepsents values that are of the given type @@ -96,7 +93,8 @@ object Objects: case class Fun(expr: Tree, thisV: OfClass, klass: ClassSymbol) extends Value: def show(using Context) = "Fun(" + expr.show + ", " + thisV.show + ", " + klass.show + ")" - /** A value which represents a set of addresses + /** + * Represents a set of values * * It comes from `if` expressions. */ @@ -121,10 +119,6 @@ object Objects: def currentObject(using rep: Rep): ClassSymbol = rep.checkingObjects.last - def instantiatedCount(using rep: Rep): Int = - val obj = currentObject - rep.instantiatedTypes(obj).size + rep.instantiatedFuncs(obj).size - def init(classes: List[ClassSymbol])(using Context): Rep = val concreteClasses = classes.filter(Semantic.isConcreteClass).toSet new Data(concreteClasses) @@ -179,7 +173,7 @@ object Objects: */ private def checkObject(classSym: ClassSymbol): Contextual[Unit] = val tpl = classSym.defTree.asInstanceOf[TypeDef].rhs.asInstanceOf[Template] - init(tpl, OfClass(classSym.typeRef, State.instantiatedCount), classSym) + init(tpl, OfClass(classSym.typeRef), classSym) def checkClasses(classes: List[ClassSymbol])(using Context): Unit = given State.Rep = State.init(classes) @@ -453,7 +447,7 @@ object Objects: */ def resolveThis(target: ClassSymbol, thisV: Value, klass: ClassSymbol): Contextual[Value] = thisV match - case OfClass(tp, _) => + case OfClass(tp) => ??? case OfType(tp) => From f8960f34730ac328c4aa4811a2aead1f8c4ef59f Mon Sep 17 00:00:00 2001 From: Fengyun Liu Date: Thu, 5 Jan 2023 01:13:42 +0100 Subject: [PATCH 007/113] Check cycles --- .../tools/dotc/transform/init/Objects.scala | 35 +++++++++++++------ 1 file changed, 24 insertions(+), 11 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/transform/init/Objects.scala b/compiler/src/dotty/tools/dotc/transform/init/Objects.scala index 0a43532a47b0..02483b048a4b 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Objects.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Objects.scala @@ -98,7 +98,8 @@ object Objects: * * It comes from `if` expressions. */ - case class RefSet(refs: List[Fun | OfClass]) extends Value: + case class RefSet(refs: List[Value]) extends Value: + assert(refs.forall(!_.isInstanceOf[RefSet])) def show(using Context) = refs.map(_.show).mkString("[", ",", "]") object State: @@ -108,16 +109,21 @@ object Objects: class Data(allConcreteClasses: Set[ClassSymbol]): // objects under check val checkingObjects = new mutable.ArrayBuffer[ClassSymbol] - - // object -> (class, types of the class) - val instantiatedTypes = mutable.Map.empty[ClassSymbol, Map[ClassSymbol, List[Type]]] - - // object -> (fun type, fun values) - val instantiatedFuncs = mutable.Map.empty[ClassSymbol, Map[Type, List[Fun]]] + val checkedObjects = new mutable.ArrayBuffer[ClassSymbol] opaque type Rep = Data - def currentObject(using rep: Rep): ClassSymbol = rep.checkingObjects.last + def checkObject(clazz: ClassSymbol)(work: => Unit)(using rep: Rep, ctx: Context) = + val index = rep.checkingObjects.indexOf(clazz) + + if index != -1 then + val cycle = rep.checkingObjects.slice(index, rep.checkingObjects.size - 1) + report.warning("Cyclic initialization: " + cycle.map(_.show).mkString(" -> ") + clazz.show, clazz.defTree) + else if rep.checkedObjects.indexOf(clazz) == -1 then + rep.checkingObjects += clazz + work + assert(rep.checkingObjects.last == clazz, "Expect = " + clazz.show + ", found = " + rep.checkingObjects.last) + rep.checkedObjects += rep.checkingObjects.remove(rep.checkingObjects.size - 1) def init(classes: List[ClassSymbol])(using Context): Rep = val concreteClasses = classes.filter(Semantic.isConcreteClass).toSet @@ -152,10 +158,15 @@ object Objects: type ArgInfo = TraceValue[Value] extension (a: Value) - def join(b: Value): Value = ??? + def join(b: Value): Value = + (a, b) match + case (RefSet(refs1), RefSet(refs2)) => RefSet(refs1 ++ refs2) + case (a, RefSet(refs)) => RefSet(a :: refs) + case (RefSet(refs), b) => RefSet(b :: refs) + case (a, b) => RefSet(a :: b :: Nil) extension (values: Seq[Value]) - def join: Value = ??? + def join: Value = values.reduce { (v1, v2) => v1.join(v2) } def call(thisV: Value, meth: Symbol, args: List[ArgInfo], receiver: Type, superType: Type, needResolve: Boolean = true): Contextual[Value] = ??? @@ -173,7 +184,9 @@ object Objects: */ private def checkObject(classSym: ClassSymbol): Contextual[Unit] = val tpl = classSym.defTree.asInstanceOf[TypeDef].rhs.asInstanceOf[Template] - init(tpl, OfClass(classSym.typeRef), classSym) + State.checkObject(classSym) { + init(tpl, OfClass(classSym.typeRef), classSym) + } def checkClasses(classes: List[ClassSymbol])(using Context): Unit = given State.Rep = State.init(classes) From 0f4afa8b49723872b288b980dfbbb325a62f69a0 Mon Sep 17 00:00:00 2001 From: Fengyun Liu Date: Thu, 5 Jan 2023 01:57:22 +0100 Subject: [PATCH 008/113] Fix cache --- .../tools/dotc/transform/init/Objects.scala | 127 +++++++++++++++--- 1 file changed, 105 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 02483b048a4b..ac4f4de04eba 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Objects.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Objects.scala @@ -129,28 +129,83 @@ object Objects: val concreteClasses = classes.filter(Semantic.isConcreteClass).toSet new Data(concreteClasses) - type Contextual[T] = (Context, State.Rep, Cache.Cache, Trace) ?=> T + type Contextual[T] = (Context, State.Rep, Cache.Data, Trace) ?=> T object Cache: /** Cache for method calls and lazy values * * Symbol -> ThisValue -> ReturnValue */ - opaque type Cache = Map[Symbol, Map[OfClass, Value]] - - def get(thisV: OfClass, sym: Symbol)(fun: => Value)(using cache: Cache): Value = - cache.get(sym).flatMap(_.get(thisV)) match - case None => - val res = fun - store(thisV, sym, res) - res - case Some(value) => - value - - private def store(thisV: OfClass, sym: Symbol, result: Value)(using cache: Cache): Unit = - cache.updated(sym, cache.getOrElse(sym, Map.empty).updated(thisV, result)) - - def fresh(): Cache = Map.empty + private type Cache = Map[Symbol, Map[Value, Value]] + + class Data: + /** The cache from last iteration */ + private var last: Cache = Map.empty + + /** The output cache + * + * The output cache is computed based on the cache values `last` from the + * last iteration. + * + * Both `last` and `current` are required to make sure evaluation happens + * once in each iteration. + */ + private var current: Cache = Map.empty + + /** Whether the current heap is different from the last heap? + * + * `changed == false` implies that the fixed point has been reached. + */ + private var changed: Boolean = false + + def assume(value: Value, sym: Symbol)(fun: => Value): Contextual[Value] = + val assumeValue: Value = + last.get(value, sym) match + case Some(value) => value + case None => + val default = OfType(defn.NothingType) + this.last = last.updatedNested(value, sym, default) + default + + this.current = current.updatedNested(value, sym, assumeValue) + + val actual = fun + if actual != assumeValue then + this.changed = true + this.current = this.current.updatedNested(value, sym, actual) + end if + + actual + end assume + + def hasChanged = changed + + /** Prepare cache for the next iteration + * + * 1. Reset changed flag. + * + * 2. Use current cache as last cache and set current cache to be empty. + * + */ + def prepareForNextIteration()(using Context) = + this.changed = false + this.last = this.current + this.current = Map.empty + end Data + + extension (cache: Cache) + private def get(value: Value, sym: Symbol): Option[Value] = + cache.get(sym).flatMap(_.get(value)) + + private def removed(value: Value, sym: Symbol) = + val innerMap2 = cache(sym).removed(value) + cache.updated(sym, innerMap2) + + private def updatedNested(value: Value, sym: Symbol, result: Value): Cache = + val innerMap = cache.getOrElse(sym, Map.empty[Value, Value]) + val innerMap2 = innerMap.updated(value, result) + cache.updated(sym, innerMap2) + end extension end Cache // --------------------------- domain operations ----------------------------- @@ -168,7 +223,23 @@ object Objects: extension (values: Seq[Value]) def join: Value = values.reduce { (v1, v2) => v1.join(v2) } - def call(thisV: Value, meth: Symbol, args: List[ArgInfo], receiver: Type, superType: Type, needResolve: Boolean = true): Contextual[Value] = ??? + def call(thisV: Value, meth: Symbol, args: List[ArgInfo], receiver: Type, superType: Type, needResolve: Boolean = true): Contextual[Value] = + thisV match + case Ext => + // TODO: promotion of values + Ext + + case OfClass(tp) => + ??? + + case OfType(tp) => + ??? + + case Fun(expr, thisV, klass) => + ??? + + case RefSet(vs) => + ??? def callConstructor(thisV: Value, ctor: Symbol, args: List[ArgInfo]): Contextual[Value] = ??? @@ -182,16 +253,27 @@ object Objects: * * The class to be checked must be an instantiable concrete class. */ - private def checkObject(classSym: ClassSymbol): Contextual[Unit] = + private def checkObject(classSym: ClassSymbol)(using Context, State.Rep): Unit = val tpl = classSym.defTree.asInstanceOf[TypeDef].rhs.asInstanceOf[Template] - State.checkObject(classSym) { + + @tailrec + def iterate(): Unit = + given cache: Cache.Data = new Cache.Data + given Trace = Trace.empty + init(tpl, OfClass(classSym.typeRef), classSym) - } + + val hasError = false // TODO + if cache.hasChanged && !hasError then + // code to prepare cache and heap for next iteration + cache.prepareForNextIteration() + iterate() + end iterate + + State.checkObject(classSym) { iterate() } def checkClasses(classes: List[ClassSymbol])(using Context): Unit = given State.Rep = State.init(classes) - given Cache.Cache = Cache.fresh() - given Trace = Trace.empty for classSym <- classes @@ -199,6 +281,7 @@ object Objects: do checkObject(classSym) + /** Evaluate a list of expressions */ def evalExprs(exprs: List[Tree], thisV: OfClass, klass: ClassSymbol): Contextual[List[Value]] = exprs.map { expr => eval(expr, thisV, klass) } From db0430c383de0e7db0bc1432945526be4ae21eaa Mon Sep 17 00:00:00 2001 From: Fengyun Liu Date: Thu, 5 Jan 2023 10:27:27 +0100 Subject: [PATCH 009/113] Stop fix point computation in case of errors --- .../dotty/tools/dotc/transform/init/Objects.scala | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/transform/init/Objects.scala b/compiler/src/dotty/tools/dotc/transform/init/Objects.scala index ac4f4de04eba..fd18babbeb62 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Objects.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Objects.scala @@ -12,6 +12,7 @@ import NameKinds.SuperAccessorName import ast.tpd.* import config.Printers.init as printer +import reporting.StoreReporter import reporting.trace as log import Errors.* @@ -257,20 +258,24 @@ object Objects: val tpl = classSym.defTree.asInstanceOf[TypeDef].rhs.asInstanceOf[Template] @tailrec - def iterate(): Unit = + def iterate()(using Context): Unit = given cache: Cache.Data = new Cache.Data given Trace = Trace.empty init(tpl, OfClass(classSym.typeRef), classSym) - val hasError = false // TODO + val hasError = ctx.reporter.pendingMessages.nonEmpty if cache.hasChanged && !hasError then - // code to prepare cache and heap for next iteration cache.prepareForNextIteration() iterate() + else + ctx.reporter.flush() end iterate - State.checkObject(classSym) { iterate() } + State.checkObject(classSym) { + val reporter = new StoreReporter(ctx.reporter) + iterate()(using ctx.fresh.setReporter(reporter)) + } def checkClasses(classes: List[ClassSymbol])(using Context): Unit = given State.Rep = State.init(classes) From f6d255a28bfb56c2305fc13ab9815060167ec07f Mon Sep 17 00:00:00 2001 From: Fengyun Liu Date: Thu, 5 Jan 2023 11:12:59 +0100 Subject: [PATCH 010/113] Introduce Bottom value --- .../tools/dotc/transform/init/Objects.scala | 28 +++++++++++-------- 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/transform/init/Objects.scala b/compiler/src/dotty/tools/dotc/transform/init/Objects.scala index fd18babbeb62..5ca0eabec4a7 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Objects.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Objects.scala @@ -103,6 +103,8 @@ object Objects: assert(refs.forall(!_.isInstanceOf[RefSet])) def show(using Context) = refs.map(_.show).mkString("[", ",", "]") + val Bottom = RefSet(Nil) + object State: /** * Remembers the instantiated types during instantiation of a static object. @@ -164,7 +166,7 @@ object Objects: last.get(value, sym) match case Some(value) => value case None => - val default = OfType(defn.NothingType) + val default = Bottom this.last = last.updatedNested(value, sym, default) default @@ -216,6 +218,8 @@ object Objects: extension (a: Value) def join(b: Value): Value = (a, b) match + case (Bottom, b) => b + case (a, Bottom) => a case (RefSet(refs1), RefSet(refs2)) => RefSet(refs1 ++ refs2) case (a, RefSet(refs)) => RefSet(a :: refs) case (RefSet(refs), b) => RefSet(b :: refs) @@ -301,7 +305,7 @@ object Objects: expr match case Ident(nme.WILDCARD) => // TODO: disallow `var x: T = _` - OfType(defn.NothingType) + Bottom case id @ Ident(name) if !id.symbol.is(Flags.Method) => assert(name.isTermName, "type trees should not reach here") @@ -362,11 +366,11 @@ object Objects: evalType(expr.tpe, thisV, klass) case Literal(_) => - OfType(defn.NothingType) + Bottom case Typed(expr, tpt) => if (tpt.tpe.hasAnnotation(defn.UncheckedAnnot)) - OfType(defn.NothingType) + Bottom else eval(expr, thisV, klass) @@ -395,7 +399,7 @@ object Objects: evalExprs(cond :: thenp :: elsep :: Nil, thisV, klass).join case Annotated(arg, annot) => - if (expr.tpe.hasAnnotation(defn.UncheckedAnnot)) OfType(defn.NothingType) + if (expr.tpe.hasAnnotation(defn.UncheckedAnnot)) Bottom else eval(arg, thisV, klass) case Match(selector, cases) => @@ -407,7 +411,7 @@ object Objects: case WhileDo(cond, body) => evalExprs(cond :: body :: Nil, thisV, klass) - OfType(defn.NothingType) + Bottom case Labeled(_, expr) => eval(expr, thisV, klass) @@ -427,7 +431,7 @@ object Objects: case Thicket(List()) => // possible in try/catch/finally, see tests/crash/i6914.scala - OfType(defn.NothingType) + Bottom case vdef : ValDef => // local val definition @@ -435,14 +439,14 @@ object Objects: case ddef : DefDef => // local method - OfType(defn.NothingType) + Bottom case tdef: TypeDef => // local type definition - OfType(defn.NothingType) + Bottom case _: Import | _: Export => - OfType(defn.NothingType) + Bottom case _ => report.error("[Internal error] unexpected tree" + Trace.show, expr) @@ -559,7 +563,7 @@ object Objects: case _ => report.error("[Internal error] unexpected this value " + thisV + Trace.show, Trace.position) - OfType(defn.NothingType) + Bottom /** Compute the outer value that correspond to `tref.prefix` * @@ -574,5 +578,5 @@ object Objects: val outerV = resolveThis(enclosing, thisV, klass) outerV else - if cls.isAllOf(Flags.JavaInterface) then OfType(defn.NothingType) + if cls.isAllOf(Flags.JavaInterface) then Bottom else evalType(tref.prefix, thisV, klass) From 03afb7d02f989b0016fb899ba0e202e46c308191 Mon Sep 17 00:00:00 2001 From: Fengyun Liu Date: Thu, 5 Jan 2023 11:42:37 +0100 Subject: [PATCH 011/113] Define evalType --- .../tools/dotc/transform/init/Objects.scala | 72 +++++++++++++------ 1 file changed, 52 insertions(+), 20 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/transform/init/Objects.scala b/compiler/src/dotty/tools/dotc/transform/init/Objects.scala index 5ca0eabec4a7..39b8ae6b89e0 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Objects.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Objects.scala @@ -254,10 +254,7 @@ object Objects: // -------------------------------- algorithm -------------------------------- - /** Check an individual class - * - * The class to be checked must be an instantiable concrete class. - */ + /** Check an individual object */ private def checkObject(classSym: ClassSymbol)(using Context, State.Rep): Unit = val tpl = classSym.defTree.asInstanceOf[TypeDef].rhs.asInstanceOf[Template] @@ -285,8 +282,7 @@ object Objects: given State.Rep = State.init(classes) for - classSym <- classes - if classSym.is(Flags.Module) && classSym.isStaticOwner + classSym <- classes if classSym.isStaticObject do checkObject(classSym) @@ -461,7 +457,51 @@ object Objects: * @param klass The enclosing class where the type `tp` is located. */ def evalType(tp: Type, thisV: OfClass, klass: ClassSymbol): Contextual[Value] = log("evaluating " + tp.show, printer, (_: Value).show) { - ??? + // TODO: identify aliasing of object & object field + tp match + case _: ConstantType => + Bottom + + case tmref: TermRef if tmref.prefix == NoPrefix => + // - params and var definitions are abstract by its type + // - evaluate the rhs of the local definition for val definitions + val sym = tmref.symbol + if sym.isOneOf(Flags.Param | Flags.Mutable) then + OfType(sym.info) + else if sym.is(Flags.Package) then + Bottom + else if sym.hasSource then + val rhs = sym.defTree.asInstanceOf[ValDef].rhs + eval(rhs, thisV, klass) + else + // pattern-bound variables + OfType(sym.info) + + case tmref: TermRef => + val sym = tmref.symbol + if sym.isStaticObject then + // TODO: check immutability + checkObject(sym.moduleClass.asClass) + OfClass(sym.moduleClass.typeRef) + else + // TODO: check object field access + val value = evalType(tmref.prefix, thisV, klass) + select(value, tmref.symbol, tmref.prefix) + + case tp @ ThisType(tref) => + val sym = tref.symbol + if sym.is(Flags.Package) then + Bottom + else if sym.isStaticObject && sym != klass then + // TODO: check immutability + checkObject(sym.moduleClass.asClass) + OfClass(sym.moduleClass.typeRef) + + else + resolveThis(tref.classSymbol.asClass, thisV, klass) + + case _ => + throw new Exception("unexpected type: " + tp) } /** Evaluate arguments of methods */ @@ -551,19 +591,7 @@ object Objects: * @param klass The enclosing class where the type `C.this` is located. */ def resolveThis(target: ClassSymbol, thisV: Value, klass: ClassSymbol): Contextual[Value] = - thisV match - case OfClass(tp) => - ??? - - case OfType(tp) => - ??? - - case RefSet(refs) => - ??? - - case _ => - report.error("[Internal error] unexpected this value " + thisV + Trace.show, Trace.position) - Bottom + OfType(target.typeRef) /** Compute the outer value that correspond to `tref.prefix` * @@ -580,3 +608,7 @@ object Objects: else if cls.isAllOf(Flags.JavaInterface) then Bottom else evalType(tref.prefix, thisV, klass) + + extension (sym: Symbol) + def isStaticObject(using Context) = + sym.is(Flags.Module, butNot = Flags.Package) && sym.isStatic From fea9fb9eaac2e24c79ea238e1f400867e89bcaa3 Mon Sep 17 00:00:00 2001 From: Fengyun Liu Date: Sat, 7 Jan 2023 00:33:17 +0100 Subject: [PATCH 012/113] Remove Ext It is impossible to take advantage of Ext if static objects can be mutable. --- .../tools/dotc/transform/init/Objects.scala | 94 +++++++++---------- 1 file changed, 44 insertions(+), 50 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/transform/init/Objects.scala b/compiler/src/dotty/tools/dotc/transform/init/Objects.scala index 39b8ae6b89e0..d1bdf6a6716d 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Objects.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Objects.scala @@ -40,14 +40,7 @@ import scala.annotation.tailrec * * 1. It is inter-procedural and flow-insensitive. * - * 2. It is modular with respect to separate compilation, even incremental - * compilation. - * - * When a value leaks beyond the boundary of analysis, we approximate by - * calling all methods of the value that overridding a method outside of - * the boundary. - * - * 3. It is receiver-sensitive but not heap-sensitive nor parameter-sensitive. + * 2. It is receiver-sensitive but not heap-sensitive nor parameter-sensitive. * * Fields and parameters are always abstracted by their types. * @@ -59,27 +52,28 @@ object Objects: sealed abstract class Value: def show(using Context): String + /** - * Represents an external value - * - * An external value is usually an instance of a class of another project, - * therefore it may not refer to objects defined in the current project. The - * only possibility is that an internal value leaks to an external value. Such - * leaking is strictly checked by the algorithm. + * A reference caches the current value. */ - case object Ext extends Value: - def show(using Context) = "Ext" + sealed abstract class Ref extends Value: + val fields: mutable.Map[Symbol, Value] = mutable.Map.empty + val outers: mutable.Map[Symbol, Value] = mutable.Map.empty + + /** A reference to a static object */ + case class ObjectRef(klass: ClassSymbol) extends Ref: + def show(using Context) = "ObjectRef(" + klass.show + ")" /** * Rerepsents values that are instances of the specified class * * `tp.classSymbol` should be the concrete class of the value at runtime. */ - case class OfClass(tp: Type) extends Value: + case class OfClass(tp: Type, outer: Value) extends Ref: def show(using Context) = "OfClass(" + tp.show + ")" /** - * Rerepsents values that are of the given type + * Rerepsents values of a specific type * * `OfType` is just a short-cut referring to currently instantiated sub-types. * @@ -91,7 +85,7 @@ object Objects: /** * Represents a lambda expression */ - case class Fun(expr: Tree, thisV: OfClass, klass: ClassSymbol) extends Value: + case class Fun(expr: Tree, thisV: Ref, klass: ClassSymbol) extends Value: def show(using Context) = "Fun(" + expr.show + ", " + thisV.show + ", " + klass.show + ")" /** @@ -109,30 +103,30 @@ object Objects: /** * Remembers the instantiated types during instantiation of a static object. */ - class Data(allConcreteClasses: Set[ClassSymbol]): + class Data: // objects under check - val checkingObjects = new mutable.ArrayBuffer[ClassSymbol] - val checkedObjects = new mutable.ArrayBuffer[ClassSymbol] + private[State] val checkingObjects = new mutable.ArrayBuffer[ClassSymbol] + private[State] val checkedObjects = new mutable.ArrayBuffer[ClassSymbol] + + // object -> (class, types of the class) + private[State] val instantiatedTypes = mutable.Map.empty[ClassSymbol, Map[ClassSymbol, List[Type]]] - opaque type Rep = Data + // object -> (fun type, fun values) + private[State] val instantiatedFuncs = mutable.Map.empty[ClassSymbol, Map[Type, List[Fun]]] - def checkObject(clazz: ClassSymbol)(work: => Unit)(using rep: Rep, ctx: Context) = - val index = rep.checkingObjects.indexOf(clazz) + def checkObject(clazz: ClassSymbol)(work: => Unit)(using data: Data, ctx: Context) = + val index = data.checkingObjects.indexOf(clazz) if index != -1 then - val cycle = rep.checkingObjects.slice(index, rep.checkingObjects.size - 1) + val cycle = data.checkingObjects.slice(index, data.checkingObjects.size - 1) report.warning("Cyclic initialization: " + cycle.map(_.show).mkString(" -> ") + clazz.show, clazz.defTree) - else if rep.checkedObjects.indexOf(clazz) == -1 then - rep.checkingObjects += clazz + else if data.checkedObjects.indexOf(clazz) == -1 then + data.checkingObjects += clazz work - assert(rep.checkingObjects.last == clazz, "Expect = " + clazz.show + ", found = " + rep.checkingObjects.last) - rep.checkedObjects += rep.checkingObjects.remove(rep.checkingObjects.size - 1) - - def init(classes: List[ClassSymbol])(using Context): Rep = - val concreteClasses = classes.filter(Semantic.isConcreteClass).toSet - new Data(concreteClasses) + assert(data.checkingObjects.last == clazz, "Expect = " + clazz.show + ", found = " + data.checkingObjects.last) + data.checkedObjects += data.checkingObjects.remove(data.checkingObjects.size - 1) - type Contextual[T] = (Context, State.Rep, Cache.Data, Trace) ?=> T + type Contextual[T] = (Context, State.Data, Cache.Data, Trace) ?=> T object Cache: /** Cache for method calls and lazy values @@ -230,11 +224,11 @@ object Objects: def call(thisV: Value, meth: Symbol, args: List[ArgInfo], receiver: Type, superType: Type, needResolve: Boolean = true): Contextual[Value] = thisV match - case Ext => - // TODO: promotion of values - Ext - case OfClass(tp) => + case ObjectRef(klass) => + ??? + + case OfClass(tp, outer) => ??? case OfType(tp) => @@ -255,7 +249,7 @@ object Objects: // -------------------------------- algorithm -------------------------------- /** Check an individual object */ - private def checkObject(classSym: ClassSymbol)(using Context, State.Rep): Unit = + private def checkObject(classSym: ClassSymbol)(using Context, State.Data): Unit = val tpl = classSym.defTree.asInstanceOf[TypeDef].rhs.asInstanceOf[Template] @tailrec @@ -263,7 +257,7 @@ object Objects: given cache: Cache.Data = new Cache.Data given Trace = Trace.empty - init(tpl, OfClass(classSym.typeRef), classSym) + init(tpl, ObjectRef(classSym), classSym) val hasError = ctx.reporter.pendingMessages.nonEmpty if cache.hasChanged && !hasError then @@ -279,7 +273,7 @@ object Objects: } def checkClasses(classes: List[ClassSymbol])(using Context): Unit = - given State.Rep = State.init(classes) + given State.Data = new State.Data for classSym <- classes if classSym.isStaticObject @@ -288,7 +282,7 @@ object Objects: /** Evaluate a list of expressions */ - def evalExprs(exprs: List[Tree], thisV: OfClass, klass: ClassSymbol): Contextual[List[Value]] = + def evalExprs(exprs: List[Tree], thisV: Ref, klass: ClassSymbol): Contextual[List[Value]] = exprs.map { expr => eval(expr, thisV, klass) } /** Handles the evaluation of different expressions @@ -297,7 +291,7 @@ object Objects: * @param thisV The value for `C.this` where `C` is represented by the parameter `klass`. * @param klass The enclosing class where the expression `expr` is located. */ - def eval(expr: Tree, thisV: OfClass, klass: ClassSymbol): Contextual[Value] = + def eval(expr: Tree, thisV: Ref, klass: ClassSymbol): Contextual[Value] = expr match case Ident(nme.WILDCARD) => // TODO: disallow `var x: T = _` @@ -456,7 +450,7 @@ object Objects: * @param thisV The value for `C.this` where `C` is represented by the parameter `klass`. * @param klass The enclosing class where the type `tp` is located. */ - def evalType(tp: Type, thisV: OfClass, klass: ClassSymbol): Contextual[Value] = log("evaluating " + tp.show, printer, (_: Value).show) { + def evalType(tp: Type, thisV: Ref, klass: ClassSymbol): Contextual[Value] = log("evaluating " + tp.show, printer, (_: Value).show) { // TODO: identify aliasing of object & object field tp match case _: ConstantType => @@ -482,7 +476,7 @@ object Objects: if sym.isStaticObject then // TODO: check immutability checkObject(sym.moduleClass.asClass) - OfClass(sym.moduleClass.typeRef) + ObjectRef(sym.moduleClass.asClass) else // TODO: check object field access val value = evalType(tmref.prefix, thisV, klass) @@ -495,7 +489,7 @@ object Objects: else if sym.isStaticObject && sym != klass then // TODO: check immutability checkObject(sym.moduleClass.asClass) - OfClass(sym.moduleClass.typeRef) + ObjectRef(sym.moduleClass.asClass) else resolveThis(tref.classSymbol.asClass, thisV, klass) @@ -505,7 +499,7 @@ object Objects: } /** Evaluate arguments of methods */ - def evalArgs(args: List[Arg], thisV: OfClass, klass: ClassSymbol): Contextual[List[ArgInfo]] = + def evalArgs(args: List[Arg], thisV: Ref, klass: ClassSymbol): Contextual[List[ArgInfo]] = val argInfos = new mutable.ArrayBuffer[ArgInfo] args.foreach { arg => val res = @@ -518,7 +512,7 @@ object Objects: } argInfos.toList - def init(tpl: Template, thisV: OfClass, klass: ClassSymbol): Contextual[Unit] = + def init(tpl: Template, thisV: Ref, klass: ClassSymbol): Contextual[Unit] = def superCall(tref: TypeRef, ctor: Symbol, args: List[ArgInfo]): Unit = val cls = tref.classSymbol.asClass @@ -599,7 +593,7 @@ object Objects: * @param thisV The value for `C.this` where `C` is represented by the parameter `klass`. * @param klass The enclosing class where the type `tref` is located. */ - def outerValue(tref: TypeRef, thisV: OfClass, klass: ClassSymbol): Contextual[Value] = + def outerValue(tref: TypeRef, thisV: Ref, klass: ClassSymbol): Contextual[Value] = val cls = tref.classSymbol.asClass if tref.prefix == NoPrefix then val enclosing = cls.owner.lexicallyEnclosingClass.asClass From 46c7b8b1424e5306d89792b2af7868eb5b0c4458 Mon Sep 17 00:00:00 2001 From: Fengyun Liu Date: Sun, 8 Jan 2023 15:03:30 +0100 Subject: [PATCH 013/113] Fix init and resolveThis --- .../tools/dotc/transform/init/Objects.scala | 153 ++++++++++++++---- 1 file changed, 125 insertions(+), 28 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/transform/init/Objects.scala b/compiler/src/dotty/tools/dotc/transform/init/Objects.scala index d1bdf6a6716d..50a209772fa7 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Objects.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Objects.scala @@ -57,8 +57,22 @@ object Objects: * A reference caches the current value. */ sealed abstract class Ref extends Value: - val fields: mutable.Map[Symbol, Value] = mutable.Map.empty - val outers: mutable.Map[Symbol, Value] = mutable.Map.empty + private val fields: mutable.Map[Symbol, Value] = mutable.Map.empty + private val outers: mutable.Map[ClassSymbol, Value] = mutable.Map.empty + + def fieldValue(sym: Symbol): Value = fields(sym) + + def outerValue(cls: ClassSymbol): Value = outers(cls) + + def hasOuter(cls: ClassSymbol): Boolean = outers.contains(cls) + + def updateField(field: Symbol, value: Value) = + assert(!fields.contains(field), "Field already set " + field) + fields(field) = value + + def updateOuter(cls: ClassSymbol, value: Value) = + assert(!outers.contains(cls), "Outer already set " + cls) + outers(cls) = value /** A reference to a static object */ case class ObjectRef(klass: ClassSymbol) extends Ref: @@ -114,7 +128,7 @@ object Objects: // object -> (fun type, fun values) private[State] val instantiatedFuncs = mutable.Map.empty[ClassSymbol, Map[Type, List[Fun]]] - def checkObject(clazz: ClassSymbol)(work: => Unit)(using data: Data, ctx: Context) = + def checkCycle(clazz: ClassSymbol)(work: => Unit)(using data: Data, ctx: Context) = val index = data.checkingObjects.indexOf(clazz) if index != -1 then @@ -219,6 +233,11 @@ object Objects: case (RefSet(refs), b) => RefSet(b :: refs) case (a, b) => RefSet(a :: b :: Nil) + def widen(using Context): Value = a.match + case RefSet(refs) => refs.map(_.widen).join + case OfClass(tp, _: OfClass) => OfType(tp) + case _ => a + extension (values: Seq[Value]) def join: Value = values.reduce { (v1, v2) => v1.join(v2) } @@ -249,7 +268,7 @@ object Objects: // -------------------------------- algorithm -------------------------------- /** Check an individual object */ - private def checkObject(classSym: ClassSymbol)(using Context, State.Data): Unit = + private def accessObject(classSym: ClassSymbol)(using Context, State.Data): Value = val tpl = classSym.defTree.asInstanceOf[TypeDef].rhs.asInstanceOf[Template] @tailrec @@ -267,18 +286,20 @@ object Objects: ctx.reporter.flush() end iterate - State.checkObject(classSym) { + State.checkCycle(classSym) { val reporter = new StoreReporter(ctx.reporter) iterate()(using ctx.fresh.setReporter(reporter)) } + ObjectRef(classSym) + def checkClasses(classes: List[ClassSymbol])(using Context): Unit = given State.Data = new State.Data for classSym <- classes if classSym.isStaticObject do - checkObject(classSym) + accessObject(classSym) /** Evaluate a list of expressions */ @@ -447,10 +468,14 @@ object Objects: * For leaf nodes, their semantics is determined by their types. * * @param tp The type to be evaluated. - * @param thisV The value for `C.this` where `C` is represented by the parameter `klass`. + * @param thisV The value for `C.this` where `C` is represented by `klass`. * @param klass The enclosing class where the type `tp` is located. + * @param elideObjectAccess Whether object access should be omitted. + * + * Object access elission happens when the object access is used as a prefix + * in `new o.C` and `C` does not need an outer. */ - def evalType(tp: Type, thisV: Ref, klass: ClassSymbol): Contextual[Value] = log("evaluating " + tp.show, printer, (_: Value).show) { + def evalType(tp: Type, thisV: Ref, klass: ClassSymbol, elideObjectAccess: Boolean = false): Contextual[Value] = log("evaluating " + tp.show, printer, (_: Value).show) { // TODO: identify aliasing of object & object field tp match case _: ConstantType => @@ -475,8 +500,10 @@ object Objects: val sym = tmref.symbol if sym.isStaticObject then // TODO: check immutability - checkObject(sym.moduleClass.asClass) - ObjectRef(sym.moduleClass.asClass) + if elideObjectAccess then + ObjectRef(sym.moduleClass.asClass) + else + accessObject(sym.moduleClass.asClass) else // TODO: check object field access val value = evalType(tmref.prefix, thisV, klass) @@ -488,8 +515,10 @@ object Objects: Bottom else if sym.isStaticObject && sym != klass then // TODO: check immutability - checkObject(sym.moduleClass.asClass) - ObjectRef(sym.moduleClass.asClass) + if elideObjectAccess then + ObjectRef(sym.moduleClass.asClass) + else + accessObject(sym.moduleClass.asClass) else resolveThis(tref.classSymbol.asClass, thisV, klass) @@ -513,44 +542,74 @@ object Objects: argInfos.toList def init(tpl: Template, thisV: Ref, klass: ClassSymbol): Contextual[Unit] = - def superCall(tref: TypeRef, ctor: Symbol, args: List[ArgInfo]): Unit = + val paramsMap = tpl.constr.termParamss.flatten.map { vdef => + vdef.name -> thisV.fieldValue(vdef.symbol) + }.toMap + + // init param fields + klass.paramGetters.foreach { acc => + val value = paramsMap(acc.name.toTermName) + thisV.updateField(acc, value) + printer.println(acc.show + " initialized with " + value) + } + + // Tasks is used to schedule super constructor calls. + // Super constructor calls are delayed until all outers are set. + type Tasks = mutable.ArrayBuffer[() => Unit] + def superCall(tref: TypeRef, ctor: Symbol, args: List[ArgInfo], tasks: Tasks): Unit = val cls = tref.classSymbol.asClass + // update outer for super class + val res = outerValue(tref, thisV, klass) + thisV.updateOuter(cls, res) // follow constructor - if cls.hasSource then callConstructor(thisV, ctor, args) + if cls.hasSource then + tasks.append { () => + printer.println("init super class " + cls.show) + callConstructor(thisV, ctor, args) + () + } // parents - def initParent(parent: Tree) = + def initParent(parent: Tree, tasks: Tasks) = parent match case tree @ Block(stats, NewExpr(tref, New(tpt), ctor, argss)) => // can happen evalExprs(stats, thisV, klass) val args = evalArgs(argss.flatten, thisV, klass) - superCall(tref, ctor, args) + superCall(tref, ctor, args, tasks) case tree @ NewExpr(tref, New(tpt), ctor, argss) => // extends A(args) val args = evalArgs(argss.flatten, thisV, klass) - superCall(tref, ctor, args) + superCall(tref, ctor, args, tasks) case _ => // extends A or extends A[T] val tref = typeRefOf(parent.tpe) - superCall(tref, tref.classSymbol.primaryConstructor, Nil) + superCall(tref, tref.classSymbol.primaryConstructor, Nil, tasks) // see spec 5.1 about "Template Evaluation". // https://www.scala-lang.org/files/archive/spec/2.13/05-classes-and-objects.html if !klass.is(Flags.Trait) then + // outers are set first + val tasks = new mutable.ArrayBuffer[() => Unit] + // 1. first init parent class recursively // 2. initialize traits according to linearization order val superParent = tpl.parents.head val superCls = superParent.tpe.classSymbol.asClass - initParent(superParent) + extendTrace(superParent) { initParent(superParent, tasks) } val parents = tpl.parents.tail val mixins = klass.baseClasses.tail.takeWhile(_ != superCls) + // The interesting case is the outers for traits. The compiler + // synthesizes proxy accessors for the outers in the class that extends + // the trait. As those outers must be stable values, they are initialized + // immediately following class parameters and before super constructor + // calls and user code in the class body. mixins.reverse.foreach { mixin => parents.find(_.tpe.classSymbol == mixin) match case Some(parent) => - initParent(parent) + extendTrace(parent) { initParent(parent, tasks) } case None => // According to the language spec, if the mixin trait requires // arguments, then the class must provide arguments to it explicitly @@ -562,15 +621,24 @@ object Objects: val tref = typeRefOf(klass.typeRef.baseType(mixin).typeConstructor) val ctor = tref.classSymbol.primaryConstructor if ctor.exists then - superCall(tref, ctor, Nil) + // The parameter check of traits comes late in the mixin phase. + // To avoid crash we supply hot values for erroneous parent calls. + // See tests/neg/i16438.scala. + val args: List[ArgInfo] = ctor.info.paramInfoss.flatten.map(_ => new ArgInfo(Bottom, Trace.empty)) + extendTrace(superParent) { + superCall(tref, ctor, args, tasks) + } } + + // initialize super classes after outers are set + tasks.foreach(task => task()) end if // class body tpl.body.foreach { case vdef : ValDef if !vdef.symbol.is(Flags.Lazy) && !vdef.rhs.isEmpty => - // Throw the field value away, as the analysis is not heap-sensitive - eval(vdef.rhs, thisV, klass) + val res = eval(vdef.rhs, thisV, klass) + thisV.updateField(vdef.symbol, res) case _: MemberDef => @@ -578,14 +646,44 @@ object Objects: eval(tree, thisV, klass) } + /** Resolve C.this that appear in `klass` * * @param target The class symbol for `C` for which `C.this` is to be resolved. * @param thisV The value for `D.this` where `D` is represented by the parameter `klass`. * @param klass The enclosing class where the type `C.this` is located. + * @param elideObjectAccess Whether object access should be omitted. + * + * Object access elission happens when the object access is used as a prefix + * in `new o.C` and `C` does not need an outer. */ - def resolveThis(target: ClassSymbol, thisV: Value, klass: ClassSymbol): Contextual[Value] = - OfType(target.typeRef) + def resolveThis(target: ClassSymbol, thisV: Value, klass: ClassSymbol, elideObjectAccess: Boolean = false): Contextual[Value] = + if target == klass then + thisV + else if target.is(Flags.Package) then + Bottom + else if target.isStaticObject then + val res = ObjectRef(target.moduleClass.asClass) + if target == klass || elideObjectAccess then res + else accessObject(target) + else + thisV match + case Bottom => Bottom + case ref: Ref => + val outerCls = klass.owner.lexicallyEnclosingClass.asClass + if !ref.hasOuter(klass) then + val error = "[Internal error] outer not yet initialized, target = " + target + ", klass = " + klass + Trace.show + report.error(error, Trace.position) + Bottom + else + resolveThis(target, ref.outerValue(klass), outerCls) + case RefSet(refs) => + refs.map(ref => resolveThis(target, ref, klass)).join + case fun: Fun => + report.error("[Internal error] unexpected thisV = " + thisV + ", target = " + target.show + ", klass = " + klass.show + Trace.show, Trace.position) + Bottom + case OfType(tp) => + OfType(target.appliedRef) /** Compute the outer value that correspond to `tref.prefix` * @@ -597,11 +695,10 @@ object Objects: val cls = tref.classSymbol.asClass if tref.prefix == NoPrefix then val enclosing = cls.owner.lexicallyEnclosingClass.asClass - val outerV = resolveThis(enclosing, thisV, klass) - outerV + resolveThis(enclosing, thisV, klass, elideObjectAccess = cls.isStatic) else if cls.isAllOf(Flags.JavaInterface) then Bottom - else evalType(tref.prefix, thisV, klass) + else evalType(tref.prefix, thisV, klass, elideObjectAccess = cls.isStatic) extension (sym: Symbol) def isStaticObject(using Context) = From e997bc0c61902c4247566bdc46a46777be1da713 Mon Sep 17 00:00:00 2001 From: Fengyun Liu Date: Sun, 8 Jan 2023 15:41:57 +0100 Subject: [PATCH 014/113] Fix call --- .../tools/dotc/transform/init/Objects.scala | 110 ++++++++++++++---- 1 file changed, 86 insertions(+), 24 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/transform/init/Objects.scala b/compiler/src/dotty/tools/dotc/transform/init/Objects.scala index 50a209772fa7..e460ed4f9308 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Objects.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Objects.scala @@ -19,7 +19,7 @@ import Errors.* import Semantic.{ NewExpr, Call, TraceValue, Trace, PolyFun, Arg, ByNameArg } -import Semantic.{ typeRefOf, hasSource, extendTrace, withTrace, trace } +import Semantic.{ typeRefOf, hasSource, extendTrace, withTrace, trace, resolve } import scala.collection.mutable import scala.annotation.tailrec @@ -60,10 +60,14 @@ object Objects: private val fields: mutable.Map[Symbol, Value] = mutable.Map.empty private val outers: mutable.Map[ClassSymbol, Value] = mutable.Map.empty + def klass: ClassSymbol + def fieldValue(sym: Symbol): Value = fields(sym) def outerValue(cls: ClassSymbol): Value = outers(cls) + def hasField(sym: Symbol): Boolean = fields.contains(sym) + def hasOuter(cls: ClassSymbol): Boolean = outers.contains(cls) def updateField(field: Symbol, value: Value) = @@ -83,7 +87,7 @@ object Objects: * * `tp.classSymbol` should be the concrete class of the value at runtime. */ - case class OfClass(tp: Type, outer: Value) extends Ref: + case class OfClass(tp: Type, klass: ClassSymbol, outer: Value) extends Ref: def show(using Context) = "OfClass(" + tp.show + ")" /** @@ -99,7 +103,7 @@ object Objects: /** * Represents a lambda expression */ - case class Fun(expr: Tree, thisV: Ref, klass: ClassSymbol) extends Value: + case class Fun(expr: Tree, thisV: Value, klass: ClassSymbol) extends Value: def show(using Context) = "Fun(" + expr.show + ", " + thisV.show + ", " + klass.show + ")" /** @@ -235,29 +239,91 @@ object Objects: def widen(using Context): Value = a.match case RefSet(refs) => refs.map(_.widen).join - case OfClass(tp, _: OfClass) => OfType(tp) + case OfClass(tp, _, _: OfClass) => OfType(tp) case _ => a extension (values: Seq[Value]) def join: Value = values.reduce { (v1, v2) => v1.join(v2) } - def call(thisV: Value, meth: Symbol, args: List[ArgInfo], receiver: Type, superType: Type, needResolve: Boolean = true): Contextual[Value] = - thisV match - - case ObjectRef(klass) => - ??? - - case OfClass(tp, outer) => - ??? + def call(value: Value, meth: Symbol, args: List[ArgInfo], receiver: Type, superType: Type, needResolve: Boolean = true): Contextual[Value] = + def checkArgs() = + // TODO: check aliasing of static objects + for + arg <- args + do + arg.value match + case ObjectRef(obj) => + report.warning("Aliasing object " + obj.show, Trace.position) + + case _ => + + value match + + case ref: Ref => + checkArgs() + + val isLocal = !meth.owner.isClass + val target = + if !needResolve then + meth + else if superType.exists then + meth + else if meth.name.is(SuperAccessorName) then + ResolveSuper.rebindSuper(ref.klass, meth) + else + resolve(ref.klass, meth) + + if target.isOneOf(Flags.Method) then + if target.hasSource then + val cls = target.owner.enclosingClass.asClass + val ddef = target.defTree.asInstanceOf[DefDef] + extendTrace(ddef) { + eval(ddef.rhs, ref, cls) + } + else + Bottom + else if target.exists then + if ref.hasField(target) then + ref.fieldValue(target) + else + select(ref, target, receiver, needResolve = false) + else + if ref.klass.isSubClass(receiver.widenSingleton.classSymbol) then + report.error("[Internal error] Unexpected resolution failure: ref.klass = " + ref.klass.show + ", meth = " + meth.show + Trace.show, Trace.position) + Bottom + else + // This is possible due to incorrect type cast. + // See tests/init/pos/Type.scala + Bottom case OfType(tp) => - ??? + checkArgs() + + if meth.exists && meth.isEffectivelyFinal then + if meth.hasSource then + val isLocal = meth.owner.isClass + val ddef = meth.defTree.asInstanceOf[DefDef] + extendTrace(ddef) { + eval(ddef.rhs, value, meth.owner.enclosingClass.asClass) + } + else + Bottom + else + // TODO: approximate call with instantiated types + report.warning("Virtual method call ", Trace.position) + Bottom case Fun(expr, thisV, klass) => - ??? + checkArgs() + + // meth == NoSymbol for poly functions + if meth.name.toString == "tupled" then + value // a call like `fun.tupled` + else + eval(expr, thisV, klass) case RefSet(vs) => - ??? + vs.map(v => call(v, meth, args, receiver, superType)).join def callConstructor(thisV: Value, ctor: Symbol, args: List[ArgInfo]): Contextual[Value] = ??? @@ -303,7 +369,7 @@ object Objects: /** Evaluate a list of expressions */ - def evalExprs(exprs: List[Tree], thisV: Ref, klass: ClassSymbol): Contextual[List[Value]] = + def evalExprs(exprs: List[Tree], thisV: Value, klass: ClassSymbol): Contextual[List[Value]] = exprs.map { expr => eval(expr, thisV, klass) } /** Handles the evaluation of different expressions @@ -312,7 +378,7 @@ object Objects: * @param thisV The value for `C.this` where `C` is represented by the parameter `klass`. * @param klass The enclosing class where the expression `expr` is located. */ - def eval(expr: Tree, thisV: Ref, klass: ClassSymbol): Contextual[Value] = + def eval(expr: Tree, thisV: Value, klass: ClassSymbol): Contextual[Value] = expr match case Ident(nme.WILDCARD) => // TODO: disallow `var x: T = _` @@ -475,8 +541,7 @@ object Objects: * Object access elission happens when the object access is used as a prefix * in `new o.C` and `C` does not need an outer. */ - def evalType(tp: Type, thisV: Ref, klass: ClassSymbol, elideObjectAccess: Boolean = false): Contextual[Value] = log("evaluating " + tp.show, printer, (_: Value).show) { - // TODO: identify aliasing of object & object field + def evalType(tp: Type, thisV: Value, klass: ClassSymbol, elideObjectAccess: Boolean = false): Contextual[Value] = log("evaluating " + tp.show, printer, (_: Value).show) { tp match case _: ConstantType => Bottom @@ -499,13 +564,11 @@ object Objects: case tmref: TermRef => val sym = tmref.symbol if sym.isStaticObject then - // TODO: check immutability if elideObjectAccess then ObjectRef(sym.moduleClass.asClass) else accessObject(sym.moduleClass.asClass) else - // TODO: check object field access val value = evalType(tmref.prefix, thisV, klass) select(value, tmref.symbol, tmref.prefix) @@ -514,7 +577,6 @@ object Objects: if sym.is(Flags.Package) then Bottom else if sym.isStaticObject && sym != klass then - // TODO: check immutability if elideObjectAccess then ObjectRef(sym.moduleClass.asClass) else @@ -528,7 +590,7 @@ object Objects: } /** Evaluate arguments of methods */ - def evalArgs(args: List[Arg], thisV: Ref, klass: ClassSymbol): Contextual[List[ArgInfo]] = + def evalArgs(args: List[Arg], thisV: Value, klass: ClassSymbol): Contextual[List[ArgInfo]] = val argInfos = new mutable.ArrayBuffer[ArgInfo] args.foreach { arg => val res = @@ -691,7 +753,7 @@ object Objects: * @param thisV The value for `C.this` where `C` is represented by the parameter `klass`. * @param klass The enclosing class where the type `tref` is located. */ - def outerValue(tref: TypeRef, thisV: Ref, klass: ClassSymbol): Contextual[Value] = + def outerValue(tref: TypeRef, thisV: Value, klass: ClassSymbol): Contextual[Value] = val cls = tref.classSymbol.asClass if tref.prefix == NoPrefix then val enclosing = cls.owner.lexicallyEnclosingClass.asClass From fab38b5abd64d000f136ff63f63a301cc56ed1e4 Mon Sep 17 00:00:00 2001 From: Fengyun Liu Date: Sun, 8 Jan 2023 16:09:36 +0100 Subject: [PATCH 015/113] Fix callConstructor --- .../tools/dotc/transform/init/Objects.scala | 44 ++++++++++++++++--- 1 file changed, 38 insertions(+), 6 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/transform/init/Objects.scala b/compiler/src/dotty/tools/dotc/transform/init/Objects.scala index e460ed4f9308..950b1d832ff1 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Objects.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Objects.scala @@ -87,8 +87,8 @@ object Objects: * * `tp.classSymbol` should be the concrete class of the value at runtime. */ - case class OfClass(tp: Type, klass: ClassSymbol, outer: Value) extends Ref: - def show(using Context) = "OfClass(" + tp.show + ")" + case class OfClass(tp: Type, klass: ClassSymbol, outer: Value, ctor: Symbol, args: List[Value]) extends Ref: + def show(using Context) = "OfClass(" + klass.show + ", outer = " + outer + ", args = " + args.map(_.show) + ")" /** * Rerepsents values of a specific type @@ -237,14 +237,17 @@ object Objects: case (RefSet(refs), b) => RefSet(b :: refs) case (a, b) => RefSet(a :: b :: Nil) - def widen(using Context): Value = a.match - case RefSet(refs) => refs.map(_.widen).join - case OfClass(tp, _, _: OfClass) => OfType(tp) + def widenArg(using Context): Value = + a match + case RefSet(refs) => refs.map(_.widenArg).join + case OfClass(tp, _, _: OfClass, _, _) => OfType(tp) case _ => a extension (values: Seq[Value]) def join: Value = values.reduce { (v1, v2) => v1.join(v2) } + def widenArgs: Contextual[List[Value]] = values.map(_.widenArg).toList + def call(value: Value, meth: Symbol, args: List[ArgInfo], receiver: Type, superType: Type, needResolve: Boolean = true): Contextual[Value] = def checkArgs() = // TODO: check aliasing of static objects @@ -325,7 +328,36 @@ object Objects: case RefSet(vs) => vs.map(v => call(v, meth, args, receiver, superType)).join - def callConstructor(thisV: Value, ctor: Symbol, args: List[ArgInfo]): Contextual[Value] = ??? + def callConstructor(thisV: Value, ctor: Symbol, args: List[ArgInfo]): Contextual[Value] = + // init "fake" param fields for parameters of primary and secondary constructors + def addParamsAsFields(args: List[Value], ref: Ref, ctorDef: DefDef) = + val params = ctorDef.termParamss.flatten.map(_.symbol) + assert(args.size == params.size, "arguments = " + args.size + ", params = " + params.size + ", ctor = " + ctor.show) + for (param, value) <- params.zip(args) do + ref.updateField(param, value) + printer.println(param.show + " initialized with " + value) + + thisV match + case ref: Ref => + if ctor.hasSource then + val cls = ctor.owner.enclosingClass.asClass + val ddef = ctor.defTree.asInstanceOf[DefDef] + val args2 = args.map(_.value).widenArgs + addParamsAsFields(args2, ref, ddef) + if ctor.isPrimaryConstructor then + val tpl = cls.defTree.asInstanceOf[TypeDef].rhs.asInstanceOf[Template] + extendTrace(cls.defTree) { eval(tpl, ref, cls) } + ref + else + extendTrace(ddef) { eval(ddef.rhs, ref, cls) } + else + // no source code available + Bottom + + case _ => + report.error("[Internal error] unexpected constructor call, meth = " + ctor + ", this = " + thisV + Trace.show, Trace.position) + Bottom + def select(thisV: Value, field: Symbol, receiver: Type, needResolve: Boolean = true): Contextual[Value] = ??? From e6dbb2df4b03f8d2f0580a4c7d424072aac92521 Mon Sep 17 00:00:00 2001 From: Fengyun Liu Date: Sun, 8 Jan 2023 16:18:40 +0100 Subject: [PATCH 016/113] Fix select --- .../tools/dotc/transform/init/Objects.scala | 40 ++++++++++++++++++- 1 file changed, 39 insertions(+), 1 deletion(-) diff --git a/compiler/src/dotty/tools/dotc/transform/init/Objects.scala b/compiler/src/dotty/tools/dotc/transform/init/Objects.scala index 950b1d832ff1..94c6bcd94b6f 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Objects.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Objects.scala @@ -359,7 +359,45 @@ object Objects: Bottom - def select(thisV: Value, field: Symbol, receiver: Type, needResolve: Boolean = true): Contextual[Value] = ??? + def select(thisV: Value, field: Symbol, receiver: Type, needResolve: Boolean = true): Contextual[Value] = + thisV match + case ref: Ref => + val target = if needResolve then resolve(ref.klass, field) else field + if target.is(Flags.Lazy) then + val rhs = target.defTree.asInstanceOf[ValDef].rhs + eval(rhs, ref, target.owner.asClass) + else if target.exists then + if ref.hasField(target) then + ref.fieldValue(target) + else + // initialization error, reported by the initialization checker + Bottom + else + if ref.klass.isSubClass(receiver.widenSingleton.classSymbol) then + report.error("[Internal error] Unexpected resolution failure: ref.klass = " + ref.klass.show + ", field = " + field.show + Trace.show, Trace.position) + Bottom + else + // This is possible due to incorrect type cast. + // See tests/init/pos/Type.scala + Bottom + + case OfType(tp) => + if field.isEffectivelyFinal then + if field.hasSource then + val vdef = field.defTree.asInstanceOf[ValDef] + eval(vdef.rhs, thisV, field.owner.enclosingClass.asClass) + else + Bottom + else + val fieldType = tp.memberInfo(field) + OfType(fieldType) + + case fun: Fun => + report.error("[Internal error] unexpected tree in selecting a function, fun = " + fun.expr.show + Trace.show, fun.expr) + Bottom + + case RefSet(refs) => + refs.map(ref => select(ref, field, receiver)).join def instantiate(outer: Value, klass: ClassSymbol, ctor: Symbol, args: List[ArgInfo]): Contextual[Value] = ??? From d6278d14ccb44a01c4d0eafad7ac7fc69358a340 Mon Sep 17 00:00:00 2001 From: Fengyun Liu Date: Sun, 8 Jan 2023 16:31:13 +0100 Subject: [PATCH 017/113] Fix instantiate --- .../tools/dotc/transform/init/Objects.scala | 24 ++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/compiler/src/dotty/tools/dotc/transform/init/Objects.scala b/compiler/src/dotty/tools/dotc/transform/init/Objects.scala index 94c6bcd94b6f..db00b1b7dd60 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Objects.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Objects.scala @@ -399,7 +399,29 @@ object Objects: case RefSet(refs) => refs.map(ref => select(ref, field, receiver)).join - def instantiate(outer: Value, klass: ClassSymbol, ctor: Symbol, args: List[ArgInfo]): Contextual[Value] = ??? + def instantiate(outer: Value, klass: ClassSymbol, ctor: Symbol, args: List[ArgInfo]): Contextual[Value] = + outer match + + case Fun(body, thisV, klass) => + report.error("[Internal error] unexpected tree in instantiating a function, fun = " + body.show + Trace.show, Trace.position) + Bottom + + case RefSet(refs) => + refs.map(ref => instantiate(ref, klass, ctor, args)).join + + case value: (ObjectRef | OfClass | OfType) => + // widen the outer to finitize the domain + val outerWidened = outer.widenArg + val argsWidened = args.map(_.value).widenArgs + val tp = value match + case v: ObjectRef => v.klass.typeRef.memberInfo(klass) + case v: OfClass => v.tp.memberInfo(klass) + case v: OfType => v.tp.memberInfo(klass) + + val instance = OfClass(tp, klass, outerWidened, ctor, argsWidened) + val argInfos2 = args.zip(argsWidened).map { (argInfo, v) => argInfo.copy(value = v) } + callConstructor(instance, ctor, argInfos2) + instance // -------------------------------- algorithm -------------------------------- From 4b8a041aa582594c62e46e5bfd962f0eb0b1e929 Mon Sep 17 00:00:00 2001 From: Fengyun Liu Date: Sun, 8 Jan 2023 17:28:53 +0100 Subject: [PATCH 018/113] First test passes --- .../tools/dotc/transform/init/Checker.scala | 1 + .../tools/dotc/transform/init/Objects.scala | 60 +++++++++++++------ tests/init/neg/global-irrelevance1.scala | 5 ++ 3 files changed, 49 insertions(+), 17 deletions(-) create mode 100644 tests/init/neg/global-irrelevance1.scala diff --git a/compiler/src/dotty/tools/dotc/transform/init/Checker.scala b/compiler/src/dotty/tools/dotc/transform/init/Checker.scala index 1efb3c88149e..e8f947b3b79a 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Checker.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Checker.scala @@ -37,6 +37,7 @@ class Checker extends Phase: val classes = traverser.getClasses() Semantic.checkClasses(classes)(using checkCtx) + Objects.checkClasses(classes)(using checkCtx) units diff --git a/compiler/src/dotty/tools/dotc/transform/init/Objects.scala b/compiler/src/dotty/tools/dotc/transform/init/Objects.scala index db00b1b7dd60..24669331173e 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Objects.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Objects.scala @@ -132,6 +132,8 @@ object Objects: // object -> (fun type, fun values) private[State] val instantiatedFuncs = mutable.Map.empty[ClassSymbol, Map[Type, List[Fun]]] + def currentObject(using data: Data) = data.checkingObjects.last + def checkCycle(clazz: ClassSymbol)(work: => Unit)(using data: Data, ctx: Context) = val index = data.checkingObjects.indexOf(clazz) @@ -248,7 +250,7 @@ object Objects: def widenArgs: Contextual[List[Value]] = values.map(_.widenArg).toList - def call(value: Value, meth: Symbol, args: List[ArgInfo], receiver: Type, superType: Type, needResolve: Boolean = true): Contextual[Value] = + def call(value: Value, meth: Symbol, args: List[ArgInfo], receiver: Type, superType: Type, needResolve: Boolean = true): Contextual[Value] = log("call " + meth.show + ", args = " + args.map(_.value.show), printer, (_: Value).show) { def checkArgs() = // TODO: check aliasing of static objects for @@ -261,6 +263,8 @@ object Objects: case _ => value match + case Bottom => + Bottom case ref: Ref => checkArgs() @@ -327,8 +331,9 @@ object Objects: case RefSet(vs) => vs.map(v => call(v, meth, args, receiver, superType)).join + } - def callConstructor(thisV: Value, ctor: Symbol, args: List[ArgInfo]): Contextual[Value] = + def callConstructor(thisV: Value, ctor: Symbol, args: List[ArgInfo]): Contextual[Value] = log("call " + ctor.show + ", args = " + args.map(_.value.show), printer, (_: Value).show) { // init "fake" param fields for parameters of primary and secondary constructors def addParamsAsFields(args: List[Value], ref: Ref, ctorDef: DefDef) = val params = ctorDef.termParamss.flatten.map(_.symbol) @@ -357,9 +362,9 @@ object Objects: case _ => report.error("[Internal error] unexpected constructor call, meth = " + ctor + ", this = " + thisV + Trace.show, Trace.position) Bottom + } - - def select(thisV: Value, field: Symbol, receiver: Type, needResolve: Boolean = true): Contextual[Value] = + def select(thisV: Value, field: Symbol, receiver: Type, needResolve: Boolean = true): Contextual[Value] = log("select " + field.show + ", this = " + thisV, printer, (_: Value).show) { thisV match case ref: Ref => val target = if needResolve then resolve(ref.klass, field) else field @@ -367,11 +372,25 @@ object Objects: val rhs = target.defTree.asInstanceOf[ValDef].rhs eval(rhs, ref, target.owner.asClass) else if target.exists then - if ref.hasField(target) then - ref.fieldValue(target) - else - // initialization error, reported by the initialization checker - Bottom + ref match + case obj: ObjectRef if State.currentObject != obj.klass => + if target.isOneOf(Flags.Mutable) then + report.warning("Reading mutable state of " + obj.klass + " during initialization of " + State.currentObject + " is discouraged as it breaks initialization-time irrelevance", Trace.position) + Bottom + else + if ref.hasField(target) then + // TODO: check immutability + ref.fieldValue(target) + else + // initialization error, reported by the initialization checker + Bottom + + case _ => + if ref.hasField(target) then + ref.fieldValue(target) + else + // initialization error, reported by the initialization checker + Bottom else if ref.klass.isSubClass(receiver.widenSingleton.classSymbol) then report.error("[Internal error] Unexpected resolution failure: ref.klass = " + ref.klass.show + ", field = " + field.show + Trace.show, Trace.position) @@ -398,8 +417,9 @@ object Objects: case RefSet(refs) => refs.map(ref => select(ref, field, receiver)).join + } - def instantiate(outer: Value, klass: ClassSymbol, ctor: Symbol, args: List[ArgInfo]): Contextual[Value] = + def instantiate(outer: Value, klass: ClassSymbol, ctor: Symbol, args: List[ArgInfo]): Contextual[Value] = log("instantiating " + klass.show + ", outer = " + outer + ", args = " + args.map(_.value.show), printer, (_: Value).show) { outer match case Fun(body, thisV, klass) => @@ -422,17 +442,18 @@ object Objects: val argInfos2 = args.zip(argsWidened).map { (argInfo, v) => argInfo.copy(value = v) } callConstructor(instance, ctor, argInfos2) instance + } // -------------------------------- algorithm -------------------------------- /** Check an individual object */ - private def accessObject(classSym: ClassSymbol)(using Context, State.Data): Value = + private def accessObject(classSym: ClassSymbol)(using Context, State.Data): Value = log("accessing object " + classSym.show, printer, (_: Value).show) { val tpl = classSym.defTree.asInstanceOf[TypeDef].rhs.asInstanceOf[Template] @tailrec def iterate()(using Context): Unit = given cache: Cache.Data = new Cache.Data - given Trace = Trace.empty + given Trace = Trace.empty.add(tpl) init(tpl, ObjectRef(classSym), classSym) @@ -440,16 +461,17 @@ object Objects: if cache.hasChanged && !hasError then cache.prepareForNextIteration() iterate() - else - ctx.reporter.flush() end iterate State.checkCycle(classSym) { val reporter = new StoreReporter(ctx.reporter) iterate()(using ctx.fresh.setReporter(reporter)) + for warning <- reporter.pendingMessages do + ctx.reporter.report(warning) } ObjectRef(classSym) + } def checkClasses(classes: List[ClassSymbol])(using Context): Unit = given State.Data = new State.Data @@ -470,7 +492,7 @@ object Objects: * @param thisV The value for `C.this` where `C` is represented by the parameter `klass`. * @param klass The enclosing class where the expression `expr` is located. */ - def eval(expr: Tree, thisV: Value, klass: ClassSymbol): Contextual[Value] = + def eval(expr: Tree, thisV: Value, klass: ClassSymbol): Contextual[Value] = log("evaluating " + expr.show + ", this = " + thisV.show + " in " + klass.show, printer, (_: Value).show) { expr match case Ident(nme.WILDCARD) => // TODO: disallow `var x: T = _` @@ -619,7 +641,8 @@ object Objects: case _ => report.error("[Internal error] unexpected tree" + Trace.show, expr) - OfType(expr.tpe) + Bottom + } /** Handle semantics of leaf nodes * @@ -695,7 +718,7 @@ object Objects: } argInfos.toList - def init(tpl: Template, thisV: Ref, klass: ClassSymbol): Contextual[Unit] = + def init(tpl: Template, thisV: Ref, klass: ClassSymbol): Contextual[Value] = log("init " + klass.show, printer, (_: Value).show) { val paramsMap = tpl.constr.termParamss.flatten.map { vdef => vdef.name -> thisV.fieldValue(vdef.symbol) }.toMap @@ -800,6 +823,9 @@ object Objects: eval(tree, thisV, klass) } + thisV + } + /** Resolve C.this that appear in `klass` * diff --git a/tests/init/neg/global-irrelevance1.scala b/tests/init/neg/global-irrelevance1.scala new file mode 100644 index 000000000000..903d3b14ae18 --- /dev/null +++ b/tests/init/neg/global-irrelevance1.scala @@ -0,0 +1,5 @@ +object A: + var x = 6 + +object B: + var y = A.x * 2 // error \ No newline at end of file From 3e0f1b90071ce9a569f43b3f5d2e6f7a74f4a08f Mon Sep 17 00:00:00 2001 From: Fengyun Liu Date: Mon, 9 Jan 2023 09:19:51 +0100 Subject: [PATCH 019/113] Extract common code to separate files --- .../dotty/tools/dotc/transform/init/Objects.scala | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/transform/init/Objects.scala b/compiler/src/dotty/tools/dotc/transform/init/Objects.scala index 24669331173e..131502a32d0c 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Objects.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Objects.scala @@ -16,10 +16,8 @@ import reporting.StoreReporter import reporting.trace as log import Errors.* - -import Semantic.{ NewExpr, Call, TraceValue, Trace, PolyFun, Arg, ByNameArg } - -import Semantic.{ typeRefOf, hasSource, extendTrace, withTrace, trace, resolve } +import Trace.* +import Util.* import scala.collection.mutable import scala.annotation.tailrec @@ -138,8 +136,8 @@ object Objects: val index = data.checkingObjects.indexOf(clazz) if index != -1 then - val cycle = data.checkingObjects.slice(index, data.checkingObjects.size - 1) - report.warning("Cyclic initialization: " + cycle.map(_.show).mkString(" -> ") + clazz.show, clazz.defTree) + val cycle = data.checkingObjects.slice(index, data.checkingObjects.size) + report.warning("Cyclic initialization: " + cycle.map(_.show).mkString(" -> ") + " -> " + clazz.show, clazz.defTree) else if data.checkedObjects.indexOf(clazz) == -1 then data.checkingObjects += clazz work @@ -879,7 +877,3 @@ object Objects: else if cls.isAllOf(Flags.JavaInterface) then Bottom else evalType(tref.prefix, thisV, klass, elideObjectAccess = cls.isStatic) - - extension (sym: Symbol) - def isStaticObject(using Context) = - sym.is(Flags.Module, butNot = Flags.Package) && sym.isStatic From c7642f3b285d922b56fc88855eba7a99c75acc37 Mon Sep 17 00:00:00 2001 From: Fengyun Liu Date: Mon, 9 Jan 2023 09:39:34 +0100 Subject: [PATCH 020/113] Fix crash for instantiating top-level classes --- .../tools/dotc/transform/init/Objects.scala | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/transform/init/Objects.scala b/compiler/src/dotty/tools/dotc/transform/init/Objects.scala index 131502a32d0c..4f4c7dd2552c 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Objects.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Objects.scala @@ -239,6 +239,7 @@ object Objects: def widenArg(using Context): Value = a match + case Bottom => Bottom case RefSet(refs) => refs.map(_.widenArg).join case OfClass(tp, _, _: OfClass, _, _) => OfType(tp) case _ => a @@ -424,22 +425,25 @@ object Objects: report.error("[Internal error] unexpected tree in instantiating a function, fun = " + body.show + Trace.show, Trace.position) Bottom - case RefSet(refs) => - refs.map(ref => instantiate(ref, klass, ctor, args)).join - - case value: (ObjectRef | OfClass | OfType) => + case value: (Bottom.type | ObjectRef | OfClass | OfType) => // widen the outer to finitize the domain val outerWidened = outer.widenArg val argsWidened = args.map(_.value).widenArgs + + // TODO: type arguments val tp = value match case v: ObjectRef => v.klass.typeRef.memberInfo(klass) case v: OfClass => v.tp.memberInfo(klass) case v: OfType => v.tp.memberInfo(klass) + case Bottom => klass.typeRef val instance = OfClass(tp, klass, outerWidened, ctor, argsWidened) val argInfos2 = args.zip(argsWidened).map { (argInfo, v) => argInfo.copy(value = v) } callConstructor(instance, ctor, argInfos2) instance + + case RefSet(refs) => + refs.map(ref => instantiate(ref, klass, ctor, args)).join } // -------------------------------- algorithm -------------------------------- @@ -637,8 +641,11 @@ object Objects: case _: Import | _: Export => Bottom + case tpl: Template => + init(tpl, thisV.asInstanceOf[Ref], klass) + case _ => - report.error("[Internal error] unexpected tree" + Trace.show, expr) + report.error("[Internal error] unexpected tree: " + expr + "\n" + Trace.show, expr) Bottom } From 9160e6c1b055f6c514187ddc80498c7b2057ff7f Mon Sep 17 00:00:00 2001 From: Fengyun Liu Date: Mon, 9 Jan 2023 09:55:02 +0100 Subject: [PATCH 021/113] Refine printing --- compiler/src/dotty/tools/dotc/transform/init/Objects.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/compiler/src/dotty/tools/dotc/transform/init/Objects.scala b/compiler/src/dotty/tools/dotc/transform/init/Objects.scala index 4f4c7dd2552c..a431e362ecd3 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Objects.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Objects.scala @@ -449,7 +449,7 @@ object Objects: // -------------------------------- algorithm -------------------------------- /** Check an individual object */ - private def accessObject(classSym: ClassSymbol)(using Context, State.Data): Value = log("accessing object " + classSym.show, printer, (_: Value).show) { + private def accessObject(classSym: ClassSymbol)(using Context, State.Data): Value = log("accessing " + classSym.show, printer, (_: Value).show) { val tpl = classSym.defTree.asInstanceOf[TypeDef].rhs.asInstanceOf[Template] @tailrec From daf1e50ee75914f0d19b01739e3ef9cbc53c3745 Mon Sep 17 00:00:00 2001 From: Fengyun Liu Date: Mon, 9 Jan 2023 20:45:25 +0100 Subject: [PATCH 022/113] Show trace in cyclic errors --- .../tools/dotc/transform/init/Objects.scala | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/transform/init/Objects.scala b/compiler/src/dotty/tools/dotc/transform/init/Objects.scala index a431e362ecd3..a6caa4e203ec 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Objects.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Objects.scala @@ -121,8 +121,9 @@ object Objects: */ class Data: // objects under check - private[State] val checkingObjects = new mutable.ArrayBuffer[ClassSymbol] + private[State] val checkingObjects = new mutable.ListBuffer[ClassSymbol] private[State] val checkedObjects = new mutable.ArrayBuffer[ClassSymbol] + private[State] val pendingTraces = new mutable.ListBuffer[Trace] // object -> (class, types of the class) private[State] val instantiatedTypes = mutable.Map.empty[ClassSymbol, Map[ClassSymbol, List[Type]]] @@ -132,16 +133,20 @@ object Objects: def currentObject(using data: Data) = data.checkingObjects.last - def checkCycle(clazz: ClassSymbol)(work: => Unit)(using data: Data, ctx: Context) = + def checkCycle(clazz: ClassSymbol)(work: => Unit)(using data: Data, ctx: Context, pendingTrace: Trace) = val index = data.checkingObjects.indexOf(clazz) if index != -1 then + val joinedTrace = data.pendingTraces.slice(index + 1, data.checkingObjects.size).foldLeft(pendingTrace) { (a, b) => a ++ b } + val callTrace = Trace.buildStacktrace(joinedTrace, "Calling trace:\n") val cycle = data.checkingObjects.slice(index, data.checkingObjects.size) - report.warning("Cyclic initialization: " + cycle.map(_.show).mkString(" -> ") + " -> " + clazz.show, clazz.defTree) + report.warning("Cyclic initialization: " + cycle.map(_.show).mkString(" -> ") + " -> " + clazz.show + ". " + callTrace, clazz.defTree) else if data.checkedObjects.indexOf(clazz) == -1 then + data.pendingTraces += pendingTrace data.checkingObjects += clazz work assert(data.checkingObjects.last == clazz, "Expect = " + clazz.show + ", found = " + data.checkingObjects.last) + data.pendingTraces.remove(data.pendingTraces.size - 1) data.checkedObjects += data.checkingObjects.remove(data.checkingObjects.size - 1) type Contextual[T] = (Context, State.Data, Cache.Data, Trace) ?=> T @@ -414,6 +419,10 @@ object Objects: report.error("[Internal error] unexpected tree in selecting a function, fun = " + fun.expr.show + Trace.show, fun.expr) Bottom + case Bottom => + if field.isStaticObject then ObjectRef(field.asClass) + else Bottom + case RefSet(refs) => refs.map(ref => select(ref, field, receiver)).join } @@ -449,7 +458,7 @@ object Objects: // -------------------------------- algorithm -------------------------------- /** Check an individual object */ - private def accessObject(classSym: ClassSymbol)(using Context, State.Data): Value = log("accessing " + classSym.show, printer, (_: Value).show) { + private def accessObject(classSym: ClassSymbol)(using Context, State.Data, Trace): Value = log("accessing " + classSym.show, printer, (_: Value).show) { val tpl = classSym.defTree.asInstanceOf[TypeDef].rhs.asInstanceOf[Template] @tailrec @@ -475,8 +484,10 @@ object Objects: ObjectRef(classSym) } + def checkClasses(classes: List[ClassSymbol])(using Context): Unit = given State.Data = new State.Data + given Trace = Trace.empty for classSym <- classes if classSym.isStaticObject From 9c2a570f5d74fdaa13a78c6b558b8fe473f70fe9 Mon Sep 17 00:00:00 2001 From: Fengyun Liu Date: Wed, 11 Jan 2023 08:43:20 +0100 Subject: [PATCH 023/113] Enable cache --- .../tools/dotc/transform/init/Objects.scala | 151 ++++++++---------- 1 file changed, 65 insertions(+), 86 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/transform/init/Objects.scala b/compiler/src/dotty/tools/dotc/transform/init/Objects.scala index a6caa4e203ec..faa58b91493f 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Objects.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Objects.scala @@ -100,6 +100,8 @@ object Objects: /** * Represents a lambda expression + * + * TODO: add concrete type of lambdas */ case class Fun(expr: Tree, thisV: Value, klass: ClassSymbol) extends Value: def show(using Context) = "Fun(" + expr.show + ", " + thisV.show + ", " + klass.show + ")" @@ -116,6 +118,12 @@ object Objects: val Bottom = RefSet(Nil) object State: + /** Record leaked instantiated types and lambdas. + * + * For more fine-grained approximation, the values are distinguished by their types. + */ + class LeakedInstances(classes: Map[ClassSymbol, List[Type]], funs: Map[Type, List[Fun]]) + /** * Remembers the instantiated types during instantiation of a static object. */ @@ -125,13 +133,12 @@ object Objects: private[State] val checkedObjects = new mutable.ArrayBuffer[ClassSymbol] private[State] val pendingTraces = new mutable.ListBuffer[Trace] - // object -> (class, types of the class) - private[State] val instantiatedTypes = mutable.Map.empty[ClassSymbol, Map[ClassSymbol, List[Type]]] + private[State] val leakedInstancesByObject = mutable.Map.empty[ClassSymbol, LeakedInstances] + end Data - // object -> (fun type, fun values) - private[State] val instantiatedFuncs = mutable.Map.empty[ClassSymbol, Map[Type, List[Fun]]] + def currentObject(using data: Data): ClassSymbol = data.checkingObjects.last - def currentObject(using data: Data) = data.checkingObjects.last + def leakedInstances(using data: Data): LeakedInstances = data.leakedInstancesByObject(currentObject) def checkCycle(clazz: ClassSymbol)(work: => Unit)(using data: Data, ctx: Context, pendingTrace: Trace) = val index = data.checkingObjects.indexOf(clazz) @@ -148,85 +155,28 @@ object Objects: assert(data.checkingObjects.last == clazz, "Expect = " + clazz.show + ", found = " + data.checkingObjects.last) data.pendingTraces.remove(data.pendingTraces.size - 1) data.checkedObjects += data.checkingObjects.remove(data.checkingObjects.size - 1) - - type Contextual[T] = (Context, State.Data, Cache.Data, Trace) ?=> T + end checkCycle + end State object Cache: - /** Cache for method calls and lazy values - * - * Symbol -> ThisValue -> ReturnValue - */ - private type Cache = Map[Symbol, Map[Value, Value]] + case class Config(value: Value, leakedInstances: State.LeakedInstances) - class Data: - /** The cache from last iteration */ - private var last: Cache = Map.empty - - /** The output cache - * - * The output cache is computed based on the cache values `last` from the - * last iteration. - * - * Both `last` and `current` are required to make sure evaluation happens - * once in each iteration. - */ - private var current: Cache = Map.empty - - /** Whether the current heap is different from the last heap? - * - * `changed == false` implies that the fixed point has been reached. - */ - private var changed: Boolean = false - - def assume(value: Value, sym: Symbol)(fun: => Value): Contextual[Value] = - val assumeValue: Value = - last.get(value, sym) match - case Some(value) => value - case None => - val default = Bottom - this.last = last.updatedNested(value, sym, default) - default - - this.current = current.updatedNested(value, sym, assumeValue) - - val actual = fun - if actual != assumeValue then - this.changed = true - this.current = this.current.updatedNested(value, sym, actual) - end if - - actual - end assume - - def hasChanged = changed - - /** Prepare cache for the next iteration - * - * 1. Reset changed flag. - * - * 2. Use current cache as last cache and set current cache to be empty. - * - */ - def prepareForNextIteration()(using Context) = - this.changed = false - this.last = this.current - this.current = Map.empty - end Data + class Data extends Cache[Config]: + def get(thisV: Value, expr: Tree)(using State.Data): Option[Value] = + val config = Config(thisV, State.leakedInstances) + super.get(config, expr).map(_.value) - extension (cache: Cache) - private def get(value: Value, sym: Symbol): Option[Value] = - cache.get(sym).flatMap(_.get(value)) + def assume(thisV: Value, expr: Tree, cacheResult: Boolean)(fun: => Value)(using State.Data): Value = + val config = Config(thisV, State.leakedInstances) + val result = super.assume(config, expr, cacheResult, default = Config(Bottom, State.leakedInstances)) { + Config(fun, State.leakedInstances) + } + result.value + end Cache - private def removed(value: Value, sym: Symbol) = - val innerMap2 = cache(sym).removed(value) - cache.updated(sym, innerMap2) + inline def cache(using c: Cache.Data): Cache.Data = c - private def updatedNested(value: Value, sym: Symbol, result: Value): Cache = - val innerMap = cache.getOrElse(sym, Map.empty[Value, Value]) - val innerMap2 = innerMap.updated(value, result) - cache.updated(sym, innerMap2) - end extension - end Cache + type Contextual[T] = (Context, State.Data, Cache.Data, Trace) ?=> T // --------------------------- domain operations ----------------------------- @@ -289,7 +239,7 @@ object Objects: val cls = target.owner.enclosingClass.asClass val ddef = target.defTree.asInstanceOf[DefDef] extendTrace(ddef) { - eval(ddef.rhs, ref, cls) + eval(ddef.rhs, ref, cls, cacheResult = true) } else Bottom @@ -315,7 +265,7 @@ object Objects: val isLocal = meth.owner.isClass val ddef = meth.defTree.asInstanceOf[DefDef] extendTrace(ddef) { - eval(ddef.rhs, value, meth.owner.enclosingClass.asClass) + eval(ddef.rhs, value, meth.owner.enclosingClass.asClass, cacheResult = true) } else Bottom @@ -331,7 +281,7 @@ object Objects: if meth.name.toString == "tupled" then value // a call like `fun.tupled` else - eval(expr, thisV, klass) + eval(expr, thisV, klass, cacheResult = true) case RefSet(vs) => vs.map(v => call(v, meth, args, receiver, superType)).join @@ -355,10 +305,10 @@ object Objects: addParamsAsFields(args2, ref, ddef) if ctor.isPrimaryConstructor then val tpl = cls.defTree.asInstanceOf[TypeDef].rhs.asInstanceOf[Template] - extendTrace(cls.defTree) { eval(tpl, ref, cls) } + extendTrace(cls.defTree) { eval(tpl, ref, cls, cacheResult = true) } ref else - extendTrace(ddef) { eval(ddef.rhs, ref, cls) } + extendTrace(ddef) { eval(ddef.rhs, ref, cls, cacheResult = true) } else // no source code available Bottom @@ -374,7 +324,7 @@ object Objects: val target = if needResolve then resolve(ref.klass, field) else field if target.is(Flags.Lazy) then val rhs = target.defTree.asInstanceOf[ValDef].rhs - eval(rhs, ref, target.owner.asClass) + eval(rhs, ref, target.owner.asClass, cacheResult = true) else if target.exists then ref match case obj: ObjectRef if State.currentObject != obj.klass => @@ -408,7 +358,7 @@ object Objects: if field.isEffectivelyFinal then if field.hasSource then val vdef = field.defTree.asInstanceOf[ValDef] - eval(vdef.rhs, thisV, field.owner.enclosingClass.asClass) + eval(vdef.rhs, thisV, field.owner.enclosingClass.asClass, cacheResult = true) else Bottom else @@ -494,18 +444,47 @@ object Objects: do accessObject(classSym) + /** Evaluate an expression with the given value for `this` in a given class `klass` + * + * Note that `klass` might be a super class of the object referred by `thisV`. + * The parameter `klass` is needed for `this` resolution. Consider the following code: + * + * class A { + * A.this + * class B extends A { A.this } + * } + * + * As can be seen above, the meaning of the expression `A.this` depends on where + * it is located. + * + * This method only handles cache logic and delegates the work to `cases`. + * + * @param expr The expression to be evaluated. + * @param thisV The value for `C.this` where `C` is represented by the parameter `klass`. + * @param klass The enclosing class where the expression is located. + * @param cacheResult It is used to reduce the size of the cache. + */ + def eval(expr: Tree, thisV: Value, klass: ClassSymbol, cacheResult: Boolean = false): Contextual[Value] = log("evaluating " + expr.show + ", this = " + thisV.show + " in " + klass.show, printer, (_: Value).show) { + cache.get(thisV, expr) match + case Some(value) => value + case None => + cache.assume(thisV, expr, cacheResult) { cases(expr, thisV, klass) } + } + /** Evaluate a list of expressions */ def evalExprs(exprs: List[Tree], thisV: Value, klass: ClassSymbol): Contextual[List[Value]] = exprs.map { expr => eval(expr, thisV, klass) } /** Handles the evaluation of different expressions + * + * Note: Recursive call should go to `eval` instead of `cases`. * * @param expr The expression to be evaluated. * @param thisV The value for `C.this` where `C` is represented by the parameter `klass`. * @param klass The enclosing class where the expression `expr` is located. */ - def eval(expr: Tree, thisV: Value, klass: ClassSymbol): Contextual[Value] = log("evaluating " + expr.show + ", this = " + thisV.show + " in " + klass.show, printer, (_: Value).show) { + def cases(expr: Tree, thisV: Value, klass: ClassSymbol): Contextual[Value] = log("evaluating " + expr.show + ", this = " + thisV.show + " in " + klass.show, printer, (_: Value).show) { expr match case Ident(nme.WILDCARD) => // TODO: disallow `var x: T = _` From 3ff377403f12ab2df2fdcc1886cc280d8c062e21 Mon Sep 17 00:00:00 2001 From: Fengyun Liu Date: Thu, 12 Jan 2023 07:28:18 +0100 Subject: [PATCH 024/113] Distinguish Config and Res in Cache --- .../dotty/tools/dotc/transform/init/Objects.scala | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/transform/init/Objects.scala b/compiler/src/dotty/tools/dotc/transform/init/Objects.scala index faa58b91493f..1504eb0ee6ec 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Objects.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Objects.scala @@ -124,6 +124,9 @@ object Objects: */ class LeakedInstances(classes: Map[ClassSymbol, List[Type]], funs: Map[Type, List[Fun]]) + object LeakedInstances: + val empty = LeakedInstances(Map.empty, Map.empty) + /** * Remembers the instantiated types during instantiation of a static object. */ @@ -138,7 +141,8 @@ object Objects: def currentObject(using data: Data): ClassSymbol = data.checkingObjects.last - def leakedInstances(using data: Data): LeakedInstances = data.leakedInstancesByObject(currentObject) + def leakedInstances(using data: Data): LeakedInstances = + data.leakedInstancesByObject.getOrElseUpdate(currentObject, LeakedInstances.empty) def checkCycle(clazz: ClassSymbol)(work: => Unit)(using data: Data, ctx: Context, pendingTrace: Trace) = val index = data.checkingObjects.indexOf(clazz) @@ -159,17 +163,18 @@ object Objects: end State object Cache: - case class Config(value: Value, leakedInstances: State.LeakedInstances) + case class Config(thisV: Value, leakedInstances: State.LeakedInstances) + case class Res(value: Value, leakedInstances: State.LeakedInstances) - class Data extends Cache[Config]: + class Data extends Cache[Config, Res]: def get(thisV: Value, expr: Tree)(using State.Data): Option[Value] = val config = Config(thisV, State.leakedInstances) super.get(config, expr).map(_.value) def assume(thisV: Value, expr: Tree, cacheResult: Boolean)(fun: => Value)(using State.Data): Value = val config = Config(thisV, State.leakedInstances) - val result = super.assume(config, expr, cacheResult, default = Config(Bottom, State.leakedInstances)) { - Config(fun, State.leakedInstances) + val result = super.assume(config, expr, cacheResult, default = Res(Bottom, State.leakedInstances)) { + Res(fun, State.leakedInstances) } result.value end Cache From 46b25947dc8803b2c9838235b14cb56250082ae2 Mon Sep 17 00:00:00 2001 From: Fengyun Liu Date: Thu, 12 Jan 2023 07:33:31 +0100 Subject: [PATCH 025/113] Better trace --- compiler/src/dotty/tools/dotc/transform/init/Objects.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/compiler/src/dotty/tools/dotc/transform/init/Objects.scala b/compiler/src/dotty/tools/dotc/transform/init/Objects.scala index 1504eb0ee6ec..6f3f5c831f62 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Objects.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Objects.scala @@ -497,7 +497,7 @@ object Objects: case id @ Ident(name) if !id.symbol.is(Flags.Method) => assert(name.isTermName, "type trees should not reach here") - evalType(expr.tpe, thisV, klass) + extendTrace(expr) { evalType(expr.tpe, thisV, klass) } case NewExpr(tref, New(tpt), ctor, argss) => // check args From 61152e0dd958f66c557900ec19e273d41ee579ba Mon Sep 17 00:00:00 2001 From: Fengyun Liu Date: Thu, 12 Jan 2023 08:29:28 +0100 Subject: [PATCH 026/113] Fix termination and cache for tests/init/neg/apply2.scala --- .../tools/dotc/transform/init/Objects.scala | 39 +++++++++++-------- 1 file changed, 23 insertions(+), 16 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/transform/init/Objects.scala b/compiler/src/dotty/tools/dotc/transform/init/Objects.scala index 6f3f5c831f62..3038527ad4fe 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Objects.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Objects.scala @@ -414,27 +414,34 @@ object Objects: /** Check an individual object */ private def accessObject(classSym: ClassSymbol)(using Context, State.Data, Trace): Value = log("accessing " + classSym.show, printer, (_: Value).show) { - val tpl = classSym.defTree.asInstanceOf[TypeDef].rhs.asInstanceOf[Template] + if classSym.hasSource then + val tpl = classSym.defTree.asInstanceOf[TypeDef].rhs.asInstanceOf[Template] - @tailrec - def iterate()(using Context): Unit = - given cache: Cache.Data = new Cache.Data + var count = 0 + given Cache.Data = new Cache.Data given Trace = Trace.empty.add(tpl) - init(tpl, ObjectRef(classSym), classSym) + @tailrec + def iterate()(using Context): Unit = + count += 1 - val hasError = ctx.reporter.pendingMessages.nonEmpty - if cache.hasChanged && !hasError then - cache.prepareForNextIteration() - iterate() - end iterate + log("Iteration " + count) { + init(tpl, ObjectRef(classSym), classSym) + } - State.checkCycle(classSym) { - val reporter = new StoreReporter(ctx.reporter) - iterate()(using ctx.fresh.setReporter(reporter)) - for warning <- reporter.pendingMessages do - ctx.reporter.report(warning) - } + val hasError = ctx.reporter.pendingMessages.nonEmpty + if cache.hasChanged && !hasError then + cache.prepareForNextIteration() + iterate() + end iterate + + State.checkCycle(classSym) { + val reporter = new StoreReporter(ctx.reporter) + iterate()(using ctx.fresh.setReporter(reporter)) + for warning <- reporter.pendingMessages do + ctx.reporter.report(warning) + } + end if ObjectRef(classSym) } From 778cf2d40d1d06bfaf36edf4dd39d724264715f8 Mon Sep 17 00:00:00 2001 From: Fengyun Liu Date: Thu, 12 Jan 2023 08:35:59 +0100 Subject: [PATCH 027/113] Fix cast crash --- compiler/src/dotty/tools/dotc/transform/init/Objects.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/compiler/src/dotty/tools/dotc/transform/init/Objects.scala b/compiler/src/dotty/tools/dotc/transform/init/Objects.scala index 3038527ad4fe..d373af1a224b 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Objects.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Objects.scala @@ -375,7 +375,7 @@ object Objects: Bottom case Bottom => - if field.isStaticObject then ObjectRef(field.asClass) + if field.isStaticObject then ObjectRef(field.moduleClass.asClass) else Bottom case RefSet(refs) => From 8793ba831f07746c31dc30b92cb2e172186e455a Mon Sep 17 00:00:00 2001 From: Fengyun Liu Date: Thu, 12 Jan 2023 19:33:16 +0100 Subject: [PATCH 028/113] Fix trace for objects --- .../tools/dotc/transform/init/Objects.scala | 31 +++++++++++-------- 1 file changed, 18 insertions(+), 13 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/transform/init/Objects.scala b/compiler/src/dotty/tools/dotc/transform/init/Objects.scala index d373af1a224b..fce76ce55350 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Objects.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Objects.scala @@ -419,12 +419,13 @@ object Objects: var count = 0 given Cache.Data = new Cache.Data - given Trace = Trace.empty.add(tpl) @tailrec def iterate()(using Context): Unit = count += 1 + given Trace = Trace.empty.add(tpl) + log("Iteration " + count) { init(tpl, ObjectRef(classSym), classSym) } @@ -497,6 +498,8 @@ object Objects: * @param klass The enclosing class where the expression `expr` is located. */ def cases(expr: Tree, thisV: Value, klass: ClassSymbol): Contextual[Value] = log("evaluating " + expr.show + ", this = " + thisV.show + " in " + klass.show, printer, (_: Value).show) { + val trace2 = trace.add(expr) + expr match case Ident(nme.WILDCARD) => // TODO: disallow `var x: T = _` @@ -504,15 +507,17 @@ object Objects: case id @ Ident(name) if !id.symbol.is(Flags.Method) => assert(name.isTermName, "type trees should not reach here") - extendTrace(expr) { evalType(expr.tpe, thisV, klass) } + withTrace(trace2) { evalType(expr.tpe, thisV, klass) } case NewExpr(tref, New(tpt), ctor, argss) => // check args val args = evalArgs(argss.flatten, thisV, klass) val cls = tref.classSymbol.asClass - val outer = outerValue(tref, thisV, klass) - instantiate(outer, cls, ctor, args) + withTrace(trace2) { + val outer = outerValue(tref, thisV, klass) + instantiate(outer, cls, ctor, args) + } case Call(ref, argss) => // check args @@ -522,14 +527,14 @@ object Objects: case Select(supert: Super, _) => val SuperType(thisTp, superTp) = supert.tpe: @unchecked val thisValue2 = extendTrace(ref) { resolveThis(thisTp.classSymbol.asClass, thisV, klass) } - call(thisValue2, ref.symbol, args, thisTp, superTp) + withTrace(trace2) { call(thisValue2, ref.symbol, args, thisTp, superTp) } case Select(qual, _) => val receiver = eval(qual, thisV, klass) if ref.symbol.isConstructor then - callConstructor(receiver, ref.symbol, args) + withTrace(trace2) { callConstructor(receiver, ref.symbol, args) } else - call(receiver, ref.symbol, args, receiver = qual.tpe, superType = NoType) + withTrace(trace2) { call(receiver, ref.symbol, args, receiver = qual.tpe, superType = NoType) } case id: Ident => id.tpe match @@ -538,13 +543,13 @@ object Objects: val enclosingClass = id.symbol.owner.enclosingClass.asClass val thisValue2 = extendTrace(ref) { resolveThis(enclosingClass, thisV, klass) } // local methods are not a member, but we can reuse the method `call` - call(thisValue2, id.symbol, args, receiver = NoType, superType = NoType, needResolve = false) + withTrace(trace2) { call(thisValue2, id.symbol, args, receiver = NoType, superType = NoType, needResolve = false) } case TermRef(prefix, _) => - val receiver = evalType(prefix, thisV, klass) + val receiver = withTrace(trace2) { evalType(prefix, thisV, klass) } if id.symbol.isConstructor then - callConstructor(receiver, id.symbol, args) + withTrace(trace2) { callConstructor(receiver, id.symbol, args) } else - call(receiver, id.symbol, args, receiver = prefix, superType = NoType) + withTrace(trace2) { call(receiver, id.symbol, args, receiver = prefix, superType = NoType) } case Select(qualifier, name) => val qual = eval(qualifier, thisV, klass) @@ -553,9 +558,9 @@ object Objects: case OuterSelectName(_, _) => val current = qualifier.tpe.classSymbol val target = expr.tpe.widenSingleton.classSymbol.asClass - resolveThis(target, qual, current.asClass) + withTrace(trace2) { resolveThis(target, qual, current.asClass) } case _ => - select(qual, expr.symbol, receiver = qualifier.tpe) + withTrace(trace2) { select(qual, expr.symbol, receiver = qualifier.tpe) } case _: This => evalType(expr.tpe, thisV, klass) From b1adb985072922af91b7a9cbe331fe995d3c4a0b Mon Sep 17 00:00:00 2001 From: Fengyun Liu Date: Fri, 13 Jan 2023 07:36:44 +0100 Subject: [PATCH 029/113] Reorganize tests --- tests/init/{full => }/neg/global-cycle1.scala | 0 tests/init/{full => }/neg/global-cycle2.scala | 0 tests/init/{full => }/neg/global-cycle3.scala | 0 tests/init/{full => }/neg/global-cycle4.scala | 0 tests/init/{full => }/neg/i9176.scala | 0 5 files changed, 0 insertions(+), 0 deletions(-) rename tests/init/{full => }/neg/global-cycle1.scala (100%) rename tests/init/{full => }/neg/global-cycle2.scala (100%) rename tests/init/{full => }/neg/global-cycle3.scala (100%) rename tests/init/{full => }/neg/global-cycle4.scala (100%) rename tests/init/{full => }/neg/i9176.scala (100%) diff --git a/tests/init/full/neg/global-cycle1.scala b/tests/init/neg/global-cycle1.scala similarity index 100% rename from tests/init/full/neg/global-cycle1.scala rename to tests/init/neg/global-cycle1.scala diff --git a/tests/init/full/neg/global-cycle2.scala b/tests/init/neg/global-cycle2.scala similarity index 100% rename from tests/init/full/neg/global-cycle2.scala rename to tests/init/neg/global-cycle2.scala diff --git a/tests/init/full/neg/global-cycle3.scala b/tests/init/neg/global-cycle3.scala similarity index 100% rename from tests/init/full/neg/global-cycle3.scala rename to tests/init/neg/global-cycle3.scala diff --git a/tests/init/full/neg/global-cycle4.scala b/tests/init/neg/global-cycle4.scala similarity index 100% rename from tests/init/full/neg/global-cycle4.scala rename to tests/init/neg/global-cycle4.scala diff --git a/tests/init/full/neg/i9176.scala b/tests/init/neg/i9176.scala similarity index 100% rename from tests/init/full/neg/i9176.scala rename to tests/init/neg/i9176.scala From 055f1d7c526560cf643b110400a056267c9daa8b Mon Sep 17 00:00:00 2001 From: Fengyun Liu Date: Fri, 13 Jan 2023 07:43:49 +0100 Subject: [PATCH 030/113] Fix error annotations --- tests/init/neg/global-cycle1.check | 10 ++++++++++ tests/init/neg/global-cycle1.scala | 8 ++++---- tests/init/neg/global-cycle2.scala | 4 ++-- tests/init/neg/global-cycle3.scala | 4 ++-- tests/init/neg/global-cycle4.scala | 4 ++-- tests/init/neg/i9176.scala | 4 ++-- 6 files changed, 22 insertions(+), 12 deletions(-) create mode 100644 tests/init/neg/global-cycle1.check diff --git a/tests/init/neg/global-cycle1.check b/tests/init/neg/global-cycle1.check new file mode 100644 index 000000000000..9456f856b371 --- /dev/null +++ b/tests/init/neg/global-cycle1.check @@ -0,0 +1,10 @@ +-- Error: tests/init/neg/global-cycle1.scala:1:7 ----------------------------------------------------------------------- +1 |object A { // error + |^ + |Cyclic initialization: object A -> object B -> object A. Calling trace: + |-> val b: Int = A.a [ global-cycle1.scala:6 ] + | ^ + |-> val a: Int = B.b [ global-cycle1.scala:2 ] + | ^ +2 | val a: Int = B.b +3 |} diff --git a/tests/init/neg/global-cycle1.scala b/tests/init/neg/global-cycle1.scala index ebd667c51ba0..ee74b08d4a06 100644 --- a/tests/init/neg/global-cycle1.scala +++ b/tests/init/neg/global-cycle1.scala @@ -1,10 +1,10 @@ -object A { - val a: Int = B.b // error +object A { // error + val a: Int = B.b } object B { - val b: Int = A.a // error + val b: Int = A.a } @main -def Test = print(A.a) \ No newline at end of file +def Test = print(A.a) diff --git a/tests/init/neg/global-cycle2.scala b/tests/init/neg/global-cycle2.scala index 30792e58af6b..cfb29bf43ec0 100644 --- a/tests/init/neg/global-cycle2.scala +++ b/tests/init/neg/global-cycle2.scala @@ -1,5 +1,5 @@ -object A { - val a: Int = B.foo() // error +object A { // error + val a: Int = B.foo() } object B { diff --git a/tests/init/neg/global-cycle3.scala b/tests/init/neg/global-cycle3.scala index 7fae20dbe894..3d3b96fc6de0 100644 --- a/tests/init/neg/global-cycle3.scala +++ b/tests/init/neg/global-cycle3.scala @@ -2,6 +2,6 @@ class A(x: Int) { def foo(): Int = B.a + 10 } -object B { - val a: Int = A(4).foo() // error +object B { // error + val a: Int = A(4).foo() } diff --git a/tests/init/neg/global-cycle4.scala b/tests/init/neg/global-cycle4.scala index 3de0533cb521..be8232f25379 100644 --- a/tests/init/neg/global-cycle4.scala +++ b/tests/init/neg/global-cycle4.scala @@ -14,6 +14,6 @@ class D(x: Int) { def bar(): A = if x > 0 then new B else new C } -object O { - val a: Int = D(5).bar().foo() // error +object O { // error + val a: Int = D(5).bar().foo() } diff --git a/tests/init/neg/i9176.scala b/tests/init/neg/i9176.scala index abb8a6394dd2..c93a16f2f8b1 100644 --- a/tests/init/neg/i9176.scala +++ b/tests/init/neg/i9176.scala @@ -1,9 +1,9 @@ class Foo(val opposite: Foo) case object A extends Foo(B) // error -case object B extends Foo(A) // error +case object B extends Foo(A) object Test { def main(args: Array[String]): Unit = { println(A.opposite) println(B.opposite) } -} \ No newline at end of file +} From d41cf8be7c8004c793ce163282fcc332fec50923 Mon Sep 17 00:00:00 2001 From: Fengyun Liu Date: Fri, 13 Jan 2023 07:58:47 +0100 Subject: [PATCH 031/113] Add t9360 and improve error message --- .../tools/dotc/transform/init/Objects.scala | 4 +-- tests/init/neg/t9360.scala | 25 +++++++++++++++++++ 2 files changed, 27 insertions(+), 2 deletions(-) create mode 100644 tests/init/neg/t9360.scala diff --git a/compiler/src/dotty/tools/dotc/transform/init/Objects.scala b/compiler/src/dotty/tools/dotc/transform/init/Objects.scala index fce76ce55350..6d64238b4b1d 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Objects.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Objects.scala @@ -148,7 +148,7 @@ object Objects: val index = data.checkingObjects.indexOf(clazz) if index != -1 then - val joinedTrace = data.pendingTraces.slice(index + 1, data.checkingObjects.size).foldLeft(pendingTrace) { (a, b) => a ++ b } + val joinedTrace = data.pendingTraces.slice(index + 1, data.checkingObjects.size).foldLeft(pendingTrace) { (a, acc) => acc ++ a } val callTrace = Trace.buildStacktrace(joinedTrace, "Calling trace:\n") val cycle = data.checkingObjects.slice(index, data.checkingObjects.size) report.warning("Cyclic initialization: " + cycle.map(_.show).mkString(" -> ") + " -> " + clazz.show + ". " + callTrace, clazz.defTree) @@ -424,7 +424,7 @@ object Objects: def iterate()(using Context): Unit = count += 1 - given Trace = Trace.empty.add(tpl) + given Trace = Trace.empty.add(classSym.defTree) log("Iteration " + count) { init(tpl, ObjectRef(classSym), classSym) diff --git a/tests/init/neg/t9360.scala b/tests/init/neg/t9360.scala new file mode 100644 index 000000000000..291c4dd05db1 --- /dev/null +++ b/tests/init/neg/t9360.scala @@ -0,0 +1,25 @@ +class BaseClass(s: String) { + def print: Unit = () +} + +object Obj { + val s: String = "hello" + + object AObj extends BaseClass(s) // error + + object BObj extends BaseClass(s) + + val list = List(AObj, BObj) + + def print = { + println(list) + } +} + +object ObjectInit { + def main(args: Array[String]) = { + Obj.AObj.print + Obj.BObj.print + Obj.print + } +} From 99eaf439870a68dd0f531e199705aa344f3b066e Mon Sep 17 00:00:00 2001 From: Fengyun Liu Date: Fri, 13 Jan 2023 08:02:38 +0100 Subject: [PATCH 032/113] Add t9312 --- tests/init/neg/t9312.scala | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 tests/init/neg/t9312.scala diff --git a/tests/init/neg/t9312.scala b/tests/init/neg/t9312.scala new file mode 100644 index 000000000000..703cf67e05c4 --- /dev/null +++ b/tests/init/neg/t9312.scala @@ -0,0 +1,23 @@ +object DeadLockTest { + def main(args: Array[String]): Unit = { + def run(block: => Unit): Unit = + new Thread(new Runnable {def run(): Unit = block}).start() + + run {println(Parent.Child1)} + run {println(Parent.Child2)} + + } + + object Parent { + trait Child { + Thread.sleep(2000) // ensure concurrent behavior + val parent = Parent + def siblings = parent.children - this + } + + object Child1 extends Child // error + object Child2 extends Child + + final val children = Set(Child1, Child2) + } +} From c04bc164f110b8463d741d0790274ac97eb0517c Mon Sep 17 00:00:00 2001 From: Fengyun Liu Date: Fri, 13 Jan 2023 08:03:52 +0100 Subject: [PATCH 033/113] Add t9261 --- tests/init/neg/t9261.scala | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 tests/init/neg/t9261.scala diff --git a/tests/init/neg/t9261.scala b/tests/init/neg/t9261.scala new file mode 100644 index 000000000000..1e23bedb9b6a --- /dev/null +++ b/tests/init/neg/t9261.scala @@ -0,0 +1,3 @@ +sealed abstract class OrderType(val reverse: OrderType) +case object Buy extends OrderType(Sell) // error +case object Sell extends OrderType(Buy) From c2ec366dd9d0fbdba45c3d2ba99fd9c7d575e24d Mon Sep 17 00:00:00 2001 From: Fengyun Liu Date: Fri, 13 Jan 2023 08:39:30 +0100 Subject: [PATCH 034/113] Add t9115 --- tests/init/neg/t9115.scala | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 tests/init/neg/t9115.scala diff --git a/tests/init/neg/t9115.scala b/tests/init/neg/t9115.scala new file mode 100644 index 000000000000..e7cfe09e560c --- /dev/null +++ b/tests/init/neg/t9115.scala @@ -0,0 +1,8 @@ +object D { + def aaa = 1 //that’s the reason + class Z (depends: Any) + case object D1 extends Z(aaa) // 'null' when calling D.D1 first time // error + case object D2 extends Z(aaa) // 'null' when calling D.D2 first time + println(D1) + println(D2) +} From e26b686d6e56a91aa49d4193ced8b3104923343d Mon Sep 17 00:00:00 2001 From: Fengyun Liu Date: Fri, 13 Jan 2023 08:41:11 +0100 Subject: [PATCH 035/113] Add t5366 --- tests/init/neg/t5366.scala | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 tests/init/neg/t5366.scala diff --git a/tests/init/neg/t5366.scala b/tests/init/neg/t5366.scala new file mode 100644 index 000000000000..854bdfe0544b --- /dev/null +++ b/tests/init/neg/t5366.scala @@ -0,0 +1,15 @@ +class IdAndMsg(val id: Int, val msg: String = "") + +case object ObjA extends IdAndMsg(1) // error +case object ObjB extends IdAndMsg(2) + +object IdAndMsg { // error + val values = List(ObjA , ObjB) +} + +object Test { + def main(args: Array[String]): Unit = { + ObjA + println(IdAndMsg.values) + } +} \ No newline at end of file From 66be029fe01cec56ffc5d8d053fa19ff32514b5a Mon Sep 17 00:00:00 2001 From: Fengyun Liu Date: Fri, 13 Jan 2023 08:44:57 +0100 Subject: [PATCH 036/113] Add i11262 --- tests/init/neg/i11262.scala | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 tests/init/neg/i11262.scala diff --git a/tests/init/neg/i11262.scala b/tests/init/neg/i11262.scala new file mode 100644 index 000000000000..5703dbe3394a --- /dev/null +++ b/tests/init/neg/i11262.scala @@ -0,0 +1,2 @@ +object A { val x: String = B.y } // error +object B { val y: String = A.x } From 3197f5a89ba898a7a016a0fbb4875f7bc9d90b40 Mon Sep 17 00:00:00 2001 From: Fengyun Liu Date: Fri, 13 Jan 2023 08:50:03 +0100 Subject: [PATCH 037/113] Add more global tests --- tests/init/neg/global-cycle10.scala | 17 +++++++++++++++++ tests/init/neg/global-cycle14.scala | 14 ++++++++++++++ tests/init/neg/global-cycle6.scala | 25 +++++++++++++++++++++++++ tests/init/neg/global-cycle7.scala | 18 ++++++++++++++++++ 4 files changed, 74 insertions(+) create mode 100644 tests/init/neg/global-cycle10.scala create mode 100644 tests/init/neg/global-cycle14.scala create mode 100644 tests/init/neg/global-cycle6.scala create mode 100644 tests/init/neg/global-cycle7.scala diff --git a/tests/init/neg/global-cycle10.scala b/tests/init/neg/global-cycle10.scala new file mode 100644 index 000000000000..5ca76d1e70fd --- /dev/null +++ b/tests/init/neg/global-cycle10.scala @@ -0,0 +1,17 @@ +abstract class Base { + def foo(): Unit + foo() +} + +object O extends Base { // error + val msg: String = "hello" + + class Inner { + println(msg) + } + + def foo() = new Inner +} + +@main +def Test = O \ No newline at end of file diff --git a/tests/init/neg/global-cycle14.scala b/tests/init/neg/global-cycle14.scala new file mode 100644 index 000000000000..ab416424478b --- /dev/null +++ b/tests/init/neg/global-cycle14.scala @@ -0,0 +1,14 @@ +object O { + case class Data(x: Int) extends (Int => Int) { + def apply(x: Int) = x * x + } + val d = Data(3) +} + +object A { // error + val n: Int = B.m +} + +object B { + val m: Int = A.n +} diff --git a/tests/init/neg/global-cycle6.scala b/tests/init/neg/global-cycle6.scala new file mode 100644 index 000000000000..2d4f23c25187 --- /dev/null +++ b/tests/init/neg/global-cycle6.scala @@ -0,0 +1,25 @@ +object A { // error + val n: Int = B.m + class Inner { + println(n) + } +} + +object B { + val a = new A.Inner + val m: Int = 10 +} + +object O { + object A { + val n: Int = B.m + class Inner { + val x: Int = 4 + } + } + + object B { + val a = new A.Inner + val m: Int = 10 + } +} \ No newline at end of file diff --git a/tests/init/neg/global-cycle7.scala b/tests/init/neg/global-cycle7.scala new file mode 100644 index 000000000000..32fdc4eb7b10 --- /dev/null +++ b/tests/init/neg/global-cycle7.scala @@ -0,0 +1,18 @@ +object A { // error + val n: Int = B.m +} + +object B { + val m: Int = A.n +} + +abstract class TokensCommon { + def maxToken: Int + + val tokenString, debugString: Array[String] = new Array[String](maxToken + 1) +} + +object JavaTokens extends TokensCommon { + final def maxToken: Int = DOUBLE + final val DOUBLE = 188 // error: but constant folding avoided the issue +} \ No newline at end of file From 3fe429f32b0d5cf06646b02cb7be751379032cd6 Mon Sep 17 00:00:00 2001 From: Fengyun Liu Date: Fri, 13 Jan 2023 09:00:34 +0100 Subject: [PATCH 038/113] Add crash test --- tests/init/crash/by-name.scala | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 tests/init/crash/by-name.scala diff --git a/tests/init/crash/by-name.scala b/tests/init/crash/by-name.scala new file mode 100644 index 000000000000..623d7af8335c --- /dev/null +++ b/tests/init/crash/by-name.scala @@ -0,0 +1,11 @@ +def time[A](f: => A): A = + val start = System.nanoTime + val res = f + val elapsed = (System.nanoTime - start) + res + +case class Foo(data: Int) + +object o: + val foo = time(Foo(3)) + println(foo.data) From 91e9c6a18aa7ec5a62d068b067981902788fb4c8 Mon Sep 17 00:00:00 2001 From: Fengyun Liu Date: Sat, 14 Jan 2023 09:18:16 +0100 Subject: [PATCH 039/113] Make sure the first outer is set --- .../tools/dotc/transform/init/Objects.scala | 19 ++++++++++++++----- tests/init/crash/t6888.scala | 19 +++++++++++++++++++ 2 files changed, 33 insertions(+), 5 deletions(-) create mode 100644 tests/init/crash/t6888.scala diff --git a/compiler/src/dotty/tools/dotc/transform/init/Objects.scala b/compiler/src/dotty/tools/dotc/transform/init/Objects.scala index 6d64238b4b1d..47ff984e7768 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Objects.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Objects.scala @@ -13,7 +13,7 @@ import NameKinds.SuperAccessorName import ast.tpd.* import config.Printers.init as printer import reporting.StoreReporter -import reporting.trace as log +import reporting.trace.force as log import Errors.* import Trace.* @@ -68,13 +68,15 @@ object Objects: def hasOuter(cls: ClassSymbol): Boolean = outers.contains(cls) - def updateField(field: Symbol, value: Value) = + def updateField(field: Symbol, value: Value)(using Context) = log("Update field " + field + " = " + value + " for " + this, printer) { assert(!fields.contains(field), "Field already set " + field) fields(field) = value + } - def updateOuter(cls: ClassSymbol, value: Value) = + def updateOuter(cls: ClassSymbol, value: Value)(using Context) = log("Update outer " + cls + " = " + value + " for " + this, printer) { assert(!outers.contains(cls), "Outer already set " + cls) outers(cls) = value + } /** A reference to a static object */ case class ObjectRef(klass: ClassSymbol) extends Ref: @@ -85,9 +87,15 @@ object Objects: * * `tp.classSymbol` should be the concrete class of the value at runtime. */ - case class OfClass(tp: Type, klass: ClassSymbol, outer: Value, ctor: Symbol, args: List[Value]) extends Ref: + case class OfClass private(tp: Type, klass: ClassSymbol, outer: Value, ctor: Symbol, args: List[Value]) extends Ref: def show(using Context) = "OfClass(" + klass.show + ", outer = " + outer + ", args = " + args.map(_.show) + ")" + object OfClass: + def apply(tp: Type, klass: ClassSymbol, outer: Value, ctor: Symbol, args: List[Value])(using Context): OfClass = + val instance = new OfClass(tp, klass, outer, ctor, args) + instance.updateOuter(klass, outer) + instance + /** * Rerepsents values of a specific type * @@ -849,7 +857,7 @@ object Objects: * Object access elission happens when the object access is used as a prefix * in `new o.C` and `C` does not need an outer. */ - def resolveThis(target: ClassSymbol, thisV: Value, klass: ClassSymbol, elideObjectAccess: Boolean = false): Contextual[Value] = + def resolveThis(target: ClassSymbol, thisV: Value, klass: ClassSymbol, elideObjectAccess: Boolean = false): Contextual[Value] = log("resolveThis target = " + target.show + ", this = " + thisV.show, printer, (_: Value).show) { if target == klass then thisV else if target.is(Flags.Package) then @@ -876,6 +884,7 @@ object Objects: Bottom case OfType(tp) => OfType(target.appliedRef) + } /** Compute the outer value that correspond to `tref.prefix` * diff --git a/tests/init/crash/t6888.scala b/tests/init/crash/t6888.scala new file mode 100644 index 000000000000..d339f840c86f --- /dev/null +++ b/tests/init/crash/t6888.scala @@ -0,0 +1,19 @@ +class C { + val x = 1 + object `$` { + val y = x + x + class abc$ { + def xy = x + y + } + object abc$ { + def xy = x + y + } + } +} + +object Test extends App { + val c = new C() + println(c.`$`.y) + println(c.`$`.abc$.xy) + println(new c.`$`.abc$().xy) +} From 5d31d3ec5031b7d251e480e97511043d9be7ec28 Mon Sep 17 00:00:00 2001 From: Fengyun Liu Date: Sat, 14 Jan 2023 09:39:54 +0100 Subject: [PATCH 040/113] Fix crash --- .../tools/dotc/transform/init/Objects.scala | 2 +- tests/init/neg/global-cycle11.scala | 22 +++++++++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) create mode 100644 tests/init/neg/global-cycle11.scala diff --git a/compiler/src/dotty/tools/dotc/transform/init/Objects.scala b/compiler/src/dotty/tools/dotc/transform/init/Objects.scala index 47ff984e7768..975e1cab61ac 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Objects.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Objects.scala @@ -213,7 +213,7 @@ object Objects: case _ => a extension (values: Seq[Value]) - def join: Value = values.reduce { (v1, v2) => v1.join(v2) } + def join: Value = if values.isEmpty then Bottom else values.reduce { (v1, v2) => v1.join(v2) } def widenArgs: Contextual[List[Value]] = values.map(_.widenArg).toList diff --git a/tests/init/neg/global-cycle11.scala b/tests/init/neg/global-cycle11.scala new file mode 100644 index 000000000000..303dcdb9c8f7 --- /dev/null +++ b/tests/init/neg/global-cycle11.scala @@ -0,0 +1,22 @@ +import scala.collection.mutable + +object NameKinds { + private val qualifiedNameKinds = mutable.HashMap[Int, QualifiedNameKind]() + + val QualifiedName: QualifiedNameKind = new QualifiedNameKind(2, ".") + + abstract class NameKind(val tag: Int) + + class QualifiedNameKind(tag: Int, val separator: String) + extends NameKind(tag) { + qualifiedNameKinds(tag) = this + } +} + +object A { // error + val n: Int = B.m +} + +object B { + val m: Int = A.n +} From e99f7065a7a7a3f222a5822ccf30d86063c6f5d9 Mon Sep 17 00:00:00 2001 From: Fengyun Liu Date: Sat, 14 Jan 2023 10:29:40 +0100 Subject: [PATCH 041/113] The two tests may cause deadlock --- .../tools/dotc/transform/init/Objects.scala | 2 +- tests/init/neg/global-cycle12.scala | 16 +++++++++++++++ tests/init/neg/global-cycle9.scala | 20 +++++++++++++++++++ 3 files changed, 37 insertions(+), 1 deletion(-) create mode 100644 tests/init/neg/global-cycle12.scala create mode 100644 tests/init/neg/global-cycle9.scala diff --git a/compiler/src/dotty/tools/dotc/transform/init/Objects.scala b/compiler/src/dotty/tools/dotc/transform/init/Objects.scala index 975e1cab61ac..c65696141db7 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Objects.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Objects.scala @@ -13,7 +13,7 @@ import NameKinds.SuperAccessorName import ast.tpd.* import config.Printers.init as printer import reporting.StoreReporter -import reporting.trace.force as log +import reporting.trace as log import Errors.* import Trace.* diff --git a/tests/init/neg/global-cycle12.scala b/tests/init/neg/global-cycle12.scala new file mode 100644 index 000000000000..300ef02ee8a9 --- /dev/null +++ b/tests/init/neg/global-cycle12.scala @@ -0,0 +1,16 @@ +// from Scala.js +object Names { // error + private final val ConstructorSimpleEncodedName: String = + "" + + final class SimpleMethodName(encoded: String) + + object SimpleMethodName { + def apply(name: String): SimpleMethodName = + val res = name == ConstructorSimpleEncodedName + new SimpleMethodName(name) + } + + val ConstructorSimpleName: SimpleMethodName = + SimpleMethodName(ConstructorSimpleEncodedName) +} diff --git a/tests/init/neg/global-cycle9.scala b/tests/init/neg/global-cycle9.scala new file mode 100644 index 000000000000..4c2421f190d8 --- /dev/null +++ b/tests/init/neg/global-cycle9.scala @@ -0,0 +1,20 @@ +object Names { // error + abstract class Name + + abstract class TermName extends Name: + def toTypeName: TypeName = ??? + + final class SimpleName(val start: Int, val length: Int) extends TermName + final class TypeName(val toTermName: TermName) extends Name + + class NameTable: + def add(index: Int, name: Name): Unit = () + add(0, EmptyTermName) + + var chrs: Array[Char] = new Array[Char](1024) + + val EmptyTermName: SimpleName = SimpleName(-1, 0) + val EmptyTypeName: TypeName = EmptyTermName.toTypeName + + val nameTable = NameTable() +} From b3b696f7a007732582d194de319225933eb42c1d Mon Sep 17 00:00:00 2001 From: Fengyun Liu Date: Sat, 14 Jan 2023 10:43:08 +0100 Subject: [PATCH 042/113] Fix error annotations for test cases --- tests/init/neg/global-cycle1.check | 8 ++++++-- tests/init/neg/global-cycle10.scala | 4 ++-- tests/init/neg/global-cycle11.scala | 10 +--------- tests/init/neg/global-cycle7.scala | 4 ++-- 4 files changed, 11 insertions(+), 15 deletions(-) diff --git a/tests/init/neg/global-cycle1.check b/tests/init/neg/global-cycle1.check index 9456f856b371..9c6febbb1171 100644 --- a/tests/init/neg/global-cycle1.check +++ b/tests/init/neg/global-cycle1.check @@ -2,9 +2,13 @@ 1 |object A { // error |^ |Cyclic initialization: object A -> object B -> object A. Calling trace: - |-> val b: Int = A.a [ global-cycle1.scala:6 ] - | ^ + |-> object A { // error [ global-cycle1.scala:1 ] + | ^ |-> val a: Int = B.b [ global-cycle1.scala:2 ] | ^ + |-> object B { [ global-cycle1.scala:5 ] + | ^ + |-> val b: Int = A.a [ global-cycle1.scala:6 ] + | ^ 2 | val a: Int = B.b 3 |} diff --git a/tests/init/neg/global-cycle10.scala b/tests/init/neg/global-cycle10.scala index 5ca76d1e70fd..9d6200cd884d 100644 --- a/tests/init/neg/global-cycle10.scala +++ b/tests/init/neg/global-cycle10.scala @@ -1,10 +1,10 @@ abstract class Base { + val msg: String = "hello" def foo(): Unit foo() } object O extends Base { // error - val msg: String = "hello" class Inner { println(msg) @@ -14,4 +14,4 @@ object O extends Base { // error } @main -def Test = O \ No newline at end of file +def Test = O diff --git a/tests/init/neg/global-cycle11.scala b/tests/init/neg/global-cycle11.scala index 303dcdb9c8f7..bbd33bd9b105 100644 --- a/tests/init/neg/global-cycle11.scala +++ b/tests/init/neg/global-cycle11.scala @@ -1,6 +1,6 @@ import scala.collection.mutable -object NameKinds { +object NameKinds { // error private val qualifiedNameKinds = mutable.HashMap[Int, QualifiedNameKind]() val QualifiedName: QualifiedNameKind = new QualifiedNameKind(2, ".") @@ -12,11 +12,3 @@ object NameKinds { qualifiedNameKinds(tag) = this } } - -object A { // error - val n: Int = B.m -} - -object B { - val m: Int = A.n -} diff --git a/tests/init/neg/global-cycle7.scala b/tests/init/neg/global-cycle7.scala index 32fdc4eb7b10..63adc342fd62 100644 --- a/tests/init/neg/global-cycle7.scala +++ b/tests/init/neg/global-cycle7.scala @@ -14,5 +14,5 @@ abstract class TokensCommon { object JavaTokens extends TokensCommon { final def maxToken: Int = DOUBLE - final val DOUBLE = 188 // error: but constant folding avoided the issue -} \ No newline at end of file + final val DOUBLE = 188 +} From 2bbab07db4acbe1e21ffc6a13e4323362c997470 Mon Sep 17 00:00:00 2001 From: Fengyun Liu Date: Sat, 14 Jan 2023 12:13:56 +0100 Subject: [PATCH 043/113] Disallow mutating other static objects --- .../tools/dotc/transform/init/Objects.scala | 39 +++++++++++++++---- tests/init/neg/global-irrelevance2.scala | 8 ++++ 2 files changed, 39 insertions(+), 8 deletions(-) create mode 100644 tests/init/neg/global-irrelevance2.scala diff --git a/compiler/src/dotty/tools/dotc/transform/init/Objects.scala b/compiler/src/dotty/tools/dotc/transform/init/Objects.scala index c65696141db7..83976fb476a3 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Objects.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Objects.scala @@ -249,11 +249,18 @@ object Objects: if target.isOneOf(Flags.Method) then if target.hasSource then - val cls = target.owner.enclosingClass.asClass - val ddef = target.defTree.asInstanceOf[DefDef] - extendTrace(ddef) { - eval(ddef.rhs, ref, cls, cacheResult = true) - } + println("ref = " + ref) + ref match + case obj: ObjectRef if obj.klass != State.currentObject && target.isOneOf(Flags.Mutable) => + errorMutateOtherStaticObject(State.currentObject, obj.klass) + Bottom + + case _ => + val cls = target.owner.enclosingClass.asClass + val ddef = target.defTree.asInstanceOf[DefDef] + extendTrace(ddef) { + eval(ddef.rhs, ref, cls, cacheResult = true) + } else Bottom else if target.exists then @@ -342,7 +349,7 @@ object Objects: ref match case obj: ObjectRef if State.currentObject != obj.klass => if target.isOneOf(Flags.Mutable) then - report.warning("Reading mutable state of " + obj.klass + " during initialization of " + State.currentObject + " is discouraged as it breaks initialization-time irrelevance", Trace.position) + report.warning("Reading mutable state of " + obj.klass + " during initialization of " + State.currentObject + " is discouraged as it breaks initialization-time irrelevance. Calling trace: " + Trace.show, Trace.position) Bottom else if ref.hasField(target) then @@ -589,9 +596,17 @@ object Objects: lhs match case Select(qual, _) => eval(qual, thisV, klass) - eval(rhs, thisV, klass) case id: Ident => - eval(rhs, thisV, klass) + id.tpe match + case TermRef(NoPrefix, _) => + case TermRef(prefix, _) => + extendTrace(id) { evalType(prefix, thisV, klass) } + + eval(rhs, thisV, klass) + if lhs.symbol.owner.isStaticObject && lhs.symbol.owner != State.currentObject then + withTrace(trace2) { errorMutateOtherStaticObject(State.currentObject, lhs.symbol.owner.asClass) } + end if + Bottom case closureDef(ddef) => Fun(ddef.rhs, thisV, klass) @@ -900,3 +915,11 @@ object Objects: else if cls.isAllOf(Flags.JavaInterface) then Bottom else evalType(tref.prefix, thisV, klass, elideObjectAccess = cls.isStatic) + + def errorMutateOtherStaticObject(currentObj: ClassSymbol, otherObj: ClassSymbol)(using Trace, Context) = + val msg = + s"Mutating ${otherObj.show} during initialization of ${currentObj.show}.\n" + + "Mutating other static objects during the initialization of one static object is discouraged. " + + "Calling trace:\n" + Trace.show + + report.warning(msg, Trace.position) diff --git a/tests/init/neg/global-irrelevance2.scala b/tests/init/neg/global-irrelevance2.scala new file mode 100644 index 000000000000..66b06677b689 --- /dev/null +++ b/tests/init/neg/global-irrelevance2.scala @@ -0,0 +1,8 @@ +object A: + var x = 6 + +class B(b: Int): + A.x = b * 2 // error + +object B: + new B(10) From b06adada21620a0b002de7c23daab3fe1c20a526 Mon Sep 17 00:00:00 2001 From: Fengyun Liu Date: Sat, 14 Jan 2023 12:15:07 +0100 Subject: [PATCH 044/113] Suppress debug printing --- compiler/src/dotty/tools/dotc/transform/init/Objects.scala | 1 - 1 file changed, 1 deletion(-) diff --git a/compiler/src/dotty/tools/dotc/transform/init/Objects.scala b/compiler/src/dotty/tools/dotc/transform/init/Objects.scala index 83976fb476a3..c66e928c6a72 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Objects.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Objects.scala @@ -249,7 +249,6 @@ object Objects: if target.isOneOf(Flags.Method) then if target.hasSource then - println("ref = " + ref) ref match case obj: ObjectRef if obj.klass != State.currentObject && target.isOneOf(Flags.Mutable) => errorMutateOtherStaticObject(State.currentObject, obj.klass) From 9800395bf2ec06715abc395790423998642d6046 Mon Sep 17 00:00:00 2001 From: Fengyun Liu Date: Sun, 29 Jan 2023 23:23:32 +0100 Subject: [PATCH 045/113] Fix rebase error --- .../src/dotty/tools/dotc/transform/init/Objects.scala | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/transform/init/Objects.scala b/compiler/src/dotty/tools/dotc/transform/init/Objects.scala index c66e928c6a72..a6777bf47a9f 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Objects.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Objects.scala @@ -179,10 +179,10 @@ object Objects: val config = Config(thisV, State.leakedInstances) super.get(config, expr).map(_.value) - def assume(thisV: Value, expr: Tree, cacheResult: Boolean)(fun: => Value)(using State.Data): Value = + def cachedEval(thisV: Value, expr: Tree, cacheResult: Boolean)(fun: Tree => Value)(using State.Data): Value = val config = Config(thisV, State.leakedInstances) - val result = super.assume(config, expr, cacheResult, default = Res(Bottom, State.leakedInstances)) { - Res(fun, State.leakedInstances) + val result = super.cachedEval(config, expr, cacheResult, default = Res(Bottom, State.leakedInstances)) { expr => + Res(fun(expr), State.leakedInstances) } result.value end Cache @@ -492,10 +492,7 @@ object Objects: * @param cacheResult It is used to reduce the size of the cache. */ def eval(expr: Tree, thisV: Value, klass: ClassSymbol, cacheResult: Boolean = false): Contextual[Value] = log("evaluating " + expr.show + ", this = " + thisV.show + " in " + klass.show, printer, (_: Value).show) { - cache.get(thisV, expr) match - case Some(value) => value - case None => - cache.assume(thisV, expr, cacheResult) { cases(expr, thisV, klass) } + cache.cachedEval(thisV, expr, cacheResult) { expr => cases(expr, thisV, klass) } } From 26e01adbfc2b81e569dab0bcc48ab023664da08a Mon Sep 17 00:00:00 2001 From: Fengyun Liu Date: Thu, 16 Feb 2023 23:30:55 +0100 Subject: [PATCH 046/113] Refine abstract domain --- .../tools/dotc/transform/init/Objects.scala | 227 +++++++++--------- 1 file changed, 114 insertions(+), 113 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/transform/init/Objects.scala b/compiler/src/dotty/tools/dotc/transform/init/Objects.scala index a6777bf47a9f..dfb89e2411c5 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Objects.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Objects.scala @@ -30,17 +30,30 @@ import scala.annotation.tailrec * case object A extends Foo(B) // A -> B * case object B extends Foo(A) // B -> A * - * In the code above, the initialization of object `A` depends on `B` and vice - * versa. There is no correct way to initialize the code above. The current - * checker issues a warning for the code above. + * In the code above, the initialization of object `A` depends on `B` and vice versa. There is no + * correct way to initialize the code above. The current checker issues a warning for the code + * above. * * At the high-level, the analysis has the following characteristics: * - * 1. It is inter-procedural and flow-insensitive. + * 1. It is inter-procedural and flow-sensitive. * - * 2. It is receiver-sensitive but not heap-sensitive nor parameter-sensitive. + * 2. It is object-sensitive by default and parameter-sensitive on-demand. * - * Fields and parameters are always abstracted by their types. + * 3. The check is modular in the sense that each object is checked separately and there is no + * whole-program analysis. However, the check is not modular in terms of project boundaries. + * + * 4. The check enforces the principle of "initialization-time irrelevance", which means that the + * time when an object is initialized should not change program semantics. For that purpose, it + * enforces the following rule: + * + * The initialization of a static object should not directly or indirectly read or write + * mutable state of another static object. + * + * 5. 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. * */ object Objects: @@ -87,29 +100,17 @@ object Objects: * * `tp.classSymbol` should be the concrete class of the value at runtime. */ - case class OfClass private(tp: Type, klass: ClassSymbol, outer: Value, ctor: Symbol, args: List[Value]) extends Ref: + case class OfClass private(klass: ClassSymbol, outer: Value, ctor: Symbol, args: List[Value]) extends Ref: def show(using Context) = "OfClass(" + klass.show + ", outer = " + outer + ", args = " + args.map(_.show) + ")" object OfClass: - def apply(tp: Type, klass: ClassSymbol, outer: Value, ctor: Symbol, args: List[Value])(using Context): OfClass = - val instance = new OfClass(tp, klass, outer, ctor, args) + def apply(klass: ClassSymbol, outer: Value, ctor: Symbol, args: List[Value])(using Context): OfClass = + val instance = new OfClass(klass, outer, ctor, args) instance.updateOuter(klass, outer) instance - /** - * Rerepsents values of a specific type - * - * `OfType` is just a short-cut referring to currently instantiated sub-types. - * - * Note: this value should never be an index in the cache. - */ - case class OfType(tp: Type) extends Value: - def show(using Context) = "OfType(" + tp.show + ")" - /** * Represents a lambda expression - * - * TODO: add concrete type of lambdas */ case class Fun(expr: Tree, thisV: Value, klass: ClassSymbol) extends Value: def show(using Context) = "Fun(" + expr.show + ", " + thisV.show + ", " + klass.show + ")" @@ -123,34 +124,33 @@ object Objects: assert(refs.forall(!_.isInstanceOf[RefSet])) def show(using Context) = refs.map(_.show).mkString("[", ",", "]") + /** A cold alias which should not be used during initialization. */ + case object Cold extends Value + val Bottom = RefSet(Nil) + /** Checking state */ object State: - /** Record leaked instantiated types and lambdas. - * - * For more fine-grained approximation, the values are distinguished by their types. - */ - class LeakedInstances(classes: Map[ClassSymbol, List[Type]], funs: Map[Type, List[Fun]]) - - object LeakedInstances: - val empty = LeakedInstances(Map.empty, Map.empty) - - /** - * Remembers the instantiated types during instantiation of a static object. - */ class Data: // objects under check private[State] val checkingObjects = new mutable.ListBuffer[ClassSymbol] private[State] val checkedObjects = new mutable.ArrayBuffer[ClassSymbol] private[State] val pendingTraces = new mutable.ListBuffer[Trace] - private[State] val leakedInstancesByObject = mutable.Map.empty[ClassSymbol, LeakedInstances] + // Use a mutable field to avoid thread through it in the program. + private[State] var heap = Heap.empty end Data - def currentObject(using data: Data): ClassSymbol = data.checkingObjects.last + def getHeap()(using data: Data): Heap.Data = data.heap + + def containsAddress(addr: Addr)(using data: Data): Value = Heap.contains(addr)(using data.heap) + + def readAddress(addr: Addr)(using data: Data): Value = Heap.read(addr)(using data.heap) - def leakedInstances(using data: Data): LeakedInstances = - data.leakedInstancesByObject.getOrElseUpdate(currentObject, LeakedInstances.empty) + def writeAddress(addr: Addr, value: Value)(using data: Data): Data = + data.heap = Heap.write(addr, value)(using data.heap) + + def currentObject(using data: Data): ClassSymbol = data.checkingObjects.last def checkCycle(clazz: ClassSymbol)(work: => Unit)(using data: Data, ctx: Context, pendingTrace: Trace) = val index = data.checkingObjects.indexOf(clazz) @@ -170,19 +170,51 @@ object Objects: end checkCycle end State + /** Environment for parameters */ + object Env: + opaque type Data = Map[Symbol, Value] + + val empty: Data = Map.empty + + def apply(x: Symbol)(using data: Data): Value = data(x) + def contains(x: Symbol)(using data: Data): Value = data.contains(x) + def of(map: Map[Symbol, Value]): Data = map + + /** Abstract heap for mutable fields + * + * To avoid threading through it in the code, we use a mutable field in `State.Data` to hold the + * information. + */ + object Heap: + case class Addr(ref: Ref, field: Symbol): + def show(using Context) = "Addr(" + ref.show + ", " + field.show + ")" + + opaque type Data = Map[Addr, Value] + + val empty: Data = Map.empty + + def contains(addr: Addr)(using data: Data): Value = data.contains(addr) + + def read(addr: Addr)(using data: Data): Value = data(addr) + + def write(addr: Addr, value: Value)(using data: Data): Data = + val current = data.getOrElse(addr, bottom) + data.updated(addr, value.join(current)) + + /** Cache used to terminate the check */ object Cache: - case class Config(thisV: Value, leakedInstances: State.LeakedInstances) - case class Res(value: Value, leakedInstances: State.LeakedInstances) + case class Config(thisV: Value, env: Env.Data, heap: Heap.Data) + case class Res(value: Value, heap: Heap.Data) class Data extends Cache[Config, Res]: - def get(thisV: Value, expr: Tree)(using State.Data): Option[Value] = - val config = Config(thisV, State.leakedInstances) + def get(thisV: Value, expr: Tree)(using State.Data, Env.Data): Option[Value] = + val config = Config(thisV, summonp[Env.Data], State.getHeap()) super.get(config, expr).map(_.value) - def cachedEval(thisV: Value, expr: Tree, cacheResult: Boolean)(fun: Tree => Value)(using State.Data): Value = - val config = Config(thisV, State.leakedInstances) - val result = super.cachedEval(config, expr, cacheResult, default = Res(Bottom, State.leakedInstances)) { expr => - Res(fun(expr), State.leakedInstances) + def cachedEval(thisV: Value, expr: Tree, cacheResult: Boolean)(fun: Tree => Value)(using State.Data, Env.Data): Value = + val config = Config(thisV, summonp[Env.Data], State.getHeap()) + val result = super.cachedEval(config, expr, cacheResult, default = Res(Bottom, State.getHeap())) { expr => + Res(fun(expr), State.getHeap()) } result.value end Cache @@ -198,6 +230,8 @@ object Objects: extension (a: Value) def join(b: Value): Value = (a, b) match + case (Cold, b) => Cold + case (a, Cold) => Cold case (Bottom, b) => b case (a, Bottom) => a case (RefSet(refs1), RefSet(refs2)) => RefSet(refs1 ++ refs2) @@ -205,37 +239,37 @@ object Objects: case (RefSet(refs), b) => RefSet(b :: refs) case (a, b) => RefSet(a :: b :: Nil) - def widenArg(using Context): Value = - a match - case Bottom => Bottom - case RefSet(refs) => refs.map(_.widenArg).join - case OfClass(tp, _, _: OfClass, _, _) => OfType(tp) - case _ => a + def widen(using Context): Value = + def widenInternal(tp: Type, fuel: Int): Value = + tp match + case Bottom => Bottom + case RefSet(refs) => refs.map(ref => widenInternal(ref, fuel)).join + case OfClass(klass, outer, args, init) => + if fuel == 0 then + Cold + else + val outer2 = widenInternal(outer, fuel - 1) + val args2 = args.map(arg => widenInternal(fuel - 1)) + OfClass(klass, outer2, args2, init) + case _ => a + + widenInternal(a, 3) extension (values: Seq[Value]) def join: Value = if values.isEmpty then Bottom else values.reduce { (v1, v2) => v1.join(v2) } - def widenArgs: Contextual[List[Value]] = values.map(_.widenArg).toList + def widen: Contextual[List[Value]] = values.map(_.widen).toList def call(value: Value, meth: Symbol, args: List[ArgInfo], receiver: Type, superType: Type, needResolve: Boolean = true): Contextual[Value] = log("call " + meth.show + ", args = " + args.map(_.value.show), printer, (_: Value).show) { - def checkArgs() = - // TODO: check aliasing of static objects - for - arg <- args - do - arg.value match - case ObjectRef(obj) => - report.warning("Aliasing object " + obj.show, Trace.position) - - case _ => - value match + case Cold => + report.warning("Using cold alias", Trace.position) + Bottom + case Bottom => Bottom case ref: Ref => - checkArgs() - val isLocal = !meth.owner.isClass val target = if !needResolve then @@ -276,26 +310,7 @@ object Objects: // See tests/init/pos/Type.scala Bottom - case OfType(tp) => - checkArgs() - - if meth.exists && meth.isEffectivelyFinal then - if meth.hasSource then - val isLocal = meth.owner.isClass - val ddef = meth.defTree.asInstanceOf[DefDef] - extendTrace(ddef) { - eval(ddef.rhs, value, meth.owner.enclosingClass.asClass, cacheResult = true) - } - else - Bottom - else - // TODO: approximate call with instantiated types - report.warning("Virtual method call ", Trace.position) - Bottom - case Fun(expr, thisV, klass) => - checkArgs() - // meth == NoSymbol for poly functions if meth.name.toString == "tupled" then value // a call like `fun.tupled` @@ -320,7 +335,7 @@ object Objects: if ctor.hasSource then val cls = ctor.owner.enclosingClass.asClass val ddef = ctor.defTree.asInstanceOf[DefDef] - val args2 = args.map(_.value).widenArgs + val args2 = args.map(_.value).widen addParamsAsFields(args2, ref, ddef) if ctor.isPrimaryConstructor then val tpl = cls.defTree.asInstanceOf[TypeDef].rhs.asInstanceOf[Template] @@ -339,6 +354,10 @@ object Objects: def select(thisV: Value, field: Symbol, receiver: Type, needResolve: Boolean = true): Contextual[Value] = log("select " + field.show + ", this = " + thisV, printer, (_: Value).show) { thisV match + case Cold => + report.warning("Using cold alias", Trace.position) + Bottom + case ref: Ref => val target = if needResolve then resolve(ref.klass, field) else field if target.is(Flags.Lazy) then @@ -373,17 +392,6 @@ object Objects: // See tests/init/pos/Type.scala Bottom - case OfType(tp) => - if field.isEffectivelyFinal then - if field.hasSource then - val vdef = field.defTree.asInstanceOf[ValDef] - eval(vdef.rhs, thisV, field.owner.enclosingClass.asClass, cacheResult = true) - else - Bottom - else - val fieldType = tp.memberInfo(field) - OfType(fieldType) - case fun: Fun => report.error("[Internal error] unexpected tree in selecting a function, fun = " + fun.expr.show + Trace.show, fun.expr) Bottom @@ -403,19 +411,14 @@ object Objects: report.error("[Internal error] unexpected tree in instantiating a function, fun = " + body.show + Trace.show, Trace.position) Bottom - case value: (Bottom.type | ObjectRef | OfClass | OfType) => - // widen the outer to finitize the domain - val outerWidened = outer.widenArg - val argsWidened = args.map(_.value).widenArgs + case value: (Bottom.type | ObjectRef | OfClass | Cold) => + // The outer can be a bottom value for top-level classes. - // TODO: type arguments - val tp = value match - case v: ObjectRef => v.klass.typeRef.memberInfo(klass) - case v: OfClass => v.tp.memberInfo(klass) - case v: OfType => v.tp.memberInfo(klass) - case Bottom => klass.typeRef + // widen the outer to finitize the domain + val outerWidened = outer.widen + val argsWidened = args.map(_.value).widen - val instance = OfClass(tp, klass, outerWidened, ctor, argsWidened) + val instance = OfClass(klass, outerWidened, ctor, argsWidened) val argInfos2 = args.zip(argsWidened).map { (argInfo, v) => argInfo.copy(value = v) } callConstructor(instance, ctor, argInfos2) instance @@ -693,11 +696,10 @@ object Objects: Bottom case tmref: TermRef if tmref.prefix == NoPrefix => - // - params and var definitions are abstract by its type - // - evaluate the rhs of the local definition for val definitions val sym = tmref.symbol if sym.isOneOf(Flags.Param | Flags.Mutable) then - OfType(sym.info) + // TODO: handle environment parameters and mutation differently + Cold else if sym.is(Flags.Package) then Bottom else if sym.hasSource then @@ -705,7 +707,7 @@ object Objects: eval(rhs, thisV, klass) else // pattern-bound variables - OfType(sym.info) + Cold case tmref: TermRef => val sym = tmref.symbol @@ -880,6 +882,7 @@ object Objects: else thisV match case Bottom => Bottom + case Cold => Cold case ref: Ref => val outerCls = klass.owner.lexicallyEnclosingClass.asClass if !ref.hasOuter(klass) then @@ -893,8 +896,6 @@ object Objects: case fun: Fun => report.error("[Internal error] unexpected thisV = " + thisV + ", target = " + target.show + ", klass = " + klass.show + Trace.show, Trace.position) Bottom - case OfType(tp) => - OfType(target.appliedRef) } /** Compute the outer value that correspond to `tref.prefix` From a5ed5269b044c7d427da23b9ab3970b57dada3a9 Mon Sep 17 00:00:00 2001 From: Fengyun Liu Date: Fri, 17 Feb 2023 00:16:18 +0100 Subject: [PATCH 047/113] Handle assignment --- .../tools/dotc/transform/init/Objects.scala | 104 +++++++++++------- 1 file changed, 67 insertions(+), 37 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/transform/init/Objects.scala b/compiler/src/dotty/tools/dotc/transform/init/Objects.scala index dfb89e2411c5..1d7e95bdcf3e 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Objects.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Objects.scala @@ -71,6 +71,8 @@ object Objects: private val fields: mutable.Map[Symbol, Value] = mutable.Map.empty private val outers: mutable.Map[ClassSymbol, Value] = mutable.Map.empty + def owner: ClassSymbol + def klass: ClassSymbol def fieldValue(sym: Symbol): Value = fields(sym) @@ -93,6 +95,8 @@ object Objects: /** A reference to a static object */ case class ObjectRef(klass: ClassSymbol) extends Ref: + val owner = klass + def show(using Context) = "ObjectRef(" + klass.show + ")" /** @@ -100,12 +104,12 @@ object Objects: * * `tp.classSymbol` should be the concrete class of the value at runtime. */ - case class OfClass private(klass: ClassSymbol, outer: Value, ctor: Symbol, args: List[Value]) extends Ref: + case class OfClass private(klass: ClassSymbol, outer: Value, ctor: Symbol, args: List[Value], owner: ClassSymbol) extends Ref: def show(using Context) = "OfClass(" + klass.show + ", outer = " + outer + ", args = " + args.map(_.show) + ")" object OfClass: - def apply(klass: ClassSymbol, outer: Value, ctor: Symbol, args: List[Value])(using Context): OfClass = - val instance = new OfClass(klass, outer, ctor, args) + def apply(klass: ClassSymbol, outer: Value, ctor: Symbol, args: List[Value])(using State.Data, Context): OfClass = + val instance = new OfClass(klass, outer, ctor, args, State.currentObject) instance.updateOuter(klass, outer) instance @@ -244,13 +248,13 @@ object Objects: tp match case Bottom => Bottom case RefSet(refs) => refs.map(ref => widenInternal(ref, fuel)).join - case OfClass(klass, outer, args, init) => + case OfClass(klass, outer, args, init, owner) => if fuel == 0 then Cold else val outer2 = widenInternal(outer, fuel - 1) val args2 = args.map(arg => widenInternal(fuel - 1)) - OfClass(klass, outer2, args2, init) + OfClass(klass, outer2, args2, init, owner) case _ => a widenInternal(a, 3) @@ -364,25 +368,19 @@ object Objects: val rhs = target.defTree.asInstanceOf[ValDef].rhs eval(rhs, ref, target.owner.asClass, cacheResult = true) else if target.exists then - ref match - case obj: ObjectRef if State.currentObject != obj.klass => - if target.isOneOf(Flags.Mutable) then - report.warning("Reading mutable state of " + obj.klass + " during initialization of " + State.currentObject + " is discouraged as it breaks initialization-time irrelevance. Calling trace: " + Trace.show, Trace.position) - Bottom - else - if ref.hasField(target) then - // TODO: check immutability - ref.fieldValue(target) - else - // initialization error, reported by the initialization checker - Bottom - - case _ => - if ref.hasField(target) then - ref.fieldValue(target) + if target.isOneOf(Flags.Mutable) then + if ref.owner == State.currentObject then + State.readAddress(Heap.Addr(thisV, vdef.symbol), res) else - // initialization error, reported by the initialization checker + report.warning("Reading mutable state of " + ref.owner.show + " during initialization of " + State.currentObject + " is discouraged as it breaks initialization-time irrelevance. Calling trace: " + Trace.show, Trace.position) Bottom + end if + else if ref.hasField(target) then + ref.fieldValue(target) + else + // initialization error, reported by the initialization checker + Bottom + else if ref.klass.isSubClass(receiver.widenSingleton.classSymbol) then report.error("[Internal error] Unexpected resolution failure: ref.klass = " + ref.klass.show + ", field = " + field.show + Trace.show, Trace.position) @@ -404,6 +402,30 @@ object Objects: refs.map(ref => select(ref, field, receiver)).join } + def assign(receiver: Value, field: Symbol, rhs: Value): Contextual[Value] = log("Assign" + field.show + " of " + receiver.show + ", rhs = " + rhs.show, printer, (_: Value).show) { + receiver match + case Fun(body, thisV, klass) => + report.error("[Internal error] unexpected tree in assignment, fun = " + body.show + Trace.show, Trace.position) + Bottom + + case Cold => + report.warning("Assigning to cold aliases is forbidden", Trace.position) + Bottom + + case Bottom => + Bottom + + case RefSet(refs) => + refs.map(ref => assign(ref, field, value)).join + + case ref: Ref => + if receiver.owner != State.currentObject then + withTrace(trace2) { errorMutateOtherStaticObject(State.currentObject, receiver.owner) } + else + State.writeAddress(Heap.Addr(ref, field), rhs) + Bottom + } + def instantiate(outer: Value, klass: ClassSymbol, ctor: Symbol, args: List[ArgInfo]): Contextual[Value] = log("instantiating " + klass.show + ", outer = " + outer + ", args = " + args.map(_.value.show), printer, (_: Value).show) { outer match @@ -592,20 +614,19 @@ object Objects: eval(arg, thisV, klass) case Assign(lhs, rhs) => - lhs match - case Select(qual, _) => - eval(qual, thisV, klass) - case id: Ident => - id.tpe match - case TermRef(NoPrefix, _) => - case TermRef(prefix, _) => - extendTrace(id) { evalType(prefix, thisV, klass) } - - eval(rhs, thisV, klass) - if lhs.symbol.owner.isStaticObject && lhs.symbol.owner != State.currentObject then - withTrace(trace2) { errorMutateOtherStaticObject(State.currentObject, lhs.symbol.owner.asClass) } - end if - Bottom + val receiver = + lhs match + case Select(qual, _) => + eval(qual, thisV, klass) + case id: Ident => + id.tpe match + case TermRef(NoPrefix, _) => + thisV + case TermRef(prefix, _) => + extendTrace(id) { evalType(prefix, thisV, klass) } + + val value = eval(rhs, thisV, klass) + assign(receiver, lhs.symbol, value) case closureDef(ddef) => Fun(ddef.rhs, thisV, klass) @@ -751,6 +772,12 @@ object Objects: } argInfos.toList + /** Initialize part of an abstract object in `klass` of the inheritance chain + * + * @param tpl The class body to be evaluated. + * @param thisV The value of the current object to be initialized. + * @param klass The class to which the template belongs. + */ def init(tpl: Template, thisV: Ref, klass: ClassSymbol): Contextual[Value] = log("init " + klass.show, printer, (_: Value).show) { val paramsMap = tpl.constr.termParamss.flatten.map { vdef => vdef.name -> thisV.fieldValue(vdef.symbol) @@ -848,7 +875,10 @@ object Objects: tpl.body.foreach { case vdef : ValDef if !vdef.symbol.is(Flags.Lazy) && !vdef.rhs.isEmpty => val res = eval(vdef.rhs, thisV, klass) - thisV.updateField(vdef.symbol, res) + if vdef.symbol.is(Flags.Mutable) then + State.writeAddress(Heap.Addr(thisV, vdef.symbol), res) + else + thisV.updateField(vdef.symbol, res) case _: MemberDef => From 8c42c6d6208ac619d776b845ae478f32c946c5dd Mon Sep 17 00:00:00 2001 From: Fengyun Liu Date: Fri, 17 Feb 2023 00:19:23 +0100 Subject: [PATCH 048/113] Fix reading local mutable variable --- compiler/src/dotty/tools/dotc/transform/init/Objects.scala | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/compiler/src/dotty/tools/dotc/transform/init/Objects.scala b/compiler/src/dotty/tools/dotc/transform/init/Objects.scala index 1d7e95bdcf3e..ccc17095ecd3 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Objects.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Objects.scala @@ -718,7 +718,9 @@ object Objects: case tmref: TermRef if tmref.prefix == NoPrefix => val sym = tmref.symbol - if sym.isOneOf(Flags.Param | Flags.Mutable) then + if sym.is(Flags.Mutable) then + State.readAddress(Heap.Address(thisV, sym)) + else if sym.is(Flags.Param) then // TODO: handle environment parameters and mutation differently Cold else if sym.is(Flags.Package) then From 3938f8ef9463fdc8fecc07aff67c38b03f1ea23a Mon Sep 17 00:00:00 2001 From: Fengyun Liu Date: Fri, 17 Feb 2023 01:23:58 +0100 Subject: [PATCH 049/113] Fix compilation --- .../tools/dotc/transform/init/Objects.scala | 86 +++++++++++-------- 1 file changed, 50 insertions(+), 36 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/transform/init/Objects.scala b/compiler/src/dotty/tools/dotc/transform/init/Objects.scala index ccc17095ecd3..11954dab0c0f 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Objects.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Objects.scala @@ -108,8 +108,8 @@ object Objects: def show(using Context) = "OfClass(" + klass.show + ", outer = " + outer + ", args = " + args.map(_.show) + ")" object OfClass: - def apply(klass: ClassSymbol, outer: Value, ctor: Symbol, args: List[Value])(using State.Data, Context): OfClass = - val instance = new OfClass(klass, outer, ctor, args, State.currentObject) + def apply(klass: ClassSymbol, outer: Value, ctor: Symbol, args: List[Value], owner: ClassSymbol)(using Context): OfClass = + val instance = new OfClass(klass, outer, ctor, args, owner) instance.updateOuter(klass, outer) instance @@ -129,7 +129,8 @@ object Objects: def show(using Context) = refs.map(_.show).mkString("[", ",", "]") /** A cold alias which should not be used during initialization. */ - case object Cold extends Value + case object Cold extends Value: + def show(using Context) = "Cold" val Bottom = RefSet(Nil) @@ -146,13 +147,8 @@ object Objects: end Data def getHeap()(using data: Data): Heap.Data = data.heap - - def containsAddress(addr: Addr)(using data: Data): Value = Heap.contains(addr)(using data.heap) - - def readAddress(addr: Addr)(using data: Data): Value = Heap.read(addr)(using data.heap) - - def writeAddress(addr: Addr, value: Value)(using data: Data): Data = - data.heap = Heap.write(addr, value)(using data.heap) + def setHeap(heap: Heap.Data)(using data: Data) = + data.heap = heap def currentObject(using data: Data): ClassSymbol = data.checkingObjects.last @@ -181,7 +177,7 @@ object Objects: val empty: Data = Map.empty def apply(x: Symbol)(using data: Data): Value = data(x) - def contains(x: Symbol)(using data: Data): Value = data.contains(x) + def contains(x: Symbol)(using data: Data): Boolean = data.contains(x) def of(map: Map[Symbol, Value]): Data = map /** Abstract heap for mutable fields @@ -190,20 +186,26 @@ object Objects: * information. */ object Heap: - case class Addr(ref: Ref, field: Symbol): + private case class Addr(ref: Ref, field: Symbol): def show(using Context) = "Addr(" + ref.show + ", " + field.show + ")" opaque type Data = Map[Addr, Value] val empty: Data = Map.empty - def contains(addr: Addr)(using data: Data): Value = data.contains(addr) + def contains(ref: Ref, field: Symbol)(using state: State.Data): Boolean = + val data: Data = State.getHeap() + data.contains(Addr(ref, field)) - def read(addr: Addr)(using data: Data): Value = data(addr) + def read(ref: Ref, field: Symbol)(using state: State.Data): Value = + val data: Data = State.getHeap() + data(Addr(ref, field)) - def write(addr: Addr, value: Value)(using data: Data): Data = - val current = data.getOrElse(addr, bottom) - data.updated(addr, value.join(current)) + def write(ref: Ref, field: Symbol, value: Value)(using state: State.Data): Unit = + val addr = Addr(ref, field) + val data: Data = State.getHeap() + val current = data.getOrElse(addr, Bottom) + State.setHeap(data.updated(addr, value.join(current))) /** Cache used to terminate the check */ object Cache: @@ -212,11 +214,11 @@ object Objects: class Data extends Cache[Config, Res]: def get(thisV: Value, expr: Tree)(using State.Data, Env.Data): Option[Value] = - val config = Config(thisV, summonp[Env.Data], State.getHeap()) + val config = Config(thisV, summon[Env.Data], State.getHeap()) super.get(config, expr).map(_.value) def cachedEval(thisV: Value, expr: Tree, cacheResult: Boolean)(fun: Tree => Value)(using State.Data, Env.Data): Value = - val config = Config(thisV, summonp[Env.Data], State.getHeap()) + val config = Config(thisV, summon[Env.Data], State.getHeap()) val result = super.cachedEval(config, expr, cacheResult, default = Res(Bottom, State.getHeap())) { expr => Res(fun(expr), State.getHeap()) } @@ -225,7 +227,7 @@ object Objects: inline def cache(using c: Cache.Data): Cache.Data = c - type Contextual[T] = (Context, State.Data, Cache.Data, Trace) ?=> T + type Contextual[T] = (Context, State.Data, Env.Data, Cache.Data, Trace) ?=> T // --------------------------- domain operations ----------------------------- @@ -244,17 +246,23 @@ object Objects: case (a, b) => RefSet(a :: b :: Nil) def widen(using Context): Value = - def widenInternal(tp: Type, fuel: Int): Value = - tp match + def widenInternal(value: Value, fuel: Int): Value = + value match case Bottom => Bottom - case RefSet(refs) => refs.map(ref => widenInternal(ref, fuel)).join - case OfClass(klass, outer, args, init, owner) => + + case RefSet(refs) => + refs.map(ref => widenInternal(ref, fuel)).join + + case Fun(expr, thisV, klass) => + Fun(expr, widenInternal(thisV, fuel), klass) + + case ref @ OfClass(klass, outer, init, args, owner) => if fuel == 0 then Cold else val outer2 = widenInternal(outer, fuel - 1) - val args2 = args.map(arg => widenInternal(fuel - 1)) - OfClass(klass, outer2, args2, init, owner) + val args2 = args.map(arg => widenInternal(arg, fuel - 1)) + OfClass(klass, outer2, init, args2, owner) case _ => a widenInternal(a, 3) @@ -370,7 +378,7 @@ object Objects: else if target.exists then if target.isOneOf(Flags.Mutable) then if ref.owner == State.currentObject then - State.readAddress(Heap.Addr(thisV, vdef.symbol), res) + Heap.read(ref, field) else report.warning("Reading mutable state of " + ref.owner.show + " during initialization of " + State.currentObject + " is discouraged as it breaks initialization-time irrelevance. Calling trace: " + Trace.show, Trace.position) Bottom @@ -416,13 +424,14 @@ object Objects: Bottom case RefSet(refs) => - refs.map(ref => assign(ref, field, value)).join + refs.map(ref => assign(ref, field, rhs)).join case ref: Ref => - if receiver.owner != State.currentObject then - withTrace(trace2) { errorMutateOtherStaticObject(State.currentObject, receiver.owner) } + if ref.owner != State.currentObject then + errorMutateOtherStaticObject(State.currentObject, ref.owner) + Bottom else - State.writeAddress(Heap.Addr(ref, field), rhs) + Heap.write(ref, field, rhs) Bottom } @@ -433,14 +442,14 @@ object Objects: report.error("[Internal error] unexpected tree in instantiating a function, fun = " + body.show + Trace.show, Trace.position) Bottom - case value: (Bottom.type | ObjectRef | OfClass | Cold) => + case value: (Bottom.type | ObjectRef | OfClass | Cold.type) => // The outer can be a bottom value for top-level classes. // widen the outer to finitize the domain val outerWidened = outer.widen val argsWidened = args.map(_.value).widen - val instance = OfClass(klass, outerWidened, ctor, argsWidened) + val instance = OfClass(klass, outerWidened, ctor, argsWidened, State.currentObject) val argInfos2 = args.zip(argsWidened).map { (argInfo, v) => argInfo.copy(value = v) } callConstructor(instance, ctor, argInfos2) instance @@ -464,6 +473,7 @@ object Objects: count += 1 given Trace = Trace.empty.add(classSym.defTree) + given env: Env.Data = Env.empty log("Iteration " + count) { init(tpl, ObjectRef(classSym), classSym) @@ -626,7 +636,7 @@ object Objects: extendTrace(id) { evalType(prefix, thisV, klass) } val value = eval(rhs, thisV, klass) - assign(receiver, lhs.symbol, value) + withTrace(trace2) { assign(receiver, lhs.symbol, value) } case closureDef(ddef) => Fun(ddef.rhs, thisV, klass) @@ -719,7 +729,11 @@ object Objects: case tmref: TermRef if tmref.prefix == NoPrefix => val sym = tmref.symbol if sym.is(Flags.Mutable) then - State.readAddress(Heap.Address(thisV, sym)) + val ownerClass = sym.enclosingClass + val ownerValue = resolveThis(ownerClass.asClass, thisV, klass) + // local mutable fields are associated with the object + select(ownerValue, sym, ownerClass.thisType, needResolve = false) + else if sym.is(Flags.Param) then // TODO: handle environment parameters and mutation differently Cold @@ -878,7 +892,7 @@ object Objects: case vdef : ValDef if !vdef.symbol.is(Flags.Lazy) && !vdef.rhs.isEmpty => val res = eval(vdef.rhs, thisV, klass) if vdef.symbol.is(Flags.Mutable) then - State.writeAddress(Heap.Addr(thisV, vdef.symbol), res) + Heap.write(thisV, vdef.symbol, res) else thisV.updateField(vdef.symbol, res) From 708a4dd0f3529f7d80ce7a586ba7e278046a5479 Mon Sep 17 00:00:00 2001 From: Fengyun Liu Date: Mon, 20 Feb 2023 05:37:44 +0100 Subject: [PATCH 050/113] Add documentation --- compiler/src/dotty/tools/dotc/transform/init/Objects.scala | 3 +++ 1 file changed, 3 insertions(+) diff --git a/compiler/src/dotty/tools/dotc/transform/init/Objects.scala b/compiler/src/dotty/tools/dotc/transform/init/Objects.scala index 11954dab0c0f..7203b1cbed16 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Objects.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Objects.scala @@ -50,6 +50,9 @@ import scala.annotation.tailrec * The initialization of a static object should not directly or indirectly read or write * mutable state of another static object. * + * This principle not only put initialization of static objects on a solid foundation, but also + * avoids whole-program analysis. + * * 5. 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 From cf49ce49c2535e82f099a52db2b391fa92eabce2 Mon Sep 17 00:00:00 2001 From: Fengyun Liu Date: Mon, 20 Feb 2023 06:00:52 +0100 Subject: [PATCH 051/113] Use referential equality for heap to improve performance --- .../tools/dotc/transform/init/Objects.scala | 29 +++++++++++++++---- 1 file changed, 23 insertions(+), 6 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/transform/init/Objects.scala b/compiler/src/dotty/tools/dotc/transform/init/Objects.scala index 7203b1cbed16..1ab6d55e943d 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Objects.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Objects.scala @@ -192,23 +192,40 @@ object Objects: private case class Addr(ref: Ref, field: Symbol): def show(using Context) = "Addr(" + ref.show + ", " + field.show + ")" - opaque type Data = Map[Addr, Value] + opaque type Data = ImmutableMapWithRefEquality + + /** Wrap the map in a class such that equality of two maps is defined as referential equality. + * + * This is a performance optimization. + */ + private class ImmutableMapWithRefEquality(val map: Map[Addr, Value]): + private[Heap] def updated(addr: Addr, value: Value): Data = + map.get(addr) match + case None => + new Data(map.updated(addr, value)) - val empty: Data = Map.empty + case Some(current) => + val value2 = value.join(current) + if value2 != current then + new Data(map.updated(addr, value2)) + else + this + + val empty: Data = new Data(Map.empty) def contains(ref: Ref, field: Symbol)(using state: State.Data): Boolean = val data: Data = State.getHeap() - data.contains(Addr(ref, field)) + data.map.contains(Addr(ref, field)) def read(ref: Ref, field: Symbol)(using state: State.Data): Value = val data: Data = State.getHeap() - data(Addr(ref, field)) + data.map(Addr(ref, field)) def write(ref: Ref, field: Symbol, value: Value)(using state: State.Data): Unit = val addr = Addr(ref, field) val data: Data = State.getHeap() - val current = data.getOrElse(addr, Bottom) - State.setHeap(data.updated(addr, value.join(current))) + val data2 = data.updated(addr, value) + State.setHeap(data2) /** Cache used to terminate the check */ object Cache: From a282ce94ff91224aaa011a247dda22480eda0124 Mon Sep 17 00:00:00 2001 From: Fengyun Liu Date: Mon, 20 Feb 2023 06:08:48 +0100 Subject: [PATCH 052/113] Make widen parametric to enable user-defined widening --- .../tools/dotc/transform/init/Objects.scala | 41 +++++++++---------- 1 file changed, 20 insertions(+), 21 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/transform/init/Objects.scala b/compiler/src/dotty/tools/dotc/transform/init/Objects.scala index 1ab6d55e943d..0ddeb6f419e8 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Objects.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Objects.scala @@ -265,32 +265,31 @@ object Objects: case (RefSet(refs), b) => RefSet(b :: refs) case (a, b) => RefSet(a :: b :: Nil) - def widen(using Context): Value = - def widenInternal(value: Value, fuel: Int): Value = - value match - case Bottom => Bottom + def widen(height: Int)(using Context): Value = + a match + case Bottom => Bottom - case RefSet(refs) => - refs.map(ref => widenInternal(ref, fuel)).join + case RefSet(refs) => + refs.map(ref => ref.widen(height)).join - case Fun(expr, thisV, klass) => - Fun(expr, widenInternal(thisV, fuel), klass) + case Fun(expr, thisV, klass) => + if height == 0 then Cold + else Fun(expr, thisV.widen(height), klass) - case ref @ OfClass(klass, outer, init, args, owner) => - if fuel == 0 then - Cold - else - val outer2 = widenInternal(outer, fuel - 1) - val args2 = args.map(arg => widenInternal(arg, fuel - 1)) - OfClass(klass, outer2, init, args2, owner) - case _ => a + case ref @ OfClass(klass, outer, init, args, owner) => + if height == 0 then + Cold + else + val outer2 = outer.widen(height - 1) + val args2 = args.map(arg => arg.widen(height - 1)) + OfClass(klass, outer2, init, args2, owner) + case _ => a - widenInternal(a, 3) extension (values: Seq[Value]) def join: Value = if values.isEmpty then Bottom else values.reduce { (v1, v2) => v1.join(v2) } - def widen: Contextual[List[Value]] = values.map(_.widen).toList + def widen(height: Int): Contextual[List[Value]] = values.map(_.widen(height)).toList def call(value: Value, meth: Symbol, args: List[ArgInfo], receiver: Type, superType: Type, needResolve: Boolean = true): Contextual[Value] = log("call " + meth.show + ", args = " + args.map(_.value.show), printer, (_: Value).show) { value match @@ -367,7 +366,7 @@ object Objects: if ctor.hasSource then val cls = ctor.owner.enclosingClass.asClass val ddef = ctor.defTree.asInstanceOf[DefDef] - val args2 = args.map(_.value).widen + val args2 = args.map(_.value).widen(0) addParamsAsFields(args2, ref, ddef) if ctor.isPrimaryConstructor then val tpl = cls.defTree.asInstanceOf[TypeDef].rhs.asInstanceOf[Template] @@ -466,8 +465,8 @@ object Objects: // The outer can be a bottom value for top-level classes. // widen the outer to finitize the domain - val outerWidened = outer.widen - val argsWidened = args.map(_.value).widen + val outerWidened = outer.widen(1) + val argsWidened = args.map(_.value).widen(0) val instance = OfClass(klass, outerWidened, ctor, argsWidened, State.currentObject) val argInfos2 = args.zip(argsWidened).map { (argInfo, v) => argInfo.copy(value = v) } From b4b0d078d7981fb4b8df230b88c4ea8a0450b3dc Mon Sep 17 00:00:00 2001 From: Fengyun Liu Date: Mon, 20 Feb 2023 06:38:23 +0100 Subject: [PATCH 053/113] Introduce init.widen --- library/src/scala/annotation/init.scala | 28 +++++++++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 library/src/scala/annotation/init.scala diff --git a/library/src/scala/annotation/init.scala b/library/src/scala/annotation/init.scala new file mode 100644 index 000000000000..e9bfb8e2441d --- /dev/null +++ b/library/src/scala/annotation/init.scala @@ -0,0 +1,28 @@ +package scala.annotation + +/** Annotations to control the behavior of the compiler check for safe initialization of static obects. + * + * Programmers usually do not need to use any annotations. They are intended for complex initialization + * code in static objects. + */ +object init: + + /** Widen the abstract value of the argument so that its height is below the specified height. + * + * It can be used to mark method or constructor arguments, as the following example shows: + * + * class A(x: Int): + * def squre(): Int = x*x + * + * object B: + * val a = build(new A(10): @init.widen(1)) // <-- usage + * + * def build(o: A) = new A(o.square()) // calling methods on parameter + * + * By default, method and constructor arguments are cold aliases, which means they may not be actively + * used --- calling methods or accessing fields on them are forbidden. By using `@widen(1)` to the + * method argument, we tell the compiler that the abstract value for the argument may have a maximum + * height of 1. As a result, the parameter is not a cold alias any more, so we may call methods + * on the parameter. + */ + final class widen(hight: Int) extends StaticAnnotation From c221da14c454a67cbf853ad35921d33c88b9fc2b Mon Sep 17 00:00:00 2001 From: Fengyun Liu Date: Mon, 20 Feb 2023 07:40:35 +0100 Subject: [PATCH 054/113] Respect environment --- .../dotty/tools/dotc/core/Definitions.scala | 2 + .../tools/dotc/transform/init/Objects.scala | 59 ++++++++++++------- library/src/scala/annotation/init.scala | 20 +++++-- tests/init/neg/global-cycle8.scala | 20 +++++++ 4 files changed, 75 insertions(+), 26 deletions(-) create mode 100644 tests/init/neg/global-cycle8.scala diff --git a/compiler/src/dotty/tools/dotc/core/Definitions.scala b/compiler/src/dotty/tools/dotc/core/Definitions.scala index 3d11ad80733e..42972c8bcc4b 100644 --- a/compiler/src/dotty/tools/dotc/core/Definitions.scala +++ b/compiler/src/dotty/tools/dotc/core/Definitions.scala @@ -997,6 +997,8 @@ class Definitions { @tu lazy val DeprecatedOverridingAnnot: ClassSymbol = requiredClass("scala.deprecatedOverriding") @tu lazy val ImplicitAmbiguousAnnot: ClassSymbol = requiredClass("scala.annotation.implicitAmbiguous") @tu lazy val ImplicitNotFoundAnnot: ClassSymbol = requiredClass("scala.annotation.implicitNotFound") + @tu lazy val InitWidenAnnot: ClassSymbol = requiredClass("scala.annotation.init.widen") + @tu lazy val InitExposeAnnot: ClassSymbol = requiredClass("scala.annotation.init.expose") @tu lazy val InlineParamAnnot: ClassSymbol = requiredClass("scala.annotation.internal.InlineParam") @tu lazy val ErasedParamAnnot: ClassSymbol = requiredClass("scala.annotation.internal.ErasedParam") @tu lazy val InvariantBetweenAnnot: ClassSymbol = requiredClass("scala.annotation.internal.InvariantBetween") diff --git a/compiler/src/dotty/tools/dotc/transform/init/Objects.scala b/compiler/src/dotty/tools/dotc/transform/init/Objects.scala index 0ddeb6f419e8..44bdc92c88a0 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Objects.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Objects.scala @@ -36,14 +36,7 @@ import scala.annotation.tailrec * * At the high-level, the analysis has the following characteristics: * - * 1. It is inter-procedural and flow-sensitive. - * - * 2. It is object-sensitive by default and parameter-sensitive on-demand. - * - * 3. The check is modular in the sense that each object is checked separately and there is no - * whole-program analysis. However, the check is not modular in terms of project boundaries. - * - * 4. The check enforces the principle of "initialization-time irrelevance", which means that the + * 1. The check enforces the principle of "initialization-time irrelevance", which means that the * time when an object is initialized should not change program semantics. For that purpose, it * enforces the following rule: * @@ -53,11 +46,18 @@ import scala.annotation.tailrec * This principle not only put initialization of static objects on a solid foundation, but also * avoids whole-program analysis. * - * 5. The design is based on the concept of "cold aliasing" --- a cold alias may not be actively + * 2. The design is based on the concept of "cold aliasing" --- a cold alias may not be actively * used during initialization, i.e., it's forbidden to call methods or access fields of a cold * alias. Method arguments are cold aliases by default unless specified to be sensitive. Method * parameters captured in lambdas or inner classes are always cold aliases. * + * 3. It is inter-procedural and flow-sensitive. + * + * 4. It is object-sensitive by default and parameter-sensitive on-demand. + * + * 5. The check is modular in the sense that each object is checked separately and there is no + * whole-program analysis. However, the check is not modular in terms of project boundaries. + * */ object Objects: @@ -180,8 +180,15 @@ object Objects: val empty: Data = Map.empty def apply(x: Symbol)(using data: Data): Value = data(x) + + def get(x: Symbol)(using data: Data): Option[Value] = data.get(x) + def contains(x: Symbol)(using data: Data): Boolean = data.contains(x) - def of(map: Map[Symbol, Value]): Data = map + + def of(ddef: DefDef, args: List[Value])(using Context): Data = + val params = ddef.termParamss.flatten.map(_.symbol) + assert(args.size == params.size, "arguments = " + args.size + ", params = " + params.size) + params.zip(args).toMap /** Abstract heap for mutable fields * @@ -322,6 +329,7 @@ object Objects: case _ => val cls = target.owner.enclosingClass.asClass val ddef = target.defTree.asInstanceOf[DefDef] + given Env.Data = Env.of(ddef, args.map(_.value)) extendTrace(ddef) { eval(ddef.rhs, ref, cls, cacheResult = true) } @@ -366,8 +374,8 @@ object Objects: if ctor.hasSource then val cls = ctor.owner.enclosingClass.asClass val ddef = ctor.defTree.asInstanceOf[DefDef] - val args2 = args.map(_.value).widen(0) - addParamsAsFields(args2, ref, ddef) + val argValues = args.map(_.value) + addParamsAsFields(argValues, ref, ddef) if ctor.isPrimaryConstructor then val tpl = cls.defTree.asInstanceOf[TypeDef].rhs.asInstanceOf[Template] extendTrace(cls.defTree) { eval(tpl, ref, cls, cacheResult = true) } @@ -464,13 +472,11 @@ object Objects: case value: (Bottom.type | ObjectRef | OfClass | Cold.type) => // The outer can be a bottom value for top-level classes. - // widen the outer to finitize the domain + // Widen the outer to finitize the domain. Arguments already widened in `evalArgs`. val outerWidened = outer.widen(1) - val argsWidened = args.map(_.value).widen(0) - val instance = OfClass(klass, outerWidened, ctor, argsWidened, State.currentObject) - val argInfos2 = args.zip(argsWidened).map { (argInfo, v) => argInfo.copy(value = v) } - callConstructor(instance, ctor, argInfos2) + val instance = OfClass(klass, outerWidened, ctor, args.map(_.value), State.currentObject) + callConstructor(instance, ctor, args) instance case RefSet(refs) => @@ -634,7 +640,7 @@ object Objects: Bottom case Typed(expr, tpt) => - if (tpt.tpe.hasAnnotation(defn.UncheckedAnnot)) + if tpt.tpe.hasAnnotation(defn.UncheckedAnnot) then Bottom else eval(expr, thisV, klass) @@ -754,13 +760,17 @@ object Objects: select(ownerValue, sym, ownerClass.thisType, needResolve = false) else if sym.is(Flags.Param) then - // TODO: handle environment parameters and mutation differently - Cold + Env.get(sym) match + case Some(v) => v + case None => Cold + else if sym.is(Flags.Package) then Bottom + else if sym.hasSource then val rhs = sym.defTree.asInstanceOf[ValDef].rhs eval(rhs, thisV, klass) + else // pattern-bound variables Cold @@ -793,7 +803,7 @@ object Objects: throw new Exception("unexpected type: " + tp) } - /** Evaluate arguments of methods */ + /** Evaluate arguments of methods and constructors */ def evalArgs(args: List[Arg], thisV: Value, klass: ClassSymbol): Contextual[List[ArgInfo]] = val argInfos = new mutable.ArrayBuffer[ArgInfo] args.foreach { arg => @@ -803,6 +813,13 @@ object Objects: else eval(arg.tree, thisV, klass) + // TODO: handle @widen(n) + val widened = + if arg.tree.tpe.hasAnnotation(defn.InitExposeAnnot) then + res.widen(1) + else + Cold + argInfos += TraceValue(res, trace.add(arg.tree)) } argInfos.toList diff --git a/library/src/scala/annotation/init.scala b/library/src/scala/annotation/init.scala index e9bfb8e2441d..075f7a1dbeaa 100644 --- a/library/src/scala/annotation/init.scala +++ b/library/src/scala/annotation/init.scala @@ -8,6 +8,15 @@ package scala.annotation object init: /** Widen the abstract value of the argument so that its height is below the specified height. + * + * This is an advanced version of `@init.expose`, where we tell the compiler the annotated method or + * constructor argument are not cold aliases and its abstract value may have a maximum height of the + * specified value. + */ + final class widen(hight: Int) extends StaticAnnotation + + /** Expose the method argument or constructor argument so that it may be actively used during the + * initialization of static objects. * * It can be used to mark method or constructor arguments, as the following example shows: * @@ -20,9 +29,10 @@ object init: * def build(o: A) = new A(o.square()) // calling methods on parameter * * By default, method and constructor arguments are cold aliases, which means they may not be actively - * used --- calling methods or accessing fields on them are forbidden. By using `@widen(1)` to the - * method argument, we tell the compiler that the abstract value for the argument may have a maximum - * height of 1. As a result, the parameter is not a cold alias any more, so we may call methods - * on the parameter. + * used --- calling methods or accessing fields on them are forbidden. By using `@expose` to the + * method argument, we tell the compiler that the argument is not a cold alias. As a result, we may + * call methods on the parameter. + * + * It is semantically equivalent to `@init.widen(1)`. */ - final class widen(hight: Int) extends StaticAnnotation + final class expose extends StaticAnnotation diff --git a/tests/init/neg/global-cycle8.scala b/tests/init/neg/global-cycle8.scala new file mode 100644 index 000000000000..29d5af83a52a --- /dev/null +++ b/tests/init/neg/global-cycle8.scala @@ -0,0 +1,20 @@ +class A { + def foo() = println(O.n) +} + +class B { + val a = new A +} + +object O { + val n: Int = 10 + println(P.m) +} + +object P { + val m = Q.bar(new B: @annotation.init.expose) +} + +object Q { + def bar(b: B) = b.a.foo() // error +} From 2f75e5efcc5dbe6c47153ecd80fbeb512afab201 Mon Sep 17 00:00:00 2001 From: Fengyun Liu Date: Mon, 20 Feb 2023 08:07:53 +0100 Subject: [PATCH 055/113] Fix error annotation --- community-build/community-projects/scala-parallel-collections | 2 +- tests/init/neg/global-cycle8.scala | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/community-build/community-projects/scala-parallel-collections b/community-build/community-projects/scala-parallel-collections index 7d0e41ae4d09..a6bd648bb188 160000 --- a/community-build/community-projects/scala-parallel-collections +++ b/community-build/community-projects/scala-parallel-collections @@ -1 +1 @@ -Subproject commit 7d0e41ae4d09e1ddf063651e377921ec493fc5bf +Subproject commit a6bd648bb188a65ab36be07e956e52fe25f64d67 diff --git a/tests/init/neg/global-cycle8.scala b/tests/init/neg/global-cycle8.scala index 29d5af83a52a..2090b53d9894 100644 --- a/tests/init/neg/global-cycle8.scala +++ b/tests/init/neg/global-cycle8.scala @@ -6,7 +6,7 @@ class B { val a = new A } -object O { +object O { // error val n: Int = 10 println(P.m) } @@ -16,5 +16,5 @@ object P { } object Q { - def bar(b: B) = b.a.foo() // error + def bar(b: B) = b.a.foo() } From 27ff0eb576d92a2e663de753e4d97c6bd72772b0 Mon Sep 17 00:00:00 2001 From: Fengyun Liu Date: Wed, 22 Feb 2023 07:48:43 +0100 Subject: [PATCH 056/113] Ignore primitive types in assignment --- .../tools/dotc/transform/init/Objects.scala | 41 ++++++++++--------- 1 file changed, 21 insertions(+), 20 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/transform/init/Objects.scala b/compiler/src/dotty/tools/dotc/transform/init/Objects.scala index 44bdc92c88a0..cef233cec17d 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Objects.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Objects.scala @@ -437,29 +437,30 @@ object Objects: refs.map(ref => select(ref, field, receiver)).join } - def assign(receiver: Value, field: Symbol, rhs: Value): Contextual[Value] = log("Assign" + field.show + " of " + receiver.show + ", rhs = " + rhs.show, printer, (_: Value).show) { - receiver match - case Fun(body, thisV, klass) => - report.error("[Internal error] unexpected tree in assignment, fun = " + body.show + Trace.show, Trace.position) - Bottom + def assign(receiver: Value, field: Symbol, rhs: Value, rhsTyp: Type): Contextual[Value] = log("Assign" + field.show + " of " + receiver.show + ", rhs = " + rhs.show, printer, (_: Value).show) { + // Ignore primitive types + if !rhsTyp.widenDealias.typeSymbol.isPrimitiveValueClass then - case Cold => - report.warning("Assigning to cold aliases is forbidden", Trace.position) - Bottom + receiver match + case Fun(body, thisV, klass) => + report.error("[Internal error] unexpected tree in assignment, fun = " + body.show + Trace.show, Trace.position) - case Bottom => - Bottom + case Cold => + report.warning("Assigning to cold aliases is forbidden", Trace.position) - case RefSet(refs) => - refs.map(ref => assign(ref, field, rhs)).join + case Bottom => - case ref: Ref => - if ref.owner != State.currentObject then - errorMutateOtherStaticObject(State.currentObject, ref.owner) - Bottom - else - Heap.write(ref, field, rhs) - Bottom + case RefSet(refs) => + refs.foreach(ref => assign(ref, field, rhs, rhsTyp)) + + case ref: Ref => + if ref.owner != State.currentObject then + errorMutateOtherStaticObject(State.currentObject, ref.owner) + else + Heap.write(ref, field, rhs) + end if + + Bottom } 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) { @@ -661,7 +662,7 @@ object Objects: extendTrace(id) { evalType(prefix, thisV, klass) } val value = eval(rhs, thisV, klass) - withTrace(trace2) { assign(receiver, lhs.symbol, value) } + withTrace(trace2) { assign(receiver, lhs.symbol, value, rhs.tpe) } case closureDef(ddef) => Fun(ddef.rhs, thisV, klass) From d69fc4fca46b7843f81947965949de2f7a0e4615 Mon Sep 17 00:00:00 2001 From: Fengyun Liu Date: Wed, 22 Feb 2023 08:06:10 +0100 Subject: [PATCH 057/113] Fix primitive value optimization --- compiler/src/dotty/tools/dotc/transform/init/Objects.scala | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/compiler/src/dotty/tools/dotc/transform/init/Objects.scala b/compiler/src/dotty/tools/dotc/transform/init/Objects.scala index cef233cec17d..8674c51c234a 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Objects.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Objects.scala @@ -226,7 +226,8 @@ object Objects: def read(ref: Ref, field: Symbol)(using state: State.Data): Value = val data: Data = State.getHeap() - data.map(Addr(ref, field)) + // Primitive values are not in the heap and initialization errors are reported by the initialization checker. + data.map.getOrElse(Addr(ref, field), Bottom) def write(ref: Ref, field: Symbol, value: Value)(using state: State.Data): Unit = val addr = Addr(ref, field) From 7a1e7f34b0414ec4286cc76eb9f5ad500e4ffd02 Mon Sep 17 00:00:00 2001 From: Fengyun Liu Date: Wed, 22 Feb 2023 08:37:51 +0100 Subject: [PATCH 058/113] Use ListSet to avoid explosion due to duplicate entries It can be demonstrated by tests/init/neg/inherit-non-hot.scala. --- .../dotty/tools/dotc/transform/init/Objects.scala | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/transform/init/Objects.scala b/compiler/src/dotty/tools/dotc/transform/init/Objects.scala index 8674c51c234a..ca0c14942fcd 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Objects.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Objects.scala @@ -19,6 +19,7 @@ import Errors.* import Trace.* import Util.* +import scala.collection.immutable.ListSet import scala.collection.mutable import scala.annotation.tailrec @@ -127,7 +128,7 @@ object Objects: * * It comes from `if` expressions. */ - case class RefSet(refs: List[Value]) extends Value: + case class RefSet(refs: ListSet[Value]) extends Value: assert(refs.forall(!_.isInstanceOf[RefSet])) def show(using Context) = refs.map(_.show).mkString("[", ",", "]") @@ -135,7 +136,7 @@ object Objects: case object Cold extends Value: def show(using Context) = "Cold" - val Bottom = RefSet(Nil) + val Bottom = RefSet(ListSet.empty) /** Checking state */ object State: @@ -269,9 +270,9 @@ object Objects: case (Bottom, b) => b case (a, Bottom) => a case (RefSet(refs1), RefSet(refs2)) => RefSet(refs1 ++ refs2) - case (a, RefSet(refs)) => RefSet(a :: refs) - case (RefSet(refs), b) => RefSet(b :: refs) - case (a, b) => RefSet(a :: b :: Nil) + case (a, RefSet(refs)) => RefSet(refs + a) + case (RefSet(refs), b) => RefSet(refs + b) + case (a, b) => RefSet(ListSet(a, b)) def widen(height: Int)(using Context): Value = a match @@ -294,7 +295,7 @@ object Objects: case _ => a - extension (values: Seq[Value]) + 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 From e96376560651eda3196a128a06debad2bb46edde Mon Sep 17 00:00:00 2001 From: Fengyun Liu Date: Wed, 22 Feb 2023 21:20:45 +0100 Subject: [PATCH 059/113] Better handling of locals --- .../tools/dotc/transform/init/Objects.scala | 132 ++++++++++++------ 1 file changed, 87 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 ca0c14942fcd..acd869513b20 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Objects.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Objects.scala @@ -176,20 +176,32 @@ object Objects: /** Environment for parameters */ object Env: - opaque type Data = Map[Symbol, Value] + case class Data(private[Env] val params: Map[Symbol, Value]): + private[Env] val locals: mutable.Map[Symbol, Value] = mutable.Map.empty - val empty: Data = Map.empty + private[Env] def get(x: Symbol)(using Context): Option[Value] = + if x.is(Flags.Param) then params.get(x) + else locals.get(x) - def apply(x: Symbol)(using data: Data): Value = data(x) + private[Env] def contains(x: Symbol): Boolean = params.contains(x) || locals.contains(x) - def get(x: Symbol)(using data: Data): Option[Value] = data.get(x) + + def empty: Data = new Data(Map.empty) + + def apply(x: Symbol)(using data: Data, ctx: Context): Value = data.get(x).get + + def get(x: Symbol)(using data: Data, ctx: Context): Option[Value] = data.get(x) + + def setLocalVal(x: Symbol, value: Value)(using data: Data, ctx: Context): Unit = + assert(!x.isOneOf(Flags.Param | Flags.Mutable), "Only local immutable variable allowed") + data.locals(x) = value def contains(x: Symbol)(using data: Data): Boolean = data.contains(x) def of(ddef: DefDef, args: List[Value])(using Context): Data = val params = ddef.termParamss.flatten.map(_.symbol) assert(args.size == params.size, "arguments = " + args.size + ", params = " + params.size) - params.zip(args).toMap + new Data(params.zip(args).toMap) /** Abstract heap for mutable fields * @@ -197,8 +209,13 @@ object Objects: * information. */ object Heap: - private case class Addr(ref: Ref, field: Symbol): - def show(using Context) = "Addr(" + ref.show + ", " + field.show + ")" + private abstract class Addr + + /** The address for mutable fields of objects. */ + private case class FieldAddr(ref: Ref, field: Symbol) extends Addr + + /** The address for mutable local variables . */ + private case class LocalVarAddr(ref: Ref, env: Env.Data, sym: Symbol) extends Addr opaque type Data = ImmutableMapWithRefEquality @@ -223,15 +240,28 @@ object Objects: def contains(ref: Ref, field: Symbol)(using state: State.Data): Boolean = val data: Data = State.getHeap() - data.map.contains(Addr(ref, field)) + data.map.contains(FieldAddr(ref, field)) def read(ref: Ref, field: Symbol)(using state: State.Data): Value = val data: Data = State.getHeap() - // Primitive values are not in the heap and initialization errors are reported by the initialization checker. - data.map.getOrElse(Addr(ref, field), Bottom) + data.map(FieldAddr(ref, field)) def write(ref: Ref, field: Symbol, value: Value)(using state: State.Data): Unit = - val addr = Addr(ref, field) + val addr = FieldAddr(ref, field) + val data: Data = State.getHeap() + val data2 = data.updated(addr, value) + State.setHeap(data2) + + def containsLocalVar(ref: Ref, env: Env.Data, sym: Symbol)(using state: State.Data): Boolean = + val data: Data = State.getHeap() + data.map.contains(LocalVarAddr(ref, env, sym)) + + def readLocalVar(ref: Ref, env: Env.Data, sym: Symbol)(using state: State.Data): Value = + val data: Data = State.getHeap() + data.map(LocalVarAddr(ref, env, sym)) + + def writeLocalVar(ref: Ref, env: Env.Data, sym: Symbol, value: Value)(using state: State.Data): Unit = + val addr = LocalVarAddr(ref, env, sym) val data: Data = State.getHeap() val data2 = data.updated(addr, value) State.setHeap(data2) @@ -440,27 +470,25 @@ object Objects: } def assign(receiver: Value, field: Symbol, rhs: Value, rhsTyp: Type): Contextual[Value] = log("Assign" + field.show + " of " + receiver.show + ", rhs = " + rhs.show, printer, (_: Value).show) { - // Ignore primitive types - if !rhsTyp.widenDealias.typeSymbol.isPrimitiveValueClass then - - receiver match - case Fun(body, thisV, klass) => - report.error("[Internal error] unexpected tree in assignment, fun = " + body.show + Trace.show, Trace.position) + receiver match + case Fun(body, thisV, klass) => + report.error("[Internal error] unexpected tree in assignment, fun = " + body.show + Trace.show, Trace.position) - case Cold => - report.warning("Assigning to cold aliases is forbidden", Trace.position) + case Cold => + report.warning("Assigning to cold aliases is forbidden", Trace.position) - case Bottom => + case Bottom => - case RefSet(refs) => - refs.foreach(ref => assign(ref, field, rhs, rhsTyp)) + case RefSet(refs) => + refs.foreach(ref => assign(ref, field, rhs, rhsTyp)) - case ref: Ref => - if ref.owner != State.currentObject then - errorMutateOtherStaticObject(State.currentObject, ref.owner) - else - Heap.write(ref, field, rhs) - end if + case ref: Ref => + println("ref = " + ref.show + ", ref.owner = " + ref.owner.show + ", current = " + State.currentObject.show) + if ref.owner != State.currentObject then + errorMutateOtherStaticObject(State.currentObject, ref.owner) + else + Heap.write(ref, field, rhs) + end match Bottom } @@ -652,6 +680,7 @@ object Objects: eval(arg, thisV, klass) case Assign(lhs, rhs) => + var isLocal = false val receiver = lhs match case Select(qual, _) => @@ -659,12 +688,18 @@ object Objects: case id: Ident => id.tpe match case TermRef(NoPrefix, _) => + isLocal = true thisV case TermRef(prefix, _) => extendTrace(id) { evalType(prefix, thisV, klass) } val value = eval(rhs, thisV, klass) - withTrace(trace2) { assign(receiver, lhs.symbol, value, rhs.tpe) } + + if isLocal then + Heap.writeLocalVar(receiver.asInstanceOf[Ref], summon[Env.Data], lhs.symbol, value) + Bottom + else + withTrace(trace2) { assign(receiver, lhs.symbol, value, rhs.tpe) } case closureDef(ddef) => Fun(ddef.rhs, thisV, klass) @@ -716,7 +751,18 @@ object Objects: case vdef : ValDef => // local val definition - eval(vdef.rhs, thisV, klass) + val rhs = eval(vdef.rhs, thisV, klass) + val sym = vdef.symbol + if vdef.symbol.is(Flags.Mutable) then + val ref = thisV.asInstanceOf[Ref] + val env = summon[Env.Data] + // Ignore writing to outer locals, will be abstracted by Cold in read. + if Heap.containsLocalVar(ref, env, sym) then + Heap.writeLocalVar(ref, env, sym, rhs) + else + Env.setLocalVal(vdef.symbol, rhs) + + Bottom case ddef : DefDef => // local method @@ -756,26 +802,22 @@ object Objects: case tmref: TermRef if tmref.prefix == NoPrefix => val sym = tmref.symbol - if sym.is(Flags.Mutable) then - val ownerClass = sym.enclosingClass - val ownerValue = resolveThis(ownerClass.asClass, thisV, klass) - // local mutable fields are associated with the object - select(ownerValue, sym, ownerClass.thisType, needResolve = false) - - else if sym.is(Flags.Param) then - Env.get(sym) match - case Some(v) => v - case None => Cold + val valueOpt = Env.get(sym) + if valueOpt.nonEmpty then + valueOpt.get + + else if sym.is(Flags.Mutable) then + val ref = thisV.asInstanceOf[Ref] + val env = summon[Env.Data] + if Heap.containsLocalVar(ref, env, sym) then + Heap.readLocalVar(ref, env, sym) + else + Cold else if sym.is(Flags.Package) then Bottom - else if sym.hasSource then - val rhs = sym.defTree.asInstanceOf[ValDef].rhs - eval(rhs, thisV, klass) - else - // pattern-bound variables Cold case tmref: TermRef => From 13f804f8b38a0e484e41fc96529a27db30975538 Mon Sep 17 00:00:00 2001 From: Fengyun Liu Date: Wed, 22 Feb 2023 21:19:38 +0100 Subject: [PATCH 060/113] Only report non-trivial cycles It is necessary for the following code to pass the check: enum Color(val x: Int): case Violet extends Color(Green.x + 1) case Green extends Color(3) --- compiler/src/dotty/tools/dotc/transform/init/Objects.scala | 5 ++--- tests/init/neg/i12544.scala | 2 +- tests/init/{neg => pos}/global-cycle10.scala | 0 tests/init/{neg => pos}/global-cycle11.scala | 0 tests/init/{neg => pos}/global-cycle12.scala | 0 tests/init/{neg => pos}/global-cycle2.scala | 0 tests/init/{neg => pos}/global-cycle3.scala | 0 tests/init/{neg => pos}/global-cycle4.scala | 0 tests/init/{neg => pos}/global-cycle9.scala | 0 9 files changed, 3 insertions(+), 4 deletions(-) rename tests/init/{neg => pos}/global-cycle10.scala (100%) rename tests/init/{neg => pos}/global-cycle11.scala (100%) rename tests/init/{neg => pos}/global-cycle12.scala (100%) rename tests/init/{neg => pos}/global-cycle2.scala (100%) rename tests/init/{neg => pos}/global-cycle3.scala (100%) rename tests/init/{neg => pos}/global-cycle4.scala (100%) rename tests/init/{neg => pos}/global-cycle9.scala (100%) diff --git a/compiler/src/dotty/tools/dotc/transform/init/Objects.scala b/compiler/src/dotty/tools/dotc/transform/init/Objects.scala index acd869513b20..8e7d78942ecb 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Objects.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Objects.scala @@ -159,12 +159,12 @@ object Objects: def checkCycle(clazz: ClassSymbol)(work: => Unit)(using data: Data, ctx: Context, pendingTrace: Trace) = val index = data.checkingObjects.indexOf(clazz) - if index != -1 then + if index != -1 && data.checkingObjects.size > 1 then val joinedTrace = data.pendingTraces.slice(index + 1, data.checkingObjects.size).foldLeft(pendingTrace) { (a, acc) => acc ++ a } val callTrace = Trace.buildStacktrace(joinedTrace, "Calling trace:\n") val cycle = data.checkingObjects.slice(index, data.checkingObjects.size) report.warning("Cyclic initialization: " + cycle.map(_.show).mkString(" -> ") + " -> " + clazz.show + ". " + callTrace, clazz.defTree) - else if data.checkedObjects.indexOf(clazz) == -1 then + else if index == -1 && data.checkedObjects.indexOf(clazz) == -1 then data.pendingTraces += pendingTrace data.checkingObjects += clazz work @@ -483,7 +483,6 @@ object Objects: refs.foreach(ref => assign(ref, field, rhs, rhsTyp)) case ref: Ref => - println("ref = " + ref.show + ", ref.owner = " + ref.owner.show + ", current = " + State.currentObject.show) if ref.owner != State.currentObject then errorMutateOtherStaticObject(State.currentObject, ref.owner) else diff --git a/tests/init/neg/i12544.scala b/tests/init/neg/i12544.scala index 2692c27134e0..41e032d35c9e 100644 --- a/tests/init/neg/i12544.scala +++ b/tests/init/neg/i12544.scala @@ -5,7 +5,7 @@ enum Enum: def g(b: Enum.B): Int = b.foo() object Enum: - object nested: + object nested: // error val a: Enum = Case val b: Enum = f(nested.a) diff --git a/tests/init/neg/global-cycle10.scala b/tests/init/pos/global-cycle10.scala similarity index 100% rename from tests/init/neg/global-cycle10.scala rename to tests/init/pos/global-cycle10.scala diff --git a/tests/init/neg/global-cycle11.scala b/tests/init/pos/global-cycle11.scala similarity index 100% rename from tests/init/neg/global-cycle11.scala rename to tests/init/pos/global-cycle11.scala diff --git a/tests/init/neg/global-cycle12.scala b/tests/init/pos/global-cycle12.scala similarity index 100% rename from tests/init/neg/global-cycle12.scala rename to tests/init/pos/global-cycle12.scala diff --git a/tests/init/neg/global-cycle2.scala b/tests/init/pos/global-cycle2.scala similarity index 100% rename from tests/init/neg/global-cycle2.scala rename to tests/init/pos/global-cycle2.scala diff --git a/tests/init/neg/global-cycle3.scala b/tests/init/pos/global-cycle3.scala similarity index 100% rename from tests/init/neg/global-cycle3.scala rename to tests/init/pos/global-cycle3.scala diff --git a/tests/init/neg/global-cycle4.scala b/tests/init/pos/global-cycle4.scala similarity index 100% rename from tests/init/neg/global-cycle4.scala rename to tests/init/pos/global-cycle4.scala diff --git a/tests/init/neg/global-cycle9.scala b/tests/init/pos/global-cycle9.scala similarity index 100% rename from tests/init/neg/global-cycle9.scala rename to tests/init/pos/global-cycle9.scala From 5f81ee65f56cd3e39fd43ef081bb9ba8bb66f793 Mon Sep 17 00:00:00 2001 From: Fengyun Liu Date: Wed, 22 Feb 2023 21:19:49 +0100 Subject: [PATCH 061/113] Reclassify test The test now has a cyclic object initialization error. --- tests/init/{pos/i12544.scala => neg/i12544b.scala} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename tests/init/{pos/i12544.scala => neg/i12544b.scala} (80%) diff --git a/tests/init/pos/i12544.scala b/tests/init/neg/i12544b.scala similarity index 80% rename from tests/init/pos/i12544.scala rename to tests/init/neg/i12544b.scala index e5e3c9a2ade2..eaa6130e0894 100644 --- a/tests/init/pos/i12544.scala +++ b/tests/init/neg/i12544b.scala @@ -2,7 +2,7 @@ enum Enum: case Case object Enum: - object nested: + object nested: // error val a: Enum = Case val b: Enum = f(nested.a) From eacbd1b0b7ebd3be3b47e716f0135d4466979d20 Mon Sep 17 00:00:00 2001 From: Fengyun Liu Date: Wed, 22 Feb 2023 21:19:53 +0100 Subject: [PATCH 062/113] Fix access local var --- .../tools/dotc/transform/init/Objects.scala | 5 ++--- tests/init/pos/global-local-var.scala | 16 ++++++++++++++++ 2 files changed, 18 insertions(+), 3 deletions(-) create mode 100644 tests/init/pos/global-local-var.scala diff --git a/compiler/src/dotty/tools/dotc/transform/init/Objects.scala b/compiler/src/dotty/tools/dotc/transform/init/Objects.scala index 8e7d78942ecb..c7e41bc499d4 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Objects.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Objects.scala @@ -695,6 +695,7 @@ object Objects: val value = eval(rhs, thisV, klass) if isLocal then + // TODO: the local var might be from outer environment. Heap.writeLocalVar(receiver.asInstanceOf[Ref], summon[Env.Data], lhs.symbol, value) Bottom else @@ -755,9 +756,7 @@ object Objects: if vdef.symbol.is(Flags.Mutable) then val ref = thisV.asInstanceOf[Ref] val env = summon[Env.Data] - // Ignore writing to outer locals, will be abstracted by Cold in read. - if Heap.containsLocalVar(ref, env, sym) then - Heap.writeLocalVar(ref, env, sym, rhs) + Heap.writeLocalVar(ref, env, sym, rhs) else Env.setLocalVal(vdef.symbol, rhs) diff --git a/tests/init/pos/global-local-var.scala b/tests/init/pos/global-local-var.scala new file mode 100644 index 000000000000..57c180342c1f --- /dev/null +++ b/tests/init/pos/global-local-var.scala @@ -0,0 +1,16 @@ +class A(x: Int) { + def foo(): Int = { + val to = x + var sum = 0 + var i = 0 + while i < to do + sum += i + i += 1 + + B.a + 10 + sum + } +} + +object B { + val a: Int = A(4).foo() +} From cf925b2fc2677f566046bf68a2f387772d1cf326 Mon Sep 17 00:00:00 2001 From: Fengyun Liu Date: Wed, 22 Feb 2023 21:19:56 +0100 Subject: [PATCH 063/113] Refine error message: don't show code for whole object --- compiler/src/dotty/tools/dotc/transform/init/Objects.scala | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/transform/init/Objects.scala b/compiler/src/dotty/tools/dotc/transform/init/Objects.scala index c7e41bc499d4..a10c73d4e43d 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Objects.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Objects.scala @@ -163,7 +163,8 @@ object Objects: val joinedTrace = data.pendingTraces.slice(index + 1, data.checkingObjects.size).foldLeft(pendingTrace) { (a, acc) => acc ++ a } val callTrace = Trace.buildStacktrace(joinedTrace, "Calling trace:\n") val cycle = data.checkingObjects.slice(index, data.checkingObjects.size) - report.warning("Cyclic initialization: " + cycle.map(_.show).mkString(" -> ") + " -> " + clazz.show + ". " + callTrace, clazz.defTree) + val pos = clazz.defTree.asInstanceOf[TypeDef].rhs.asInstanceOf[Template].constr + report.warning("Cyclic initialization: " + cycle.map(_.show).mkString(" -> ") + " -> " + clazz.show + ". " + callTrace, pos) else if index == -1 && data.checkedObjects.indexOf(clazz) == -1 then data.pendingTraces += pendingTrace data.checkingObjects += clazz @@ -527,7 +528,7 @@ object Objects: def iterate()(using Context): Unit = count += 1 - given Trace = Trace.empty.add(classSym.defTree) + given Trace = Trace.empty.add(tpl.constr) given env: Env.Data = Env.empty log("Iteration " + count) { From 2a73df241cca42bd50f81a609afa9de246a40b66 Mon Sep 17 00:00:00 2001 From: Fengyun Liu Date: Thu, 23 Feb 2023 08:30:54 +0100 Subject: [PATCH 064/113] Handle local variables properly With the environment in values and values in environment, the domain is still finite because the abstract value `OfClass` does not exceed a constant height due to widening. --- .../tools/dotc/transform/init/Objects.scala | 216 ++++++++++++------ 1 file changed, 144 insertions(+), 72 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/transform/init/Objects.scala b/compiler/src/dotty/tools/dotc/transform/init/Objects.scala index a10c73d4e43d..34782ad7b926 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Objects.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Objects.scala @@ -69,7 +69,7 @@ object Objects: /** - * A reference caches the current value. + * A reference caches the values for outers and immutable fields. */ sealed abstract class Ref extends Value: private val fields: mutable.Map[Symbol, Value] = mutable.Map.empty @@ -106,21 +106,20 @@ object Objects: /** * Rerepsents values that are instances of the specified class * - * `tp.classSymbol` should be the concrete class of the value at runtime. */ - case class OfClass private(klass: ClassSymbol, outer: Value, ctor: Symbol, args: List[Value], owner: ClassSymbol) extends Ref: + case class OfClass private(klass: ClassSymbol, outer: Value, ctor: Symbol, args: List[Value], env: Env.Data, owner: ClassSymbol) extends Ref: def show(using Context) = "OfClass(" + klass.show + ", outer = " + outer + ", args = " + args.map(_.show) + ")" object OfClass: - def apply(klass: ClassSymbol, outer: Value, ctor: Symbol, args: List[Value], owner: ClassSymbol)(using Context): OfClass = - val instance = new OfClass(klass, outer, ctor, args, owner) + def apply(klass: ClassSymbol, outer: Value, ctor: Symbol, args: List[Value], env: Env.Data, owner: ClassSymbol)(using Context): OfClass = + val instance = new OfClass(klass, outer, ctor, args, env, owner) instance.updateOuter(klass, outer) instance /** * Represents a lambda expression */ - case class Fun(expr: Tree, thisV: Value, klass: ClassSymbol) extends Value: + case class Fun(expr: Tree, thisV: Value, klass: ClassSymbol, env: Env.Data) extends Value: def show(using Context) = "Fun(" + expr.show + ", " + thisV.show + ", " + klass.show + ")" /** @@ -177,32 +176,93 @@ object Objects: /** Environment for parameters */ object Env: - case class Data(private[Env] val params: Map[Symbol, Value]): + abstract class Data: + private[Env] def get(x: Symbol)(using Context): Option[Value] + private[Env] def contains(x: Symbol): Boolean + + def exists: Boolean + def widen(height: Int)(using Context): Data + + /** Local environments can be deeply nested, therefore we need `outer`. */ + private case class LocalEnv(private[Env] val params: Map[Symbol, Value], owner: Symbol, outer: Data) extends Data: private[Env] val locals: mutable.Map[Symbol, Value] = mutable.Map.empty private[Env] def get(x: Symbol)(using Context): Option[Value] = if x.is(Flags.Param) then params.get(x) else locals.get(x) - private[Env] def contains(x: Symbol): Boolean = params.contains(x) || locals.contains(x) + private[Env] def contains(x: Symbol): Boolean = + params.contains(x) || locals.contains(x) + + val exists: Boolean = true + + def widen(height: Int)(using Context): Data = + new LocalEnv(params.map(_ -> _.widen(height)), owner, outer.widen(height)) + end LocalEnv + object NoEnv extends Data: + private[Env] def get(x: Symbol)(using Context): Option[Value] = + throw new RuntimeException("Invalid usage of non-existent env") + + private[Env] def contains(x: Symbol): Boolean = + throw new RuntimeException("Invalid usage of non-existent env") - def empty: Data = new Data(Map.empty) + val exists: Boolean = false + + def widen(height: Int)(using Context): Data = this + end NoEnv + + /** An empty environment can be used for non-method environments, e.g., field initializers. + */ + def emptyEnv(owner: Symbol): Data = new LocalEnv(Map.empty, owner, NoEnv) def apply(x: Symbol)(using data: Data, ctx: Context): Value = data.get(x).get def get(x: Symbol)(using data: Data, ctx: Context): Option[Value] = data.get(x) - def setLocalVal(x: Symbol, value: Value)(using data: Data, ctx: Context): Unit = - assert(!x.isOneOf(Flags.Param | Flags.Mutable), "Only local immutable variable allowed") - data.locals(x) = value - def contains(x: Symbol)(using data: Data): Boolean = data.contains(x) - def of(ddef: DefDef, args: List[Value])(using Context): Data = + def of(ddef: DefDef, args: List[Value], outer: Data)(using Context): Data = val params = ddef.termParamss.flatten.map(_.symbol) - assert(args.size == params.size, "arguments = " + args.size + ", params = " + params.size) - new Data(params.zip(args).toMap) + assert(args.size == params.size && (ddef.symbol.owner.isClass ^ outer.exists), "arguments = " + args.size + ", params = " + params.size) + new LocalEnv(params.zip(args).toMap, ddef.symbol, outer) + + def setLocalVal(x: Symbol, value: Value)(using data: Data, ctx: Context): Unit = + assert(!x.isOneOf(Flags.Param | Flags.Mutable), "Only local immutable variable allowed") + data match + case localEnv: LocalEnv => + assert(!localEnv.locals.contains(x), "Already initialized local " + x.show) + localEnv.locals(x) = value + case _ => + throw new RuntimeException("Incorrect local environment for initializing " + x.show) + + /** + * Resolve the definition environment for the given local variable. + * + * A local variable could be located in outer scope with intermixed classes between its + * definition site and usage site. + * + * Due to widening, the corresponding environment might not exist. As a result reading the local + * variable will return `Cold` and it's forbidden to write to the local variable. + * + * @param sym The symbol of the local variable + * @param thisV The value for `this` of the enclosing class where the local variable is referenced. + * @param env The local environment where the local variable is referenced. + */ + def resolveDefinitionEnv(sym: Symbol, thisV: Value, env: Data): Option[(Value, Data)] = + if env.contains(sym) then Some(thisV -> env) + else + env match + case localEnv: LocalEnv => + resolveDefinitionEnv(sym, thisV, localEnv.outer) + case NoEnv => + // TODO: handle RefSet + thisV match + case ref: OfClass => + resolveDefinitionEnv(sym, ref.outer, ref.env) + case _ => + None + end Env /** Abstract heap for mutable fields * @@ -253,10 +313,6 @@ object Objects: val data2 = data.updated(addr, value) State.setHeap(data2) - def containsLocalVar(ref: Ref, env: Env.Data, sym: Symbol)(using state: State.Data): Boolean = - val data: Data = State.getHeap() - data.map.contains(LocalVarAddr(ref, env, sym)) - def readLocalVar(ref: Ref, env: Env.Data, sym: Symbol)(using state: State.Data): Value = val data: Data = State.getHeap() data.map(LocalVarAddr(ref, env, sym)) @@ -312,17 +368,18 @@ object Objects: case RefSet(refs) => refs.map(ref => ref.widen(height)).join - case Fun(expr, thisV, klass) => + case Fun(expr, thisV, klass, env) => if height == 0 then Cold - else Fun(expr, thisV.widen(height), klass) + else Fun(expr, thisV.widen(height), klass, env.widen(height)) - case ref @ OfClass(klass, outer, init, args, owner) => + case ref @ OfClass(klass, outer, init, args, env, owner) => if height == 0 then Cold else val outer2 = outer.widen(height - 1) - val args2 = args.map(arg => arg.widen(height - 1)) - OfClass(klass, outer2, init, args2, owner) + val args2 = args.map(_.widen(height - 1)) + val env2 = env.widen(height - 1) + OfClass(klass, outer2, init, args2, env2, owner) case _ => a @@ -362,8 +419,9 @@ object Objects: case _ => val cls = target.owner.enclosingClass.asClass val ddef = target.defTree.asInstanceOf[DefDef] - given Env.Data = Env.of(ddef, args.map(_.value)) + val env2 = Env.of(ddef, args.map(_.value), if ddef.symbol.owner.isClass then Env.NoEnv else summon[Env.Data]) extendTrace(ddef) { + given Env.Data = env2 eval(ddef.rhs, ref, cls, cacheResult = true) } else @@ -382,11 +440,12 @@ object Objects: // See tests/init/pos/Type.scala Bottom - case Fun(expr, thisV, klass) => + case Fun(expr, thisV, klass, env) => // meth == NoSymbol for poly functions if meth.name.toString == "tupled" then value // a call like `fun.tupled` else + given Env.Data = env eval(expr, thisV, klass, cacheResult = true) case RefSet(vs) => @@ -394,13 +453,6 @@ object Objects: } def callConstructor(thisV: Value, ctor: Symbol, args: List[ArgInfo]): Contextual[Value] = log("call " + ctor.show + ", args = " + args.map(_.value.show), printer, (_: Value).show) { - // init "fake" param fields for parameters of primary and secondary constructors - def addParamsAsFields(args: List[Value], ref: Ref, ctorDef: DefDef) = - val params = ctorDef.termParamss.flatten.map(_.symbol) - assert(args.size == params.size, "arguments = " + args.size + ", params = " + params.size + ", ctor = " + ctor.show) - for (param, value) <- params.zip(args) do - ref.updateField(param, value) - printer.println(param.show + " initialized with " + value) thisV match case ref: Ref => @@ -408,7 +460,8 @@ object Objects: val cls = ctor.owner.enclosingClass.asClass val ddef = ctor.defTree.asInstanceOf[DefDef] val argValues = args.map(_.value) - addParamsAsFields(argValues, ref, ddef) + + given Env.Data = Env.of(ddef, argValues, Env.NoEnv) if ctor.isPrimaryConstructor then val tpl = cls.defTree.asInstanceOf[TypeDef].rhs.asInstanceOf[Template] extendTrace(cls.defTree) { eval(tpl, ref, cls, cacheResult = true) } @@ -472,8 +525,8 @@ object Objects: def assign(receiver: Value, field: Symbol, rhs: Value, rhsTyp: Type): Contextual[Value] = log("Assign" + field.show + " of " + receiver.show + ", rhs = " + rhs.show, printer, (_: Value).show) { receiver match - case Fun(body, thisV, klass) => - report.error("[Internal error] unexpected tree in assignment, fun = " + body.show + Trace.show, Trace.position) + case fun: Fun => + report.error("[Internal error] unexpected tree in assignment, fun = " + fun.expr.show + Trace.show, Trace.position) case Cold => report.warning("Assigning to cold aliases is forbidden", Trace.position) @@ -496,8 +549,8 @@ object Objects: def instantiate(outer: Value, klass: ClassSymbol, ctor: Symbol, args: List[ArgInfo]): Contextual[Value] = log("instantiating " + klass.show + ", outer = " + outer + ", args = " + args.map(_.value.show), printer, (_: Value).show) { outer match - case Fun(body, thisV, klass) => - report.error("[Internal error] unexpected tree in instantiating a function, fun = " + body.show + Trace.show, Trace.position) + case fun: Fun => + report.error("[Internal error] unexpected tree in instantiating a function, fun = " + fun.expr.show + Trace.show, Trace.position) Bottom case value: (Bottom.type | ObjectRef | OfClass | Cold.type) => @@ -505,8 +558,9 @@ object Objects: // Widen the outer to finitize the domain. Arguments already widened in `evalArgs`. val outerWidened = outer.widen(1) + val envWidened = if klass.owner.isClass then Env.NoEnv else summon[Env.Data].widen(1) - val instance = OfClass(klass, outerWidened, ctor, args.map(_.value), State.currentObject) + val instance = OfClass(klass, outerWidened, ctor, args.map(_.value), envWidened, State.currentObject) callConstructor(instance, ctor, args) instance @@ -514,6 +568,47 @@ object Objects: refs.map(ref => instantiate(ref, klass, ctor, args)).join } + def initLocal(ref: Ref, sym: Symbol, value: Value): Contextual[Unit] = log("initialize local " + sym.show + " with " + value.show, printer) { + if sym.is(Flags.Mutable) then + val env = summon[Env.Data] + Heap.writeLocalVar(ref, env, sym, value) + else + Env.setLocalVal(sym, value) + } + + def readLocal(thisV: Value, sym: Symbol): Contextual[Value] = log("reading local " + sym.show, printer, (_: Value).show) { + Env.resolveDefinitionEnv(sym, thisV, summon[Env.Data]) match + case Some(thisV -> env) => + if sym.is(Flags.Mutable) then + thisV match + case ref: Ref => + Heap.readLocalVar(ref, env, sym) + case _ => + Cold + else + Env(sym) + + case _ => Cold + } + + def writeLocal(thisV: Value, sym: Symbol, value: Value): Contextual[Value] = log("write local " + sym.show + " with " + value.show, printer, (_: Value).show) { + + assert(sym.is(Flags.Mutable), "Writing to immutable variable " + sym.show) + + Env.resolveDefinitionEnv(sym, thisV, summon[Env.Data]) match + case Some(thisV -> env) => + thisV match + case ref: Ref => + Heap.writeLocalVar(ref, summon[Env.Data], sym, value) + case _ => + report.warning("Assigning to variables in outer scope", Trace.position) + + case _ => + report.warning("Assigning to variables in outer scope", Trace.position) + + Bottom + } + // -------------------------------- algorithm -------------------------------- /** Check an individual object */ @@ -529,7 +624,7 @@ object Objects: count += 1 given Trace = Trace.empty.add(tpl.constr) - given env: Env.Data = Env.empty + given env: Env.Data = Env.NoEnv log("Iteration " + count) { init(tpl, ObjectRef(classSym), classSym) @@ -696,17 +791,15 @@ object Objects: val value = eval(rhs, thisV, klass) if isLocal then - // TODO: the local var might be from outer environment. - Heap.writeLocalVar(receiver.asInstanceOf[Ref], summon[Env.Data], lhs.symbol, value) - Bottom + writeLocal(receiver.asInstanceOf[Ref], lhs.symbol, value) else withTrace(trace2) { assign(receiver, lhs.symbol, value, rhs.tpe) } case closureDef(ddef) => - Fun(ddef.rhs, thisV, klass) + Fun(ddef.rhs, thisV, klass, summon[Env.Data]) case PolyFun(body) => - Fun(body, thisV, klass) + Fun(body, thisV, klass, summon[Env.Data]) case Block(stats, expr) => evalExprs(stats, thisV, klass) @@ -754,13 +847,7 @@ object Objects: // local val definition val rhs = eval(vdef.rhs, thisV, klass) val sym = vdef.symbol - if vdef.symbol.is(Flags.Mutable) then - val ref = thisV.asInstanceOf[Ref] - val env = summon[Env.Data] - Heap.writeLocalVar(ref, env, sym, rhs) - else - Env.setLocalVal(vdef.symbol, rhs) - + initLocal(ref.asInstanceOf[Ref], vdef.symbol, rhs) Bottom case ddef : DefDef => @@ -801,23 +888,8 @@ object Objects: case tmref: TermRef if tmref.prefix == NoPrefix => val sym = tmref.symbol - val valueOpt = Env.get(sym) - if valueOpt.nonEmpty then - valueOpt.get - - else if sym.is(Flags.Mutable) then - val ref = thisV.asInstanceOf[Ref] - val env = summon[Env.Data] - if Heap.containsLocalVar(ref, env, sym) then - Heap.readLocalVar(ref, env, sym) - else - Cold - - else if sym.is(Flags.Package) then - Bottom - - else - Cold + if sym.is(Flags.Package) then Bottom + else readLocal(thisV, sym) case tmref: TermRef => val sym = tmref.symbol @@ -853,7 +925,7 @@ object Objects: args.foreach { arg => val res = if arg.isByName then - Fun(arg.tree, thisV, klass) + Fun(arg.tree, thisV, klass, summon[Env.Data]) else eval(arg.tree, thisV, klass) @@ -876,7 +948,7 @@ object Objects: */ def init(tpl: Template, thisV: Ref, klass: ClassSymbol): Contextual[Value] = log("init " + klass.show, printer, (_: Value).show) { val paramsMap = tpl.constr.termParamss.flatten.map { vdef => - vdef.name -> thisV.fieldValue(vdef.symbol) + vdef.name -> Env(vdef.symbol) }.toMap // init param fields From 1c66251be413242c3019e0c4d1ade8c9964000f9 Mon Sep 17 00:00:00 2001 From: Fengyun Liu Date: Thu, 23 Feb 2023 08:54:35 +0100 Subject: [PATCH 065/113] Fix resolution of definition environment --- .../tools/dotc/transform/init/Objects.scala | 36 +++++++++++-------- 1 file changed, 21 insertions(+), 15 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/transform/init/Objects.scala b/compiler/src/dotty/tools/dotc/transform/init/Objects.scala index 34782ad7b926..9273ab70af93 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Objects.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Objects.scala @@ -198,6 +198,12 @@ object Objects: def widen(height: Int)(using Context): Data = new LocalEnv(params.map(_ -> _.widen(height)), owner, outer.widen(height)) + + override def toString() = + "params: " + params + "\n" + + "locals: " + locals + "\n" + + "outer {\n" + outer + "\n}" + end LocalEnv object NoEnv extends Data: @@ -249,19 +255,19 @@ object Objects: * @param thisV The value for `this` of the enclosing class where the local variable is referenced. * @param env The local environment where the local variable is referenced. */ - def resolveDefinitionEnv(sym: Symbol, thisV: Value, env: Data): Option[(Value, Data)] = - if env.contains(sym) then Some(thisV -> env) - else - env match - case localEnv: LocalEnv => - resolveDefinitionEnv(sym, thisV, localEnv.outer) - case NoEnv => - // TODO: handle RefSet - thisV match - case ref: OfClass => - resolveDefinitionEnv(sym, ref.outer, ref.env) - case _ => - None + def resolveDefinitionEnv(sym: Symbol, thisV: Value, env: Data)(using Context): Option[(Value, Data)] = + env match + case localEnv: LocalEnv => + if localEnv.owner == sym.owner then Some(thisV -> env) + else resolveDefinitionEnv(sym, thisV, localEnv.outer) + case NoEnv => + // TODO: handle RefSet + thisV match + case ref: OfClass => + resolveDefinitionEnv(sym, ref.outer, ref.env) + case _ => + None + end resolveDefinitionEnv end Env /** Abstract heap for mutable fields @@ -791,7 +797,7 @@ object Objects: val value = eval(rhs, thisV, klass) if isLocal then - writeLocal(receiver.asInstanceOf[Ref], lhs.symbol, value) + writeLocal(thisV, lhs.symbol, value) else withTrace(trace2) { assign(receiver, lhs.symbol, value, rhs.tpe) } @@ -847,7 +853,7 @@ object Objects: // local val definition val rhs = eval(vdef.rhs, thisV, klass) val sym = vdef.symbol - initLocal(ref.asInstanceOf[Ref], vdef.symbol, rhs) + initLocal(thisV.asInstanceOf[Ref], vdef.symbol, rhs) Bottom case ddef : DefDef => From 7ade2e7253731869ec84b25d76cd5a1e8bd7300c Mon Sep 17 00:00:00 2001 From: Fengyun Liu Date: Fri, 24 Feb 2023 06:05:12 +0100 Subject: [PATCH 066/113] Lookup local variable using the right environment --- .../src/dotty/tools/dotc/transform/init/Objects.scala | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/transform/init/Objects.scala b/compiler/src/dotty/tools/dotc/transform/init/Objects.scala index 9273ab70af93..1ef146ca680d 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Objects.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Objects.scala @@ -180,7 +180,6 @@ object Objects: private[Env] def get(x: Symbol)(using Context): Option[Value] private[Env] def contains(x: Symbol): Boolean - def exists: Boolean def widen(height: Int)(using Context): Data /** Local environments can be deeply nested, therefore we need `outer`. */ @@ -194,8 +193,6 @@ object Objects: private[Env] def contains(x: Symbol): Boolean = params.contains(x) || locals.contains(x) - val exists: Boolean = true - def widen(height: Int)(using Context): Data = new LocalEnv(params.map(_ -> _.widen(height)), owner, outer.widen(height)) @@ -213,8 +210,6 @@ object Objects: private[Env] def contains(x: Symbol): Boolean = throw new RuntimeException("Invalid usage of non-existent env") - val exists: Boolean = false - def widen(height: Int)(using Context): Data = this end NoEnv @@ -230,7 +225,7 @@ object Objects: def of(ddef: DefDef, args: List[Value], outer: Data)(using Context): Data = val params = ddef.termParamss.flatten.map(_.symbol) - assert(args.size == params.size && (ddef.symbol.owner.isClass ^ outer.exists), "arguments = " + args.size + ", params = " + params.size) + assert(args.size == params.size && (ddef.symbol.owner.isClass ^ (outer != NoEnv)), "arguments = " + args.size + ", params = " + params.size) new LocalEnv(params.zip(args).toMap, ddef.symbol, outer) def setLocalVal(x: Symbol, value: Value)(using data: Data, ctx: Context): Unit = @@ -591,7 +586,11 @@ object Objects: Heap.readLocalVar(ref, env, sym) case _ => Cold + else if sym.isPatternBound then + // TODO: handle patterns + Cold else + given Env.Data = env Env(sym) case _ => Cold From 0d3ca9d8eaea7f61aecf4b476172d3c14d3662ab Mon Sep 17 00:00:00 2001 From: Fengyun Liu Date: Fri, 24 Feb 2023 07:04:15 +0100 Subject: [PATCH 067/113] Use correct outer environment for local methods --- .../tools/dotc/transform/init/Objects.scala | 50 +++++++++++++------ tests/init/pos/global-recursion.scala | 6 +++ 2 files changed, 41 insertions(+), 15 deletions(-) create mode 100644 tests/init/pos/global-recursion.scala diff --git a/compiler/src/dotty/tools/dotc/transform/init/Objects.scala b/compiler/src/dotty/tools/dotc/transform/init/Objects.scala index 1ef146ca680d..81ccff1b551c 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Objects.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Objects.scala @@ -182,8 +182,15 @@ object Objects: def widen(height: Int)(using Context): Data + def level: Int + /** Local environments can be deeply nested, therefore we need `outer`. */ - private case class LocalEnv(private[Env] val params: Map[Symbol, Value], owner: Symbol, outer: Data) extends Data: + private case class LocalEnv(private[Env] val params: Map[Symbol, Value], owner: Symbol, outer: Data)(using Context) extends Data: + val level = outer.level + 1 + + if (level > 3) + report.warning("[Internal error] Deeply nested environemnt, level = " + level + ", " + owner.show + " in " + owner.enclosingClass.show, owner.defTree) + private[Env] val locals: mutable.Map[Symbol, Value] = mutable.Map.empty private[Env] def get(x: Symbol)(using Context): Option[Value] = @@ -204,6 +211,8 @@ object Objects: end LocalEnv object NoEnv extends Data: + val level = 0 + private[Env] def get(x: Symbol)(using Context): Option[Value] = throw new RuntimeException("Invalid usage of non-existent env") @@ -215,7 +224,7 @@ object Objects: /** An empty environment can be used for non-method environments, e.g., field initializers. */ - def emptyEnv(owner: Symbol): Data = new LocalEnv(Map.empty, owner, NoEnv) + def emptyEnv(owner: Symbol)(using Context): Data = new LocalEnv(Map.empty, owner, NoEnv) def apply(x: Symbol)(using data: Data, ctx: Context): Value = data.get(x).get @@ -238,31 +247,31 @@ object Objects: throw new RuntimeException("Incorrect local environment for initializing " + x.show) /** - * Resolve the definition environment for the given local variable. + * Resolve the environment owned by the given symbol. * - * A local variable could be located in outer scope with intermixed classes between its + * The owner (e.g., method) could be located in outer scope with intermixed classes between its * definition site and usage site. * * Due to widening, the corresponding environment might not exist. As a result reading the local * variable will return `Cold` and it's forbidden to write to the local variable. * - * @param sym The symbol of the local variable + * @param meth The method which owns the environment * @param thisV The value for `this` of the enclosing class where the local variable is referenced. * @param env The local environment where the local variable is referenced. */ - def resolveDefinitionEnv(sym: Symbol, thisV: Value, env: Data)(using Context): Option[(Value, Data)] = + def resolveEnv(owner: Symbol, thisV: Value, env: Data)(using Context): Option[(Value, Data)] = env match case localEnv: LocalEnv => - if localEnv.owner == sym.owner then Some(thisV -> env) - else resolveDefinitionEnv(sym, thisV, localEnv.outer) + if localEnv.owner == owner then Some(thisV -> env) + else resolveEnv(owner, thisV, localEnv.outer) case NoEnv => // TODO: handle RefSet thisV match case ref: OfClass => - resolveDefinitionEnv(sym, ref.outer, ref.env) + resolveEnv(owner, ref.outer, ref.env) case _ => None - end resolveDefinitionEnv + end resolveEnv end Env /** Abstract heap for mutable fields @@ -420,7 +429,15 @@ object Objects: case _ => val cls = target.owner.enclosingClass.asClass val ddef = target.defTree.asInstanceOf[DefDef] - val env2 = Env.of(ddef, args.map(_.value), if ddef.symbol.owner.isClass then Env.NoEnv else summon[Env.Data]) + val meth = ddef.symbol + + val (thisV, outerEnv) = + if meth.owner.isClass then + (ref, Env.NoEnv) + else + Env.resolveEnv(meth.owner, ref, summon[Env.Data]).getOrElse(Cold -> Env.NoEnv) + + val env2 = Env.of(ddef, args.map(_.value), outerEnv) extendTrace(ddef) { given Env.Data = env2 eval(ddef.rhs, ref, cls, cacheResult = true) @@ -558,8 +575,11 @@ object Objects: // The outer can be a bottom value for top-level classes. // Widen the outer to finitize the domain. Arguments already widened in `evalArgs`. - val outerWidened = outer.widen(1) - val envWidened = if klass.owner.isClass then Env.NoEnv else summon[Env.Data].widen(1) + val (outerWidened, envWidened) = + if klass.owner.isClass then + (outer.widen(1), Env.NoEnv) + else + Env.resolveEnv(klass.owner, outer, summon[Env.Data]).getOrElse(Cold -> Env.NoEnv) val instance = OfClass(klass, outerWidened, ctor, args.map(_.value), envWidened, State.currentObject) callConstructor(instance, ctor, args) @@ -578,7 +598,7 @@ object Objects: } def readLocal(thisV: Value, sym: Symbol): Contextual[Value] = log("reading local " + sym.show, printer, (_: Value).show) { - Env.resolveDefinitionEnv(sym, thisV, summon[Env.Data]) match + Env.resolveEnv(sym.owner, thisV, summon[Env.Data]) match case Some(thisV -> env) => if sym.is(Flags.Mutable) then thisV match @@ -600,7 +620,7 @@ object Objects: assert(sym.is(Flags.Mutable), "Writing to immutable variable " + sym.show) - Env.resolveDefinitionEnv(sym, thisV, summon[Env.Data]) match + Env.resolveEnv(sym.owner, thisV, summon[Env.Data]) match case Some(thisV -> env) => thisV match case ref: Ref => diff --git a/tests/init/pos/global-recursion.scala b/tests/init/pos/global-recursion.scala new file mode 100644 index 000000000000..42c80c46fde3 --- /dev/null +++ b/tests/init/pos/global-recursion.scala @@ -0,0 +1,6 @@ +object Recursion: + def foo(): Int = + def fact(x: Int): Int = if x == 0 then 1 else x * fact(x - 1) + fact(5) + + val n = foo() \ No newline at end of file From e8520c21b138d6692bd40b5856fa7e9e43cd04fb Mon Sep 17 00:00:00 2001 From: Fengyun Liu Date: Fri, 24 Feb 2023 08:03:23 +0100 Subject: [PATCH 068/113] Use correct environment for val definitions --- .../tools/dotc/transform/init/Objects.scala | 21 +++++++++++-------- tests/init/pos/global-val-owner.scala | 15 +++++++++++++ 2 files changed, 27 insertions(+), 9 deletions(-) create mode 100644 tests/init/pos/global-val-owner.scala diff --git a/compiler/src/dotty/tools/dotc/transform/init/Objects.scala b/compiler/src/dotty/tools/dotc/transform/init/Objects.scala index 81ccff1b551c..160673e96139 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Objects.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Objects.scala @@ -234,7 +234,8 @@ object Objects: def of(ddef: DefDef, args: List[Value], outer: Data)(using Context): Data = val params = ddef.termParamss.flatten.map(_.symbol) - assert(args.size == params.size && (ddef.symbol.owner.isClass ^ (outer != NoEnv)), "arguments = " + args.size + ", params = " + params.size) + assert(args.size == params.size, "arguments = " + args.size + ", params = " + params.size) + assert(ddef.symbol.owner.isClass ^ (outer != NoEnv), "ddef.owner = " + ddef.symbol.owner.show + ", outer = " + outer + ", " + ddef.source) new LocalEnv(params.zip(args).toMap, ddef.symbol, outer) def setLocalVal(x: Symbol, value: Value)(using data: Data, ctx: Context): Unit = @@ -247,10 +248,10 @@ object Objects: throw new RuntimeException("Incorrect local environment for initializing " + x.show) /** - * Resolve the environment owned by the given symbol. + * Resolve the environment owned by the given method. * - * The owner (e.g., method) could be located in outer scope with intermixed classes between its - * definition site and usage site. + * The method could be located in outer scope with intermixed classes between its definition + * site and usage site. * * Due to widening, the corresponding environment might not exist. As a result reading the local * variable will return `Cold` and it's forbidden to write to the local variable. @@ -272,6 +273,8 @@ object Objects: case _ => None end resolveEnv + + def withEnv[T](env: Data)(fn: Data ?=> T): T = fn(using env) end Env /** Abstract heap for mutable fields @@ -435,7 +438,7 @@ object Objects: if meth.owner.isClass then (ref, Env.NoEnv) else - Env.resolveEnv(meth.owner, ref, summon[Env.Data]).getOrElse(Cold -> Env.NoEnv) + Env.resolveEnv(meth.owner.enclosingMethod, ref, summon[Env.Data]).getOrElse(Cold -> Env.NoEnv) val env2 = Env.of(ddef, args.map(_.value), outerEnv) extendTrace(ddef) { @@ -579,7 +582,7 @@ object Objects: if klass.owner.isClass then (outer.widen(1), Env.NoEnv) else - Env.resolveEnv(klass.owner, outer, summon[Env.Data]).getOrElse(Cold -> Env.NoEnv) + Env.resolveEnv(klass.enclosingMethod, outer, summon[Env.Data]).getOrElse(Cold -> Env.NoEnv) val instance = OfClass(klass, outerWidened, ctor, args.map(_.value), envWidened, State.currentObject) callConstructor(instance, ctor, args) @@ -598,7 +601,7 @@ object Objects: } def readLocal(thisV: Value, sym: Symbol): Contextual[Value] = log("reading local " + sym.show, printer, (_: Value).show) { - Env.resolveEnv(sym.owner, thisV, summon[Env.Data]) match + Env.resolveEnv(sym.enclosingMethod, thisV, summon[Env.Data]) match case Some(thisV -> env) => if sym.is(Flags.Mutable) then thisV match @@ -620,7 +623,7 @@ object Objects: assert(sym.is(Flags.Mutable), "Writing to immutable variable " + sym.show) - Env.resolveEnv(sym.owner, thisV, summon[Env.Data]) match + Env.resolveEnv(sym.enclosingMethod, thisV, summon[Env.Data]) match case Some(thisV -> env) => thisV match case ref: Ref => @@ -649,7 +652,7 @@ object Objects: count += 1 given Trace = Trace.empty.add(tpl.constr) - given env: Env.Data = Env.NoEnv + given env: Env.Data = Env.emptyEnv(tpl.constr.symbol) log("Iteration " + count) { init(tpl, ObjectRef(classSym), classSym) diff --git a/tests/init/pos/global-val-owner.scala b/tests/init/pos/global-val-owner.scala new file mode 100644 index 000000000000..164e56d9e776 --- /dev/null +++ b/tests/init/pos/global-val-owner.scala @@ -0,0 +1,15 @@ +object Test: + val n = { + def fact(x: Int): Int = if x == 0 then 1 else x * fact(x - 1) + fact(5) + } + + def foo() = + val n = { + def fact(x: Int): Int = if x == 0 then 1 else x * fact(x - 1) + fact(5) + } + n + + val x = foo() + From 2467571054add061979e04278b6c6341c4554ad9 Mon Sep 17 00:00:00 2001 From: Fengyun Liu Date: Fri, 24 Feb 2023 21:30:10 +0100 Subject: [PATCH 069/113] Workaround bug in typer --- .../tools/dotc/transform/init/Objects.scala | 33 +++++++++++++++---- tests/init/pos/global-val-owner2.scala | 5 +++ 2 files changed, 31 insertions(+), 7 deletions(-) create mode 100644 tests/init/pos/global-val-owner2.scala diff --git a/compiler/src/dotty/tools/dotc/transform/init/Objects.scala b/compiler/src/dotty/tools/dotc/transform/init/Objects.scala index 160673e96139..ac0a3f2aa04c 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Objects.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Objects.scala @@ -184,6 +184,8 @@ object Objects: def level: Int + def show(using Context): String + /** Local environments can be deeply nested, therefore we need `outer`. */ private case class LocalEnv(private[Env] val params: Map[Symbol, Value], owner: Symbol, outer: Data)(using Context) extends Data: val level = outer.level + 1 @@ -203,10 +205,11 @@ object Objects: def widen(height: Int)(using Context): Data = new LocalEnv(params.map(_ -> _.widen(height)), owner, outer.widen(height)) - override def toString() = - "params: " + params + "\n" + - "locals: " + locals + "\n" + - "outer {\n" + outer + "\n}" + def show(using Context) = + "owner: " + owner.show + "\n" + + "params: " + params.map(_.show + " ->" + _.show).mkString("{", ", ", "}") + "\n" + + "locals: " + locals.map(_.show + " ->" + _.show).mkString("{", ", ", "}") + "\n" + + "outer = {\n" + outer.show + "\n}" end LocalEnv @@ -220,6 +223,8 @@ object Objects: throw new RuntimeException("Invalid usage of non-existent env") def widen(height: Int)(using Context): Data = this + + def show(using Context): String = "NoEnv" end NoEnv /** An empty environment can be used for non-method environments, e.g., field initializers. @@ -614,7 +619,11 @@ object Objects: Cold else given Env.Data = env - Env(sym) + try + Env(sym) + catch ex => + report.warning("[Internal error] Not found " + sym.show + "\nenv = " + env.show + ". Calling trace:\n" + Trace.show, Trace.position) + Bottom case _ => Cold } @@ -916,8 +925,18 @@ object Objects: case tmref: TermRef if tmref.prefix == NoPrefix => val sym = tmref.symbol - if sym.is(Flags.Package) then Bottom - else readLocal(thisV, sym) + if sym.is(Flags.Package) then + Bottom + else if sym.owner.isClass then + // The typer incorrectly assigns a TermRef with NoPrefix for `config`, + // while the actual denotation points to the symbol of the class member + // instead of the parameter symbol for the primary constructor. + // + // abstract class Base(implicit config: Int) + // case class A(x: Int)(implicit config: Int) extends Base + evalType(sym.termRef, thisV, klass, elideObjectAccess) + else + readLocal(thisV, sym) case tmref: TermRef => val sym = tmref.symbol diff --git a/tests/init/pos/global-val-owner2.scala b/tests/init/pos/global-val-owner2.scala new file mode 100644 index 000000000000..8d84b792cc81 --- /dev/null +++ b/tests/init/pos/global-val-owner2.scala @@ -0,0 +1,5 @@ +object Test: + abstract class Base(implicit config: Int) + case class A(x: Int)(implicit config: Int) extends Base + + val a: A = A(3)(using 5) From dd3ef2cf871e370e8ff973c8216fa41b6b7155f1 Mon Sep 17 00:00:00 2001 From: Fengyun Liu Date: Fri, 24 Feb 2023 21:30:18 +0100 Subject: [PATCH 070/113] Suppress trivial self cycle warning --- .../src/dotty/tools/dotc/transform/init/Objects.scala | 2 +- tests/init/pos/global-trivial-cycle.scala | 9 +++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) create mode 100644 tests/init/pos/global-trivial-cycle.scala diff --git a/compiler/src/dotty/tools/dotc/transform/init/Objects.scala b/compiler/src/dotty/tools/dotc/transform/init/Objects.scala index ac0a3f2aa04c..38d237b07bc8 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Objects.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Objects.scala @@ -158,7 +158,7 @@ object Objects: def checkCycle(clazz: ClassSymbol)(work: => Unit)(using data: Data, ctx: Context, pendingTrace: Trace) = val index = data.checkingObjects.indexOf(clazz) - if index != -1 && data.checkingObjects.size > 1 then + if index != -1 && data.checkingObjects.size - 1 > index then val joinedTrace = data.pendingTraces.slice(index + 1, data.checkingObjects.size).foldLeft(pendingTrace) { (a, acc) => acc ++ a } val callTrace = Trace.buildStacktrace(joinedTrace, "Calling trace:\n") val cycle = data.checkingObjects.slice(index, data.checkingObjects.size) diff --git a/tests/init/pos/global-trivial-cycle.scala b/tests/init/pos/global-trivial-cycle.scala new file mode 100644 index 000000000000..b9371e8600db --- /dev/null +++ b/tests/init/pos/global-trivial-cycle.scala @@ -0,0 +1,9 @@ +object C: + val n: Int = A.n + +object A: + val x: Int = 10 + val n: Int = B.foo() + +object B: + def foo(): Int = A.x From 9bc0df6f74954a13b8f53bf3a6ca49e38970a30f Mon Sep 17 00:00:00 2001 From: Fengyun Liu Date: Fri, 24 Feb 2023 22:15:05 +0100 Subject: [PATCH 071/113] Fix environment for local class --- .../tools/dotc/transform/init/Objects.scala | 31 ++++++++++--------- 1 file changed, 17 insertions(+), 14 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/transform/init/Objects.scala b/compiler/src/dotty/tools/dotc/transform/init/Objects.scala index 38d237b07bc8..0e184171eaef 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Objects.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Objects.scala @@ -162,7 +162,7 @@ object Objects: val joinedTrace = data.pendingTraces.slice(index + 1, data.checkingObjects.size).foldLeft(pendingTrace) { (a, acc) => acc ++ a } val callTrace = Trace.buildStacktrace(joinedTrace, "Calling trace:\n") val cycle = data.checkingObjects.slice(index, data.checkingObjects.size) - val pos = clazz.defTree.asInstanceOf[TypeDef].rhs.asInstanceOf[Template].constr + val pos = clazz.defTree report.warning("Cyclic initialization: " + cycle.map(_.show).mkString(" -> ") + " -> " + clazz.show + ". " + callTrace, pos) else if index == -1 && data.checkedObjects.indexOf(clazz) == -1 then data.pendingTraces += pendingTrace @@ -186,12 +186,15 @@ object Objects: def show(using Context): String - /** Local environments can be deeply nested, therefore we need `outer`. */ - private case class LocalEnv(private[Env] val params: Map[Symbol, Value], owner: Symbol, outer: Data)(using Context) extends Data: + /** Local environments can be deeply nested, therefore we need `outer`. + * + * For local variables in rhs of class field definitions, the `meth` is the primary constructor. + */ + private case class LocalEnv(private[Env] val params: Map[Symbol, Value], meth: Symbol, outer: Data)(using Context) extends Data: val level = outer.level + 1 if (level > 3) - report.warning("[Internal error] Deeply nested environemnt, level = " + level + ", " + owner.show + " in " + owner.enclosingClass.show, owner.defTree) + report.warning("[Internal error] Deeply nested environemnt, level = " + level + ", " + meth.show + " in " + meth.enclosingClass.show, meth.defTree) private[Env] val locals: mutable.Map[Symbol, Value] = mutable.Map.empty @@ -203,10 +206,10 @@ object Objects: params.contains(x) || locals.contains(x) def widen(height: Int)(using Context): Data = - new LocalEnv(params.map(_ -> _.widen(height)), owner, outer.widen(height)) + new LocalEnv(params.map(_ -> _.widen(height)), meth, outer.widen(height)) def show(using Context) = - "owner: " + owner.show + "\n" + + "owner: " + meth.show + "\n" + "params: " + params.map(_.show + " ->" + _.show).mkString("{", ", ", "}") + "\n" + "locals: " + locals.map(_.show + " ->" + _.show).mkString("{", ", ", "}") + "\n" + "outer = {\n" + outer.show + "\n}" @@ -265,19 +268,19 @@ object Objects: * @param thisV The value for `this` of the enclosing class where the local variable is referenced. * @param env The local environment where the local variable is referenced. */ - def resolveEnv(owner: Symbol, thisV: Value, env: Data)(using Context): Option[(Value, Data)] = + def resolveEnv(meth: Symbol, thisV: Value, env: Data)(using Context): Option[(Value, Data)] = log("Resolving env for " + meth.show + ", this = " + thisV.show + ", env = " + env.show, printer) { env match case localEnv: LocalEnv => - if localEnv.owner == owner then Some(thisV -> env) - else resolveEnv(owner, thisV, localEnv.outer) + if localEnv.meth == meth then Some(thisV -> env) + else resolveEnv(meth, thisV, localEnv.outer) case NoEnv => // TODO: handle RefSet thisV match case ref: OfClass => - resolveEnv(owner, ref.outer, ref.env) + resolveEnv(meth, ref.outer, ref.env) case _ => None - end resolveEnv + } def withEnv[T](env: Data)(fn: Data ?=> T): T = fn(using env) end Env @@ -587,7 +590,8 @@ object Objects: if klass.owner.isClass then (outer.widen(1), Env.NoEnv) else - Env.resolveEnv(klass.enclosingMethod, outer, summon[Env.Data]).getOrElse(Cold -> Env.NoEnv) + // klass.enclosingMethod returns its primary constructor + Env.resolveEnv(klass.owner.enclosingMethod, outer, summon[Env.Data]).getOrElse(Cold -> Env.NoEnv) val instance = OfClass(klass, outerWidened, ctor, args.map(_.value), envWidened, State.currentObject) callConstructor(instance, ctor, args) @@ -631,7 +635,6 @@ object Objects: def writeLocal(thisV: Value, sym: Symbol, value: Value): Contextual[Value] = log("write local " + sym.show + " with " + value.show, printer, (_: Value).show) { assert(sym.is(Flags.Mutable), "Writing to immutable variable " + sym.show) - Env.resolveEnv(sym.enclosingMethod, thisV, summon[Env.Data]) match case Some(thisV -> env) => thisV match @@ -660,7 +663,7 @@ object Objects: def iterate()(using Context): Unit = count += 1 - given Trace = Trace.empty.add(tpl.constr) + given Trace = Trace.empty.add(classSym.defTree) given env: Env.Data = Env.emptyEnv(tpl.constr.symbol) log("Iteration " + count) { From 1b1727e9ea0b6fcfae8236153f073cb5d119d84e Mon Sep 17 00:00:00 2001 From: Fengyun Liu Date: Sat, 25 Feb 2023 01:50:04 +0100 Subject: [PATCH 072/113] Refactor heap: make it disjoint for different objects --- .../tools/dotc/transform/init/Objects.scala | 89 +++++++++---------- 1 file changed, 40 insertions(+), 49 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/transform/init/Objects.scala b/compiler/src/dotty/tools/dotc/transform/init/Objects.scala index 0e184171eaef..ac320c404fb6 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Objects.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Objects.scala @@ -15,6 +15,8 @@ import config.Printers.init as printer import reporting.StoreReporter import reporting.trace as log +import util.HashSet + import Errors.* import Trace.* import Util.* @@ -141,18 +143,11 @@ object Objects: object State: class Data: // objects under check - private[State] val checkingObjects = new mutable.ListBuffer[ClassSymbol] + private[State] val checkingObjects = new mutable.ArrayBuffer[ClassSymbol] private[State] val checkedObjects = new mutable.ArrayBuffer[ClassSymbol] - private[State] val pendingTraces = new mutable.ListBuffer[Trace] - - // Use a mutable field to avoid thread through it in the program. - private[State] var heap = Heap.empty + private[State] val pendingTraces = new mutable.ArrayBuffer[Trace] end Data - def getHeap()(using data: Data): Heap.Data = data.heap - def setHeap(heap: Heap.Data)(using data: Data) = - data.heap = heap - def currentObject(using data: Data): ClassSymbol = data.checkingObjects.last def checkCycle(clazz: ClassSymbol)(work: => Unit)(using data: Data, ctx: Context, pendingTrace: Trace) = @@ -286,9 +281,6 @@ object Objects: end Env /** Abstract heap for mutable fields - * - * To avoid threading through it in the code, we use a mutable field in `State.Data` to hold the - * information. */ object Heap: private abstract class Addr @@ -299,50 +291,48 @@ object Objects: /** The address for mutable local variables . */ private case class LocalVarAddr(ref: Ref, env: Env.Data, sym: Symbol) extends Addr + /** Immutable heap data */ opaque type Data = ImmutableMapWithRefEquality - /** Wrap the map in a class such that equality of two maps is defined as referential equality. - * - * This is a performance optimization. - */ - private class ImmutableMapWithRefEquality(val map: Map[Addr, Value]): - private[Heap] def updated(addr: Addr, value: Value): Data = - map.get(addr) match + /** Store the heap as a mutable field to avoid thread through it in the program. */ + class MutableData(private[Heap] var heap: Data): + private[Heap] def update(addr: Addr, value: Value): Unit = + heap.map.get(addr) match case None => - new Data(map.updated(addr, value)) + heap = new Data(heap.map.updated(addr, value)) case Some(current) => val value2 = value.join(current) if value2 != current then - new Data(map.updated(addr, value2)) - else - this + heap = new Data(heap.map.updated(addr, value2)) + + + /** Wrap the map in a class such that equality of two maps is defined as referential equality. + * + * This is a performance optimization. + */ + private class ImmutableMapWithRefEquality(val map: Map[Addr, Value]) - val empty: Data = new Data(Map.empty) + def empty(): MutableData = new MutableData(new Data(Map.empty)) - def contains(ref: Ref, field: Symbol)(using state: State.Data): Boolean = - val data: Data = State.getHeap() - data.map.contains(FieldAddr(ref, field)) + def contains(ref: Ref, field: Symbol)(using mutable: MutableData): Boolean = + mutable.heap.map.contains(FieldAddr(ref, field)) - def read(ref: Ref, field: Symbol)(using state: State.Data): Value = - val data: Data = State.getHeap() - data.map(FieldAddr(ref, field)) + def read(ref: Ref, field: Symbol)(using mutable: MutableData): Value = + mutable.heap.map(FieldAddr(ref, field)) - def write(ref: Ref, field: Symbol, value: Value)(using state: State.Data): Unit = + def write(ref: Ref, field: Symbol, value: Value)(using mutable: MutableData): Unit = val addr = FieldAddr(ref, field) - val data: Data = State.getHeap() - val data2 = data.updated(addr, value) - State.setHeap(data2) + mutable.update(addr, value) - def readLocalVar(ref: Ref, env: Env.Data, sym: Symbol)(using state: State.Data): Value = - val data: Data = State.getHeap() - data.map(LocalVarAddr(ref, env, sym)) + def readLocalVar(ref: Ref, env: Env.Data, sym: Symbol)(using mutable: MutableData): Value = + mutable.heap.map(LocalVarAddr(ref, env, sym)) - def writeLocalVar(ref: Ref, env: Env.Data, sym: Symbol, value: Value)(using state: State.Data): Unit = + def writeLocalVar(ref: Ref, env: Env.Data, sym: Symbol, value: Value)(using mutable: MutableData): Unit = val addr = LocalVarAddr(ref, env, sym) - val data: Data = State.getHeap() - val data2 = data.updated(addr, value) - State.setHeap(data2) + mutable.update(addr, value) + + def getHeapData()(using mutable: MutableData): Data = mutable.heap /** Cache used to terminate the check */ object Cache: @@ -350,21 +340,21 @@ object Objects: case class Res(value: Value, heap: Heap.Data) class Data extends Cache[Config, Res]: - def get(thisV: Value, expr: Tree)(using State.Data, Env.Data): Option[Value] = - val config = Config(thisV, summon[Env.Data], State.getHeap()) + def get(thisV: Value, expr: Tree)(using Heap.MutableData, Env.Data): Option[Value] = + val config = Config(thisV, summon[Env.Data], Heap.getHeapData()) super.get(config, expr).map(_.value) - def cachedEval(thisV: Value, expr: Tree, cacheResult: Boolean)(fun: Tree => Value)(using State.Data, Env.Data): Value = - val config = Config(thisV, summon[Env.Data], State.getHeap()) - val result = super.cachedEval(config, expr, cacheResult, default = Res(Bottom, State.getHeap())) { expr => - Res(fun(expr), State.getHeap()) + def cachedEval(thisV: Value, expr: Tree, cacheResult: Boolean)(fun: Tree => Value)(using Heap.MutableData, Env.Data): Value = + val config = Config(thisV, summon[Env.Data], Heap.getHeapData()) + val result = super.cachedEval(config, expr, cacheResult, default = Res(Bottom, Heap.getHeapData())) { expr => + Res(fun(expr), Heap.getHeapData()) } result.value end Cache inline def cache(using c: Cache.Data): Cache.Data = c - type Contextual[T] = (Context, State.Data, Env.Data, Cache.Data, Trace) ?=> T + type Contextual[T] = (Context, State.Data, Env.Data, Cache.Data, Heap.MutableData, Trace) ?=> T // --------------------------- domain operations ----------------------------- @@ -664,7 +654,8 @@ object Objects: count += 1 given Trace = Trace.empty.add(classSym.defTree) - given env: Env.Data = Env.emptyEnv(tpl.constr.symbol) + given Env.Data = Env.emptyEnv(tpl.constr.symbol) + given Heap.MutableData = Heap.empty() log("Iteration " + count) { init(tpl, ObjectRef(classSym), classSym) From 602b7750f728a03c643015d710fd0e8d1b92815b Mon Sep 17 00:00:00 2001 From: Fengyun Liu Date: Sun, 26 Feb 2023 15:35:59 +0100 Subject: [PATCH 073/113] Fix non-termination cause by equality of heap --- .../tools/dotc/transform/init/Objects.scala | 31 +++++++++---------- 1 file changed, 14 insertions(+), 17 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/transform/init/Objects.scala b/compiler/src/dotty/tools/dotc/transform/init/Objects.scala index ac320c404fb6..44fd8a3b137a 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Objects.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Objects.scala @@ -15,8 +15,6 @@ import config.Printers.init as printer import reporting.StoreReporter import reporting.trace as log -import util.HashSet - import Errors.* import Trace.* import Util.* @@ -291,42 +289,41 @@ object Objects: /** The address for mutable local variables . */ private case class LocalVarAddr(ref: Ref, env: Env.Data, sym: Symbol) extends Addr - /** Immutable heap data */ - opaque type Data = ImmutableMapWithRefEquality + /** Immutable heap data used in the cache. + * + * We need to use structural equivalence so that in different iterations the cache can be effective. + * + * TODO: speed up equality check for heap. + */ + opaque type Data = Map[Addr, Value] /** Store the heap as a mutable field to avoid thread through it in the program. */ class MutableData(private[Heap] var heap: Data): private[Heap] def update(addr: Addr, value: Value): Unit = - heap.map.get(addr) match + heap.get(addr) match case None => - heap = new Data(heap.map.updated(addr, value)) + heap = heap.updated(addr, value) case Some(current) => val value2 = value.join(current) if value2 != current then - heap = new Data(heap.map.updated(addr, value2)) + heap = heap.updated(addr, value2) - /** Wrap the map in a class such that equality of two maps is defined as referential equality. - * - * This is a performance optimization. - */ - private class ImmutableMapWithRefEquality(val map: Map[Addr, Value]) - - def empty(): MutableData = new MutableData(new Data(Map.empty)) + def empty(): MutableData = new MutableData(Map.empty) def contains(ref: Ref, field: Symbol)(using mutable: MutableData): Boolean = - mutable.heap.map.contains(FieldAddr(ref, field)) + mutable.heap.contains(FieldAddr(ref, field)) def read(ref: Ref, field: Symbol)(using mutable: MutableData): Value = - mutable.heap.map(FieldAddr(ref, field)) + mutable.heap(FieldAddr(ref, field)) def write(ref: Ref, field: Symbol, value: Value)(using mutable: MutableData): Unit = val addr = FieldAddr(ref, field) mutable.update(addr, value) def readLocalVar(ref: Ref, env: Env.Data, sym: Symbol)(using mutable: MutableData): Value = - mutable.heap.map(LocalVarAddr(ref, env, sym)) + mutable.heap(LocalVarAddr(ref, env, sym)) def writeLocalVar(ref: Ref, env: Env.Data, sym: Symbol, value: Value)(using mutable: MutableData): Unit = val addr = LocalVarAddr(ref, env, sym) From 3c07f621ee5538015a202bc2a64a88b757483ece Mon Sep 17 00:00:00 2001 From: Fengyun Liu Date: Sun, 26 Feb 2023 17:12:26 +0100 Subject: [PATCH 074/113] Fix typo: Use the widened value as method args --- compiler/src/dotty/tools/dotc/transform/init/Objects.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/compiler/src/dotty/tools/dotc/transform/init/Objects.scala b/compiler/src/dotty/tools/dotc/transform/init/Objects.scala index 44fd8a3b137a..7c68ce995214 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Objects.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Objects.scala @@ -974,7 +974,7 @@ object Objects: else Cold - argInfos += TraceValue(res, trace.add(arg.tree)) + argInfos += TraceValue(widened, trace.add(arg.tree)) } argInfos.toList From 5dfaf0be15f520b5933b59a8801e7c624b1ca4ca Mon Sep 17 00:00:00 2001 From: Fengyun Liu Date: Sun, 26 Feb 2023 17:30:49 +0100 Subject: [PATCH 075/113] Handle by-name parameters --- .../tools/dotc/transform/init/Objects.scala | 23 +++++++++++++++++-- tests/init/neg/global-by-name.scala | 11 +++++++++ tests/init/pos/global-by-name.scala | 13 +++++++++++ 3 files changed, 45 insertions(+), 2 deletions(-) create mode 100644 tests/init/neg/global-by-name.scala create mode 100644 tests/init/pos/global-by-name.scala diff --git a/compiler/src/dotty/tools/dotc/transform/init/Objects.scala b/compiler/src/dotty/tools/dotc/transform/init/Objects.scala index 7c68ce995214..7ce31516ba9e 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Objects.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Objects.scala @@ -611,12 +611,31 @@ object Objects: else given Env.Data = env try - Env(sym) + val value = Env(sym) + if sym.is(Flags.Param) && sym.info.isInstanceOf[ExprType] then + value match + case fun: Fun => + given Env.Data = fun.env + eval(fun.expr, fun.thisV, fun.klass) + case Cold => + report.warning("Calling cold by-name alias. Call trace: \n" + Trace.show, Trace.position) + Bottom + case _: RefSet | _: OfClass | _: ObjectRef => + report.warning("[Internal error] Unexpected by-name value " + value.show + ". Calling trace:\n" + Trace.show, Trace.position) + Bottom + else + value + catch ex => report.warning("[Internal error] Not found " + sym.show + "\nenv = " + env.show + ". Calling trace:\n" + Trace.show, Trace.position) Bottom - case _ => Cold + case _ => + if sym.is(Flags.Param) && sym.info.isInstanceOf[ExprType] then + report.warning("Calling cold by-name alias. Call trace: \n" + Trace.show, Trace.position) + Bottom + else + Cold } def writeLocal(thisV: Value, sym: Symbol, value: Value): Contextual[Value] = log("write local " + sym.show + " with " + value.show, printer, (_: Value).show) { diff --git a/tests/init/neg/global-by-name.scala b/tests/init/neg/global-by-name.scala new file mode 100644 index 000000000000..a7227650ccf5 --- /dev/null +++ b/tests/init/neg/global-by-name.scala @@ -0,0 +1,11 @@ +def time[A](f: => A): A = + val start = System.nanoTime + val res = f // error + val elapsed = (System.nanoTime - start) + res + +case class Foo(data: Int) + +object o: + val foo = time(Foo(3)) + println(foo.data) diff --git a/tests/init/pos/global-by-name.scala b/tests/init/pos/global-by-name.scala new file mode 100644 index 000000000000..bdb3c24350f8 --- /dev/null +++ b/tests/init/pos/global-by-name.scala @@ -0,0 +1,13 @@ +import scala.annotation.init + +def time[A](f: => A): A = + val start = System.nanoTime + val res = f + val elapsed = (System.nanoTime - start) + res + +case class Foo(data: Int) + +object o: + val foo = time(Foo(3): @init.expose) + println(foo.data) From 4b8ba9be1d90e7dd8e1476c7c35cfc687d870edd Mon Sep 17 00:00:00 2001 From: Fengyun Liu Date: Sun, 26 Feb 2023 22:00:36 +0100 Subject: [PATCH 076/113] Tighten initialization-time irrelevance for local vars --- .../tools/dotc/transform/init/Objects.scala | 62 +++++++++++++++---- tests/init/neg/global-irrelevance3.scala | 11 ++++ tests/init/neg/global-irrelevance4.scala | 11 ++++ 3 files changed, 72 insertions(+), 12 deletions(-) create mode 100644 tests/init/neg/global-irrelevance3.scala create mode 100644 tests/init/neg/global-irrelevance4.scala diff --git a/compiler/src/dotty/tools/dotc/transform/init/Objects.scala b/compiler/src/dotty/tools/dotc/transform/init/Objects.scala index 7ce31516ba9e..fb5fa7ffd4fc 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Objects.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Objects.scala @@ -13,7 +13,7 @@ import NameKinds.SuperAccessorName import ast.tpd.* import config.Printers.init as printer import reporting.StoreReporter -import reporting.trace as log +import reporting.trace.force as log import Errors.* import Trace.* @@ -179,11 +179,31 @@ object Objects: def show(using Context): String + def owner: ClassSymbol + /** Local environments can be deeply nested, therefore we need `outer`. * * For local variables in rhs of class field definitions, the `meth` is the primary constructor. + * + * We need to record the `owner` of a local environment, which is the static object which + * creates the environment, as a local environment may contain mutable vars. To enforce + * initialization-time irrelevance, we need to make sure that when the environment is captured + * in lambdas or local classes, it is impossible to read/write the captured mutable vars from a + * different object other than the object which creates and owns it. + * + * An environment and its outer environment might have different owners. For example: + * + * object A: + * def foo(): Int => Int = + * var n = 0 + * x => { n += 1; x + n } + * val f: Int => Int = foo() + * + * object B: + * val m = A.f(10) + * */ - private case class LocalEnv(private[Env] val params: Map[Symbol, Value], meth: Symbol, outer: Data)(using Context) extends Data: + private case class LocalEnv(private[Env] val params: Map[Symbol, Value], meth: Symbol, owner: ClassSymbol, outer: Data)(using Context) extends Data: val level = outer.level + 1 if (level > 3) @@ -199,7 +219,7 @@ object Objects: params.contains(x) || locals.contains(x) def widen(height: Int)(using Context): Data = - new LocalEnv(params.map(_ -> _.widen(height)), meth, outer.widen(height)) + new LocalEnv(params.map(_ -> _.widen(height)), meth, owner, outer.widen(height)) def show(using Context) = "owner: " + meth.show + "\n" + @@ -221,11 +241,13 @@ object Objects: def widen(height: Int)(using Context): Data = this def show(using Context): String = "NoEnv" + + def owner = throw new RuntimeException("Invalid usage of non-existent env") end NoEnv /** An empty environment can be used for non-method environments, e.g., field initializers. */ - def emptyEnv(owner: Symbol)(using Context): Data = new LocalEnv(Map.empty, owner, NoEnv) + def emptyEnv(meth: Symbol, owner: ClassSymbol)(using Context): Data = new LocalEnv(Map.empty, meth, owner, NoEnv) def apply(x: Symbol)(using data: Data, ctx: Context): Value = data.get(x).get @@ -233,11 +255,11 @@ object Objects: def contains(x: Symbol)(using data: Data): Boolean = data.contains(x) - def of(ddef: DefDef, args: List[Value], outer: Data)(using Context): Data = + def of(ddef: DefDef, args: List[Value], owner: ClassSymbol, outer: Data)(using Context): Data = val params = ddef.termParamss.flatten.map(_.symbol) assert(args.size == params.size, "arguments = " + args.size + ", params = " + params.size) assert(ddef.symbol.owner.isClass ^ (outer != NoEnv), "ddef.owner = " + ddef.symbol.owner.show + ", outer = " + outer + ", " + ddef.source) - new LocalEnv(params.zip(args).toMap, ddef.symbol, outer) + new LocalEnv(params.zip(args).toMap, ddef.symbol, owner, outer) def setLocalVal(x: Symbol, value: Value)(using data: Data, ctx: Context): Unit = assert(!x.isOneOf(Flags.Param | Flags.Mutable), "Only local immutable variable allowed") @@ -435,7 +457,7 @@ object Objects: else Env.resolveEnv(meth.owner.enclosingMethod, ref, summon[Env.Data]).getOrElse(Cold -> Env.NoEnv) - val env2 = Env.of(ddef, args.map(_.value), outerEnv) + val env2 = Env.of(ddef, args.map(_.value), State.currentObject, outerEnv) extendTrace(ddef) { given Env.Data = env2 eval(ddef.rhs, ref, cls, cacheResult = true) @@ -477,7 +499,7 @@ object Objects: val ddef = ctor.defTree.asInstanceOf[DefDef] val argValues = args.map(_.value) - given Env.Data = Env.of(ddef, argValues, Env.NoEnv) + given Env.Data = Env.of(ddef, argValues, State.currentObject, Env.NoEnv) if ctor.isPrimaryConstructor then val tpl = cls.defTree.asInstanceOf[TypeDef].rhs.asInstanceOf[Template] extendTrace(cls.defTree) { eval(tpl, ref, cls, cacheResult = true) } @@ -509,7 +531,7 @@ object Objects: if ref.owner == State.currentObject then Heap.read(ref, field) else - report.warning("Reading mutable state of " + ref.owner.show + " during initialization of " + State.currentObject + " is discouraged as it breaks initialization-time irrelevance. Calling trace: " + Trace.show, Trace.position) + errorReadOtherStaticObject(State.currentObject, ref.owner) Bottom end if else if ref.hasField(target) then @@ -602,7 +624,12 @@ object Objects: if sym.is(Flags.Mutable) then thisV match case ref: Ref => - Heap.readLocalVar(ref, env, sym) + if env.owner == State.currentObject then + Heap.readLocalVar(ref, env, sym) + else + errorReadOtherStaticObject(State.currentObject, env.owner) + Bottom + end if case _ => Cold else if sym.isPatternBound then @@ -645,7 +672,10 @@ object Objects: case Some(thisV -> env) => thisV match case ref: Ref => - Heap.writeLocalVar(ref, summon[Env.Data], sym, value) + if env.owner != State.currentObject then + errorMutateOtherStaticObject(State.currentObject, env.owner) + else + Heap.writeLocalVar(ref, summon[Env.Data], sym, value) case _ => report.warning("Assigning to variables in outer scope", Trace.position) @@ -670,7 +700,7 @@ object Objects: count += 1 given Trace = Trace.empty.add(classSym.defTree) - given Env.Data = Env.emptyEnv(tpl.constr.symbol) + given Env.Data = Env.emptyEnv(tpl.constr.symbol, classSym) given Heap.MutableData = Heap.empty() log("Iteration " + count) { @@ -1175,3 +1205,11 @@ object Objects: "Calling trace:\n" + Trace.show report.warning(msg, Trace.position) + + def errorReadOtherStaticObject(currentObj: ClassSymbol, otherObj: ClassSymbol)(using Trace, Context) = + val msg = + "Reading mutable state of " + otherObj.show + " during initialization of " + currentObj.show + ".\n" + + "Reading mutable state of other static objects is discouraged as it breaks initialization-time irrelevance." + + "Calling trace: " + Trace.show + + report.warning(msg, Trace.position) diff --git a/tests/init/neg/global-irrelevance3.scala b/tests/init/neg/global-irrelevance3.scala new file mode 100644 index 000000000000..58eab8f464d2 --- /dev/null +++ b/tests/init/neg/global-irrelevance3.scala @@ -0,0 +1,11 @@ +object A: + class Pair(val f: Int => Unit, val g: () => Int) + val p: Pair = foo() + + def foo(): Pair = + var x = 6 + new Pair(y => x = y, () => x) + + +object B: + var y = A.p.g() // error diff --git a/tests/init/neg/global-irrelevance4.scala b/tests/init/neg/global-irrelevance4.scala new file mode 100644 index 000000000000..c3563062acee --- /dev/null +++ b/tests/init/neg/global-irrelevance4.scala @@ -0,0 +1,11 @@ +object A: + class Pair(val f: Int => Unit, val g: () => Int) + val p: Pair = foo() + + def foo(): Pair = + var x = 6 + new Pair(y => x = y, () => x) + + +object B: + A.p.f(10) // error From 6d279606954636243907f94d641de38f2c48bda3 Mon Sep 17 00:00:00 2001 From: Fengyun Liu Date: Mon, 27 Feb 2023 06:44:34 +0100 Subject: [PATCH 077/113] Handle arguments for functions --- .../tools/dotc/transform/init/Objects.scala | 35 +++++++++++-------- .../tools/dotc/transform/init/Semantic.scala | 4 +-- .../tools/dotc/transform/init/Util.scala | 4 +-- tests/init/neg/global-fun.scala | 12 +++++++ tests/init/pos/global-fun.scala | 14 ++++++++ 5 files changed, 51 insertions(+), 18 deletions(-) create mode 100644 tests/init/neg/global-fun.scala create mode 100644 tests/init/pos/global-fun.scala diff --git a/compiler/src/dotty/tools/dotc/transform/init/Objects.scala b/compiler/src/dotty/tools/dotc/transform/init/Objects.scala index fb5fa7ffd4fc..02354210044b 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Objects.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Objects.scala @@ -119,8 +119,8 @@ object Objects: /** * Represents a lambda expression */ - case class Fun(expr: Tree, thisV: Value, klass: ClassSymbol, env: Env.Data) extends Value: - def show(using Context) = "Fun(" + expr.show + ", " + thisV.show + ", " + klass.show + ")" + case class Fun(code: Tree, thisV: Value, klass: ClassSymbol, env: Env.Data) extends Value: + def show(using Context) = "Fun(" + code.show + ", " + thisV.show + ", " + klass.show + ")" /** * Represents a set of values @@ -398,9 +398,9 @@ object Objects: case RefSet(refs) => refs.map(ref => ref.widen(height)).join - case Fun(expr, thisV, klass, env) => + case Fun(code, thisV, klass, env) => if height == 0 then Cold - else Fun(expr, thisV.widen(height), klass, env.widen(height)) + else Fun(code, thisV.widen(height), klass, env.widen(height)) case ref @ OfClass(klass, outer, init, args, env, owner) => if height == 0 then @@ -478,13 +478,20 @@ object Objects: // See tests/init/pos/Type.scala Bottom - case Fun(expr, thisV, klass, env) => + case Fun(code, thisV, klass, env) => // meth == NoSymbol for poly functions if meth.name.toString == "tupled" then value // a call like `fun.tupled` else - given Env.Data = env - eval(expr, thisV, klass, cacheResult = true) + code match + case ddef: DefDef => + given Env.Data = Env.of(ddef, args.map(_.value), State.currentObject, env) + extendTrace(code) { eval(ddef.rhs, thisV, klass, cacheResult = true) } + + case _ => + // by-name closure + given Env.Data = env + extendTrace(code) { eval(code, thisV, klass, cacheResult = true) } case RefSet(vs) => vs.map(v => call(v, meth, args, receiver, superType)).join @@ -550,7 +557,7 @@ object Objects: Bottom case fun: Fun => - report.error("[Internal error] unexpected tree in selecting a function, fun = " + fun.expr.show + Trace.show, fun.expr) + report.error("[Internal error] unexpected tree in selecting a function, fun = " + fun.code.show + Trace.show, fun.code) Bottom case Bottom => @@ -564,7 +571,7 @@ object Objects: def assign(receiver: Value, field: Symbol, rhs: Value, rhsTyp: Type): Contextual[Value] = log("Assign" + field.show + " of " + receiver.show + ", rhs = " + rhs.show, printer, (_: Value).show) { receiver match case fun: Fun => - report.error("[Internal error] unexpected tree in assignment, fun = " + fun.expr.show + Trace.show, Trace.position) + report.error("[Internal error] unexpected tree in assignment, fun = " + fun.code.show + Trace.show, Trace.position) case Cold => report.warning("Assigning to cold aliases is forbidden", Trace.position) @@ -588,7 +595,7 @@ object Objects: outer match case fun: Fun => - report.error("[Internal error] unexpected tree in instantiating a function, fun = " + fun.expr.show + Trace.show, Trace.position) + report.error("[Internal error] unexpected tree in instantiating a function, fun = " + fun.code.show + Trace.show, Trace.position) Bottom case value: (Bottom.type | ObjectRef | OfClass | Cold.type) => @@ -643,7 +650,7 @@ object Objects: value match case fun: Fun => given Env.Data = fun.env - eval(fun.expr, fun.thisV, fun.klass) + eval(fun.code, fun.thisV, fun.klass) case Cold => report.warning("Calling cold by-name alias. Call trace: \n" + Trace.show, Trace.position) Bottom @@ -873,10 +880,10 @@ object Objects: withTrace(trace2) { assign(receiver, lhs.symbol, value, rhs.tpe) } case closureDef(ddef) => - Fun(ddef.rhs, thisV, klass, summon[Env.Data]) + Fun(ddef, thisV, klass, summon[Env.Data]) - case PolyFun(body) => - Fun(body, thisV, klass, summon[Env.Data]) + case PolyFun(ddef) => + Fun(ddef, thisV, klass, summon[Env.Data]) case Block(stats, expr) => evalExprs(stats, thisV, klass) diff --git a/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala b/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala index 4548dccb598f..f04b4d59f72f 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala @@ -1300,8 +1300,8 @@ object Semantic: case closureDef(ddef) => Fun(ddef.rhs, thisV, klass) - case PolyFun(body) => - Fun(body, thisV, klass) + case PolyFun(ddef) => + Fun(ddef.rhs, thisV, klass) case Block(stats, expr) => eval(stats, thisV, klass) diff --git a/compiler/src/dotty/tools/dotc/transform/init/Util.scala b/compiler/src/dotty/tools/dotc/transform/init/Util.scala index 4e60c1325b09..57fd1bdb7a2d 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Util.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Util.scala @@ -64,14 +64,14 @@ object Util: case _ => None object PolyFun: - def unapply(tree: Tree)(using Context): Option[Tree] = + def unapply(tree: Tree)(using Context): Option[DefDef] = tree match case Block((cdef: TypeDef) :: Nil, Typed(NewExpr(tref, _, _, _), _)) if tref.symbol.isAnonymousClass && tref <:< defn.PolyFunctionType => val body = cdef.rhs.asInstanceOf[Template].body val apply = body.head.asInstanceOf[DefDef] - Some(apply.rhs) + Some(apply) case _ => None diff --git a/tests/init/neg/global-fun.scala b/tests/init/neg/global-fun.scala new file mode 100644 index 000000000000..1b750f1b05cc --- /dev/null +++ b/tests/init/neg/global-fun.scala @@ -0,0 +1,12 @@ +class C: + def double(x: Int): Int = x * 2 + +object A: + val f: C => Int = foo(10) + + def foo(x: Int): C => Int = + c => c.double(x) // error + + +object B: + var y = A.f(new C) diff --git a/tests/init/pos/global-fun.scala b/tests/init/pos/global-fun.scala new file mode 100644 index 000000000000..2a145a48aeff --- /dev/null +++ b/tests/init/pos/global-fun.scala @@ -0,0 +1,14 @@ +import scala.annotation.init + +class C: + def double(x: Int): Int = x * 2 + +object A: + val f: C => Int = foo(10) + + def foo(x: Int): C => Int = + c => c.double(x) // error + + +object B: + var y = A.f(new C: @init.expose) From 60ef6d1ef2f1b5485710ead1410e460ae4c6bb09 Mon Sep 17 00:00:00 2001 From: Fengyun Liu Date: Mon, 27 Feb 2023 07:51:20 +0100 Subject: [PATCH 078/113] Re use static objects: so that the fields hold correct values --- .../tools/dotc/transform/init/Objects.scala | 125 ++++++++++-------- tests/init/pos/global-fun.scala | 2 +- 2 files changed, 72 insertions(+), 55 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/transform/init/Objects.scala b/compiler/src/dotty/tools/dotc/transform/init/Objects.scala index 02354210044b..c9b9fffd1b0c 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Objects.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Objects.scala @@ -13,7 +13,7 @@ import NameKinds.SuperAccessorName import ast.tpd.* import config.Printers.init as printer import reporting.StoreReporter -import reporting.trace.force as log +import reporting.trace as log import Errors.* import Trace.* @@ -141,30 +141,76 @@ object Objects: object State: class Data: // objects under check - private[State] val checkingObjects = new mutable.ArrayBuffer[ClassSymbol] - private[State] val checkedObjects = new mutable.ArrayBuffer[ClassSymbol] + private[State] val checkingObjects = new mutable.ArrayBuffer[ObjectRef] + private[State] val checkedObjects = new mutable.ArrayBuffer[ObjectRef] private[State] val pendingTraces = new mutable.ArrayBuffer[Trace] end Data - def currentObject(using data: Data): ClassSymbol = data.checkingObjects.last - - def checkCycle(clazz: ClassSymbol)(work: => Unit)(using data: Data, ctx: Context, pendingTrace: Trace) = - val index = data.checkingObjects.indexOf(clazz) - - if index != -1 && data.checkingObjects.size - 1 > index then - val joinedTrace = data.pendingTraces.slice(index + 1, data.checkingObjects.size).foldLeft(pendingTrace) { (a, acc) => acc ++ a } - val callTrace = Trace.buildStacktrace(joinedTrace, "Calling trace:\n") - val cycle = data.checkingObjects.slice(index, data.checkingObjects.size) - val pos = clazz.defTree - report.warning("Cyclic initialization: " + cycle.map(_.show).mkString(" -> ") + " -> " + clazz.show + ". " + callTrace, pos) - else if index == -1 && data.checkedObjects.indexOf(clazz) == -1 then - data.pendingTraces += pendingTrace - data.checkingObjects += clazz - work - assert(data.checkingObjects.last == clazz, "Expect = " + clazz.show + ", found = " + data.checkingObjects.last) - data.pendingTraces.remove(data.pendingTraces.size - 1) - data.checkedObjects += data.checkingObjects.remove(data.checkingObjects.size - 1) - end checkCycle + def currentObject(using data: Data): ClassSymbol = data.checkingObjects.last.klass + + private def doCheckObject(classSym: ClassSymbol)(using ctx: Context, data: Data) = + val tpl = classSym.defTree.asInstanceOf[TypeDef].rhs.asInstanceOf[Template] + + var count = 0 + given Cache.Data = new Cache.Data + + @tailrec + def iterate()(using Context): ObjectRef = + count += 1 + + given Trace = Trace.empty.add(classSym.defTree) + given Env.Data = Env.emptyEnv(tpl.constr.symbol, classSym) + given Heap.MutableData = Heap.empty() + + val obj = ObjectRef(classSym) + log("Iteration " + count) { + data.checkingObjects += obj + init(tpl, obj, classSym) + assert(data.checkingObjects.last.klass == classSym, "Expect = " + classSym.show + ", found = " + data.checkingObjects.last.klass) + data.checkingObjects.remove(data.checkingObjects.size - 1) + } + + val hasError = ctx.reporter.pendingMessages.nonEmpty + if cache.hasChanged && !hasError then + cache.prepareForNextIteration() + iterate() + else + data.checkedObjects += obj + obj + end iterate + + val reporter = new StoreReporter(ctx.reporter) + val obj = iterate()(using ctx.fresh.setReporter(reporter)) + for warning <- reporter.pendingMessages do + ctx.reporter.report(warning) + + obj + end doCheckObject + + def checkObjectAccess(clazz: ClassSymbol)(using data: Data, ctx: Context, pendingTrace: Trace): Value = + val index = data.checkingObjects.indexOf(ObjectRef(clazz)) + + if index != -1 then + if data.checkingObjects.size - 1 > index then + // Only report errors for non-trivial cycles, ignore self cycles. + val joinedTrace = data.pendingTraces.slice(index + 1, data.checkingObjects.size).foldLeft(pendingTrace) { (a, acc) => acc ++ a } + val callTrace = Trace.buildStacktrace(joinedTrace, "Calling trace:\n") + val cycle = data.checkingObjects.slice(index, data.checkingObjects.size) + val pos = clazz.defTree + report.warning("Cyclic initialization: " + cycle.map(_.show).mkString(" -> ") + " -> " + clazz.show + ". " + callTrace, pos) + end if + data.checkingObjects(index) + else + val objOpt = data.checkedObjects.find(_.klass == clazz) + objOpt match + case Some(obj) => obj + + case None => + data.pendingTraces += pendingTrace + val obj = doCheckObject(clazz) + data.pendingTraces.remove(data.pendingTraces.size - 1) + obj + end checkObjectAccess end State /** Environment for parameters */ @@ -697,38 +743,9 @@ object Objects: /** Check an individual object */ private def accessObject(classSym: ClassSymbol)(using Context, State.Data, Trace): Value = log("accessing " + classSym.show, printer, (_: Value).show) { if classSym.hasSource then - val tpl = classSym.defTree.asInstanceOf[TypeDef].rhs.asInstanceOf[Template] - - var count = 0 - given Cache.Data = new Cache.Data - - @tailrec - def iterate()(using Context): Unit = - count += 1 - - given Trace = Trace.empty.add(classSym.defTree) - given Env.Data = Env.emptyEnv(tpl.constr.symbol, classSym) - given Heap.MutableData = Heap.empty() - - log("Iteration " + count) { - init(tpl, ObjectRef(classSym), classSym) - } - - val hasError = ctx.reporter.pendingMessages.nonEmpty - if cache.hasChanged && !hasError then - cache.prepareForNextIteration() - iterate() - end iterate - - State.checkCycle(classSym) { - val reporter = new StoreReporter(ctx.reporter) - iterate()(using ctx.fresh.setReporter(reporter)) - for warning <- reporter.pendingMessages do - ctx.reporter.report(warning) - } - end if - - ObjectRef(classSym) + State.checkObjectAccess(classSym) + else + ObjectRef(classSym) } diff --git a/tests/init/pos/global-fun.scala b/tests/init/pos/global-fun.scala index 2a145a48aeff..bf0e9c091101 100644 --- a/tests/init/pos/global-fun.scala +++ b/tests/init/pos/global-fun.scala @@ -7,7 +7,7 @@ object A: val f: C => Int = foo(10) def foo(x: Int): C => Int = - c => c.double(x) // error + c => c.double(x: @init.expose) object B: From 1a540ee5b80b1fb953eada1205e1959f42a41a14 Mon Sep 17 00:00:00 2001 From: Fengyun Liu Date: Mon, 27 Feb 2023 08:53:25 +0100 Subject: [PATCH 079/113] Constant folding drop anntations --- tests/init/pos/global-fun.scala | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/init/pos/global-fun.scala b/tests/init/pos/global-fun.scala index bf0e9c091101..31a09416df9b 100644 --- a/tests/init/pos/global-fun.scala +++ b/tests/init/pos/global-fun.scala @@ -4,7 +4,8 @@ class C: def double(x: Int): Int = x * 2 object A: - val f: C => Int = foo(10) + val n: Int = 10 + val f: C => Int = foo(n: @init.expose) def foo(x: Int): C => Int = c => c.double(x: @init.expose) From 0fa9779efea05774c19c52ca0ce62a1f916a300b Mon Sep 17 00:00:00 2001 From: Fengyun Liu Date: Mon, 27 Feb 2023 20:02:26 +0100 Subject: [PATCH 080/113] Simplify ownership tracking --- .../tools/dotc/transform/init/Objects.scala | 274 +++++++++--------- 1 file changed, 140 insertions(+), 134 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/transform/init/Objects.scala b/compiler/src/dotty/tools/dotc/transform/init/Objects.scala index c9b9fffd1b0c..59113dd1dbc7 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Objects.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Objects.scala @@ -72,27 +72,37 @@ object Objects: * A reference caches the values for outers and immutable fields. */ sealed abstract class Ref extends Value: - private val fields: mutable.Map[Symbol, Value] = mutable.Map.empty + private val vals: mutable.Map[Symbol, Value] = mutable.Map.empty + private val vars: mutable.Map[Symbol, Heap.Addr] = mutable.Map.empty private val outers: mutable.Map[ClassSymbol, Value] = mutable.Map.empty - def owner: ClassSymbol - def klass: ClassSymbol - def fieldValue(sym: Symbol): Value = fields(sym) + def valValue(sym: Symbol): Value = vals(sym) + + def varAddr(sym: Symbol): Heap.Addr = vars(sym) def outerValue(cls: ClassSymbol): Value = outers(cls) - def hasField(sym: Symbol): Boolean = fields.contains(sym) + def hasVal(sym: Symbol): Boolean = vals.contains(sym) + + def hasVar(sym: Symbol): Boolean = vars.contains(sym) def hasOuter(cls: ClassSymbol): Boolean = outers.contains(cls) - def updateField(field: Symbol, value: Value)(using Context) = log("Update field " + field + " = " + value + " for " + this, printer) { - assert(!fields.contains(field), "Field already set " + field) - fields(field) = value + def initVal(field: Symbol, value: Value)(using Context) = log("Initialize " + field.show + " = " + value + " for " + this, printer) { + assert(!field.is(Flags.Mutable), "Field is mutable: " + field.show) + assert(!vals.contains(field), "Field already set " + field.show) + vals(field) = value } - def updateOuter(cls: ClassSymbol, value: Value)(using Context) = log("Update outer " + cls + " = " + value + " for " + this, printer) { + def initVar(field: Symbol, addr: Heap.Addr)(using Context) = log("Initialize " + field.show + " = " + addr + " for " + this, printer) { + assert(field.is(Flags.Mutable), "Field is not mutable: " + field.show) + assert(!vars.contains(field), "Field already set: " + field.show) + vars(field) = addr + } + + def initOuter(cls: ClassSymbol, value: Value)(using Context) = log("Initialize outer " + cls.show + " = " + value + " for " + this, printer) { assert(!outers.contains(cls), "Outer already set " + cls) outers(cls) = value } @@ -107,13 +117,13 @@ object Objects: * Rerepsents values that are instances of the specified class * */ - case class OfClass private(klass: ClassSymbol, outer: Value, ctor: Symbol, args: List[Value], env: Env.Data, owner: ClassSymbol) extends Ref: + case class OfClass private(klass: ClassSymbol, outer: Value, ctor: Symbol, args: List[Value], env: Env.Data) extends Ref: def show(using Context) = "OfClass(" + klass.show + ", outer = " + outer + ", args = " + args.map(_.show) + ")" object OfClass: - def apply(klass: ClassSymbol, outer: Value, ctor: Symbol, args: List[Value], env: Env.Data, owner: ClassSymbol)(using Context): OfClass = - val instance = new OfClass(klass, outer, ctor, args, env, owner) - instance.updateOuter(klass, outer) + def apply(klass: ClassSymbol, outer: Value, ctor: Symbol, args: List[Value], env: Env.Data)(using Context): OfClass = + val instance = new OfClass(klass, outer, ctor, args, env) + instance.initOuter(klass, outer) instance /** @@ -159,7 +169,7 @@ object Objects: count += 1 given Trace = Trace.empty.add(classSym.defTree) - given Env.Data = Env.emptyEnv(tpl.constr.symbol, classSym) + given Env.Data = Env.emptyEnv(tpl.constr.symbol) given Heap.MutableData = Heap.empty() val obj = ObjectRef(classSym) @@ -216,8 +226,8 @@ object Objects: /** Environment for parameters */ object Env: abstract class Data: - private[Env] def get(x: Symbol)(using Context): Option[Value] - private[Env] def contains(x: Symbol): Boolean + private[Env] def getVal(x: Symbol)(using Context): Option[Value] + private[Env] def getVar(x: Symbol)(using Context): Option[Heap.Addr] def widen(height: Int)(using Context): Data @@ -225,52 +235,36 @@ object Objects: def show(using Context): String - def owner: ClassSymbol - /** Local environments can be deeply nested, therefore we need `outer`. * * For local variables in rhs of class field definitions, the `meth` is the primary constructor. * - * We need to record the `owner` of a local environment, which is the static object which - * creates the environment, as a local environment may contain mutable vars. To enforce - * initialization-time irrelevance, we need to make sure that when the environment is captured - * in lambdas or local classes, it is impossible to read/write the captured mutable vars from a - * different object other than the object which creates and owns it. - * - * An environment and its outer environment might have different owners. For example: - * - * object A: - * def foo(): Int => Int = - * var n = 0 - * x => { n += 1; x + n } - * val f: Int => Int = foo() - * - * object B: - * val m = A.f(10) - * */ - private case class LocalEnv(private[Env] val params: Map[Symbol, Value], meth: Symbol, owner: ClassSymbol, outer: Data)(using Context) extends Data: + private case class LocalEnv(private[Env] val params: Map[Symbol, Value], meth: Symbol, outer: Data)(using Context) extends Data: val level = outer.level + 1 if (level > 3) report.warning("[Internal error] Deeply nested environemnt, level = " + level + ", " + meth.show + " in " + meth.enclosingClass.show, meth.defTree) - private[Env] val locals: mutable.Map[Symbol, Value] = mutable.Map.empty + private[Env] val vals: mutable.Map[Symbol, Value] = mutable.Map.empty + private[Env] val vars: mutable.Map[Symbol, Heap.Addr] = mutable.Map.empty - private[Env] def get(x: Symbol)(using Context): Option[Value] = + private[Env] def getVal(x: Symbol)(using Context): Option[Value] = if x.is(Flags.Param) then params.get(x) - else locals.get(x) + else vals.get(x) - private[Env] def contains(x: Symbol): Boolean = - params.contains(x) || locals.contains(x) + private[Env] def getVar(x: Symbol)(using Context): Option[Heap.Addr] = + vars.get(x) def widen(height: Int)(using Context): Data = - new LocalEnv(params.map(_ -> _.widen(height)), meth, owner, outer.widen(height)) + // TODO: locals do not exist for the widened environment + new LocalEnv(params.map(_ -> _.widen(height)), meth, outer.widen(height)) def show(using Context) = "owner: " + meth.show + "\n" + "params: " + params.map(_.show + " ->" + _.show).mkString("{", ", ", "}") + "\n" + - "locals: " + locals.map(_.show + " ->" + _.show).mkString("{", ", ", "}") + "\n" + + "vals: " + vals.map(_.show + " ->" + _.show).mkString("{", ", ", "}") + "\n" + + "vars: " + vars.map(_.show + " ->" + _).mkString("{", ", ", "}") + "\n" + "outer = {\n" + outer.show + "\n}" end LocalEnv @@ -278,41 +272,53 @@ object Objects: object NoEnv extends Data: val level = 0 - private[Env] def get(x: Symbol)(using Context): Option[Value] = + private[Env] def getVal(x: Symbol)(using Context): Option[Value] = throw new RuntimeException("Invalid usage of non-existent env") - private[Env] def contains(x: Symbol): Boolean = + private[Env] def getVar(x: Symbol)(using Context): Option[Heap.Addr] = throw new RuntimeException("Invalid usage of non-existent env") def widen(height: Int)(using Context): Data = this def show(using Context): String = "NoEnv" - - def owner = throw new RuntimeException("Invalid usage of non-existent env") end NoEnv /** An empty environment can be used for non-method environments, e.g., field initializers. + * + * The owner for the local environment for field initializers is the primary constructor of the + * enclosing class. */ - def emptyEnv(meth: Symbol, owner: ClassSymbol)(using Context): Data = new LocalEnv(Map.empty, meth, owner, NoEnv) + def emptyEnv(meth: Symbol)(using Context): Data = new LocalEnv(Map.empty, meth, NoEnv) + + def valValue(x: Symbol)(using data: Data, ctx: Context): Value = data.getVal(x).get - def apply(x: Symbol)(using data: Data, ctx: Context): Value = data.get(x).get + def varAddr(x: Symbol)(using data: Data, ctx: Context): Heap.Addr = data.getVar(x).get - def get(x: Symbol)(using data: Data, ctx: Context): Option[Value] = data.get(x) + def getVal(x: Symbol)(using data: Data, ctx: Context): Option[Value] = data.getVal(x) - def contains(x: Symbol)(using data: Data): Boolean = data.contains(x) + def getVar(x: Symbol)(using data: Data, ctx: Context): Option[Heap.Addr] = data.getVar(x) - def of(ddef: DefDef, args: List[Value], owner: ClassSymbol, outer: Data)(using Context): Data = + def of(ddef: DefDef, args: List[Value], outer: Data)(using Context): Data = val params = ddef.termParamss.flatten.map(_.symbol) assert(args.size == params.size, "arguments = " + args.size + ", params = " + params.size) assert(ddef.symbol.owner.isClass ^ (outer != NoEnv), "ddef.owner = " + ddef.symbol.owner.show + ", outer = " + outer + ", " + ddef.source) - new LocalEnv(params.zip(args).toMap, ddef.symbol, owner, outer) + new LocalEnv(params.zip(args).toMap, ddef.symbol, outer) def setLocalVal(x: Symbol, value: Value)(using data: Data, ctx: Context): Unit = assert(!x.isOneOf(Flags.Param | Flags.Mutable), "Only local immutable variable allowed") data match case localEnv: LocalEnv => - assert(!localEnv.locals.contains(x), "Already initialized local " + x.show) - localEnv.locals(x) = value + assert(!localEnv.vals.contains(x), "Already initialized local " + x.show) + localEnv.vals(x) = value + case _ => + throw new RuntimeException("Incorrect local environment for initializing " + x.show) + + def setLocalVar(x: Symbol, addr: Heap.Addr)(using data: Data, ctx: Context): Unit = + assert(x.is(Flags.Mutable, butNot = Flags.Param), "Only local mutable variable allowed") + data match + case localEnv: LocalEnv => + assert(!localEnv.vars.contains(x), "Already initialized local " + x.show) + localEnv.vars(x) = addr case _ => throw new RuntimeException("Incorrect local environment for initializing " + x.show) @@ -349,13 +355,15 @@ object Objects: /** Abstract heap for mutable fields */ object Heap: - private abstract class Addr + abstract class Addr: + /** The static object which owns the mutable slot */ + def owner: ClassSymbol /** The address for mutable fields of objects. */ - private case class FieldAddr(ref: Ref, field: Symbol) extends Addr + private case class FieldAddr(ref: Ref, field: Symbol, owner: ClassSymbol) extends Addr /** The address for mutable local variables . */ - private case class LocalVarAddr(ref: Ref, env: Env.Data, sym: Symbol) extends Addr + private case class LocalVarAddr(ref: Ref, env: Env.Data, sym: Symbol, owner: ClassSymbol) extends Addr /** Immutable heap data used in the cache. * @@ -380,22 +388,20 @@ object Objects: def empty(): MutableData = new MutableData(Map.empty) - def contains(ref: Ref, field: Symbol)(using mutable: MutableData): Boolean = - mutable.heap.contains(FieldAddr(ref, field)) + def contains(addr: Addr)(using mutable: MutableData): Boolean = + mutable.heap.contains(addr) - def read(ref: Ref, field: Symbol)(using mutable: MutableData): Value = - mutable.heap(FieldAddr(ref, field)) + def read(addr: Addr)(using mutable: MutableData): Value = + mutable.heap(addr) - def write(ref: Ref, field: Symbol, value: Value)(using mutable: MutableData): Unit = - val addr = FieldAddr(ref, field) + def write(addr: Addr, value: Value)(using mutable: MutableData): Unit = mutable.update(addr, value) - def readLocalVar(ref: Ref, env: Env.Data, sym: Symbol)(using mutable: MutableData): Value = - mutable.heap(LocalVarAddr(ref, env, sym)) + def localVarAddr(ref: Ref, env: Env.Data, sym: Symbol, owner: ClassSymbol): Addr = + LocalVarAddr(ref, env, sym, owner) - def writeLocalVar(ref: Ref, env: Env.Data, sym: Symbol, value: Value)(using mutable: MutableData): Unit = - val addr = LocalVarAddr(ref, env, sym) - mutable.update(addr, value) + def fieldVarAddr(ref: Ref, sym: Symbol, owner: ClassSymbol): Addr = + FieldAddr(ref, sym, owner) def getHeapData()(using mutable: MutableData): Data = mutable.heap @@ -448,14 +454,14 @@ object Objects: if height == 0 then Cold else Fun(code, thisV.widen(height), klass, env.widen(height)) - case ref @ OfClass(klass, outer, init, args, env, owner) => + case ref @ OfClass(klass, outer, init, args, env) => if height == 0 then Cold else val outer2 = outer.widen(height - 1) val args2 = args.map(_.widen(height - 1)) val env2 = env.widen(height - 1) - OfClass(klass, outer2, init, args2, env2, owner) + OfClass(klass, outer2, init, args2, env2) case _ => a @@ -487,34 +493,25 @@ object Objects: if target.isOneOf(Flags.Method) then if target.hasSource then - ref match - case obj: ObjectRef if obj.klass != State.currentObject && target.isOneOf(Flags.Mutable) => - errorMutateOtherStaticObject(State.currentObject, obj.klass) - Bottom + val cls = target.owner.enclosingClass.asClass + val ddef = target.defTree.asInstanceOf[DefDef] + val meth = ddef.symbol - case _ => - val cls = target.owner.enclosingClass.asClass - val ddef = target.defTree.asInstanceOf[DefDef] - val meth = ddef.symbol - - val (thisV, outerEnv) = - if meth.owner.isClass then - (ref, Env.NoEnv) - else - Env.resolveEnv(meth.owner.enclosingMethod, ref, summon[Env.Data]).getOrElse(Cold -> Env.NoEnv) - - val env2 = Env.of(ddef, args.map(_.value), State.currentObject, outerEnv) - extendTrace(ddef) { - given Env.Data = env2 - eval(ddef.rhs, ref, cls, cacheResult = true) - } + val (thisV, outerEnv) = + if meth.owner.isClass then + (ref, Env.NoEnv) + else + Env.resolveEnv(meth.owner.enclosingMethod, ref, summon[Env.Data]).getOrElse(Cold -> Env.NoEnv) + + val env2 = Env.of(ddef, args.map(_.value), outerEnv) + extendTrace(ddef) { + given Env.Data = env2 + eval(ddef.rhs, ref, cls, cacheResult = true) + } else Bottom else if target.exists then - if ref.hasField(target) then - ref.fieldValue(target) - else - select(ref, target, receiver, needResolve = false) + select(ref, target, receiver, needResolve = false) else if ref.klass.isSubClass(receiver.widenSingleton.classSymbol) then report.error("[Internal error] Unexpected resolution failure: ref.klass = " + ref.klass.show + ", meth = " + meth.show + Trace.show, Trace.position) @@ -531,7 +528,7 @@ object Objects: else code match case ddef: DefDef => - given Env.Data = Env.of(ddef, args.map(_.value), State.currentObject, env) + given Env.Data = Env.of(ddef, args.map(_.value), env) extendTrace(code) { eval(ddef.rhs, thisV, klass, cacheResult = true) } case _ => @@ -552,7 +549,7 @@ object Objects: val ddef = ctor.defTree.asInstanceOf[DefDef] val argValues = args.map(_.value) - given Env.Data = Env.of(ddef, argValues, State.currentObject, Env.NoEnv) + given Env.Data = Env.of(ddef, argValues, Env.NoEnv) if ctor.isPrimaryConstructor then val tpl = cls.defTree.asInstanceOf[TypeDef].rhs.asInstanceOf[Template] extendTrace(cls.defTree) { eval(tpl, ref, cls, cacheResult = true) } @@ -577,18 +574,23 @@ object Objects: case ref: Ref => val target = if needResolve then resolve(ref.klass, field) else field if target.is(Flags.Lazy) then + given Env.Data = Env.emptyEnv(target.owner.asInstanceOf[ClassSymbol].primaryConstructor) val rhs = target.defTree.asInstanceOf[ValDef].rhs eval(rhs, ref, target.owner.asClass, cacheResult = true) else if target.exists then if target.isOneOf(Flags.Mutable) then - if ref.owner == State.currentObject then - Heap.read(ref, field) + if ref.hasVar(target) then + val addr = ref.varAddr(target) + if addr.owner == State.currentObject then + Heap.read(addr) + else + errorReadOtherStaticObject(State.currentObject, addr.owner) + Bottom else - errorReadOtherStaticObject(State.currentObject, ref.owner) + // initialization error, reported by the initialization checker Bottom - end if - else if ref.hasField(target) then - ref.fieldValue(target) + else if ref.hasVal(target) then + ref.valValue(target) else // initialization error, reported by the initialization checker Bottom @@ -628,10 +630,14 @@ object Objects: refs.foreach(ref => assign(ref, field, rhs, rhsTyp)) case ref: Ref => - if ref.owner != State.currentObject then - errorMutateOtherStaticObject(State.currentObject, ref.owner) + if ref.hasVar(field) then + val addr = ref.varAddr(field) + if addr.owner != State.currentObject then + errorMutateOtherStaticObject(State.currentObject, addr.owner) + else + Heap.write(addr, rhs) else - Heap.write(ref, field, rhs) + report.warning("Mutating a field before its initialization: " + field.show, Trace.position) end match Bottom @@ -655,7 +661,7 @@ object Objects: // klass.enclosingMethod returns its primary constructor Env.resolveEnv(klass.owner.enclosingMethod, outer, summon[Env.Data]).getOrElse(Cold -> Env.NoEnv) - val instance = OfClass(klass, outerWidened, ctor, args.map(_.value), envWidened, State.currentObject) + val instance = OfClass(klass, outerWidened, ctor, args.map(_.value), envWidened) callConstructor(instance, ctor, args) instance @@ -665,8 +671,9 @@ object Objects: def initLocal(ref: Ref, sym: Symbol, value: Value): Contextual[Unit] = log("initialize local " + sym.show + " with " + value.show, printer) { if sym.is(Flags.Mutable) then - val env = summon[Env.Data] - Heap.writeLocalVar(ref, env, sym, value) + val addr = Heap.localVarAddr(ref, summon[Env.Data], sym, State.currentObject) + Env.setLocalVar(sym, addr) + Heap.write(addr, value) else Env.setLocalVal(sym, value) } @@ -675,23 +682,22 @@ object Objects: Env.resolveEnv(sym.enclosingMethod, thisV, summon[Env.Data]) match case Some(thisV -> env) => if sym.is(Flags.Mutable) then - thisV match - case ref: Ref => - if env.owner == State.currentObject then - Heap.readLocalVar(ref, env, sym) - else - errorReadOtherStaticObject(State.currentObject, env.owner) - Bottom - end if - case _ => - Cold + // Assume forward reference check is doing a good job + val addr = Env.varAddr(sym) + if addr.owner == State.currentObject then + Heap.read(addr) + else + errorReadOtherStaticObject(State.currentObject, addr.owner) + Bottom + end if else if sym.isPatternBound then // TODO: handle patterns Cold else given Env.Data = env try - val value = Env(sym) + // Assume forward reference check is doing a good job + val value = Env.valValue(sym) if sym.is(Flags.Param) && sym.info.isInstanceOf[ExprType] then value match case fun: Fun => @@ -723,14 +729,11 @@ object Objects: assert(sym.is(Flags.Mutable), "Writing to immutable variable " + sym.show) Env.resolveEnv(sym.enclosingMethod, thisV, summon[Env.Data]) match case Some(thisV -> env) => - thisV match - case ref: Ref => - if env.owner != State.currentObject then - errorMutateOtherStaticObject(State.currentObject, env.owner) - else - Heap.writeLocalVar(ref, summon[Env.Data], sym, value) - case _ => - report.warning("Assigning to variables in outer scope", Trace.position) + val addr = Env.varAddr(sym) + if addr.owner != State.currentObject then + errorMutateOtherStaticObject(State.currentObject, addr.owner) + else + Heap.write(addr, value) case _ => report.warning("Assigning to variables in outer scope", Trace.position) @@ -1059,13 +1062,13 @@ object Objects: */ def init(tpl: Template, thisV: Ref, klass: ClassSymbol): Contextual[Value] = log("init " + klass.show, printer, (_: Value).show) { val paramsMap = tpl.constr.termParamss.flatten.map { vdef => - vdef.name -> Env(vdef.symbol) + vdef.name -> Env.valValue(vdef.symbol) }.toMap // init param fields klass.paramGetters.foreach { acc => val value = paramsMap(acc.name.toTermName) - thisV.updateField(acc, value) + thisV.initVal(acc, value) printer.println(acc.show + " initialized with " + value) } @@ -1076,7 +1079,7 @@ object Objects: val cls = tref.classSymbol.asClass // update outer for super class val res = outerValue(tref, thisV, klass) - thisV.updateOuter(cls, res) + thisV.initOuter(cls, res) // follow constructor if cls.hasSource then @@ -1154,10 +1157,13 @@ object Objects: tpl.body.foreach { case vdef : ValDef if !vdef.symbol.is(Flags.Lazy) && !vdef.rhs.isEmpty => val res = eval(vdef.rhs, thisV, klass) - if vdef.symbol.is(Flags.Mutable) then - Heap.write(thisV, vdef.symbol, res) + val sym = vdef.symbol + if sym.is(Flags.Mutable) then + val addr = Heap.fieldVarAddr(thisV, sym, State.currentObject) + thisV.initVar(sym, addr) + Heap.write(addr, res) else - thisV.updateField(vdef.symbol, res) + thisV.initVal(sym, res) case _: MemberDef => From 19a88def6486f522fc128264b734e0abede7f1a3 Mon Sep 17 00:00:00 2001 From: Fengyun Liu Date: Mon, 27 Feb 2023 20:02:30 +0100 Subject: [PATCH 081/113] Fix error message printing --- compiler/src/dotty/tools/dotc/transform/init/Objects.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/compiler/src/dotty/tools/dotc/transform/init/Objects.scala b/compiler/src/dotty/tools/dotc/transform/init/Objects.scala index 59113dd1dbc7..ba714a148f5a 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Objects.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Objects.scala @@ -207,7 +207,7 @@ object Objects: val callTrace = Trace.buildStacktrace(joinedTrace, "Calling trace:\n") val cycle = data.checkingObjects.slice(index, data.checkingObjects.size) val pos = clazz.defTree - report.warning("Cyclic initialization: " + cycle.map(_.show).mkString(" -> ") + " -> " + clazz.show + ". " + callTrace, pos) + report.warning("Cyclic initialization: " + cycle.map(_.klass.show).mkString(" -> ") + " -> " + clazz.show + ". " + callTrace, pos) end if data.checkingObjects(index) else From fb1f0c2e61fa5d40c5ab55ff187cca854cbbb79d Mon Sep 17 00:00:00 2001 From: Fengyun Liu Date: Mon, 27 Feb 2023 20:02:35 +0100 Subject: [PATCH 082/113] Handle immutable body values in widening --- .../tools/dotc/transform/init/Objects.scala | 59 +++++++++++++------ 1 file changed, 41 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 ba714a148f5a..c7e3b4a34e20 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Objects.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Objects.scala @@ -71,10 +71,14 @@ object Objects: /** * A reference caches the values for outers and immutable fields. */ - sealed abstract class Ref extends Value: - private val vals: mutable.Map[Symbol, Value] = mutable.Map.empty - private val vars: mutable.Map[Symbol, Heap.Addr] = mutable.Map.empty - private val outers: mutable.Map[ClassSymbol, Value] = mutable.Map.empty + sealed abstract class Ref( + valsMap: mutable.Map[Symbol, Value], + varsMap: mutable.Map[Symbol, Heap.Addr], + outersMap: mutable.Map[ClassSymbol, Value]) + extends Value: + protected val vals: mutable.Map[Symbol, Value] = valsMap + protected val vars: mutable.Map[Symbol, Heap.Addr] = varsMap + protected val outers: mutable.Map[ClassSymbol, Value] = outersMap def klass: ClassSymbol @@ -108,7 +112,8 @@ object Objects: } /** A reference to a static object */ - case class ObjectRef(klass: ClassSymbol) extends Ref: + case class ObjectRef(klass: ClassSymbol) + extends Ref(valsMap = mutable.Map.empty, varsMap = mutable.Map.empty, outersMap = mutable.Map.empty): val owner = klass def show(using Context) = "ObjectRef(" + klass.show + ")" @@ -117,12 +122,26 @@ object Objects: * Rerepsents values that are instances of the specified class * */ - case class OfClass private(klass: ClassSymbol, outer: Value, ctor: Symbol, args: List[Value], env: Env.Data) extends Ref: - def show(using Context) = "OfClass(" + klass.show + ", outer = " + outer + ", args = " + args.map(_.show) + ")" + case class OfClass private (klass: ClassSymbol, outer: Value, ctor: Symbol, args: List[Value], env: Env.Data) + (valsMap: mutable.Map[Symbol, Value], varsMap: mutable.Map[Symbol, Heap.Addr], outersMap: mutable.Map[ClassSymbol, Value]) + extends Ref(valsMap, varsMap, outersMap): + + def widenedCopy(outer: Value, args: List[Value], env: Env.Data): OfClass = + new OfClass(klass, outer, ctor, args, env)(this.valsMap, this.varsMap, outersMap) + + def show(using Context) = + val valFields = vals.map(_.show + " -> " + _.show) + "OfClass(" + klass.show + ", outer = " + outer + ", args = " + args.map(_.show) + ", vals = " + valFields + ")" object OfClass: - def apply(klass: ClassSymbol, outer: Value, ctor: Symbol, args: List[Value], env: Env.Data)(using Context): OfClass = - val instance = new OfClass(klass, outer, ctor, args, env) + def apply( + klass: ClassSymbol, outer: Value, ctor: Symbol, args: List[Value], env: Env.Data)( + using Context + ): OfClass = + val instance = + new OfClass(klass, outer, ctor, args, env)( + valsMap = mutable.Map.empty, varsMap = mutable.Map.empty, outersMap = mutable.Map.empty + ) instance.initOuter(klass, outer) instance @@ -240,14 +259,18 @@ object Objects: * For local variables in rhs of class field definitions, the `meth` is the primary constructor. * */ - private case class LocalEnv(private[Env] val params: Map[Symbol, Value], meth: Symbol, outer: Data)(using Context) extends Data: + private case class LocalEnv + (private[Env] val params: Map[Symbol, Value], meth: Symbol, outer: Data) + (valsMap: mutable.Map[Symbol, Value], varsMap: mutable.Map[Symbol, Heap.Addr]) + (using Context) + extends Data: val level = outer.level + 1 if (level > 3) report.warning("[Internal error] Deeply nested environemnt, level = " + level + ", " + meth.show + " in " + meth.enclosingClass.show, meth.defTree) - private[Env] val vals: mutable.Map[Symbol, Value] = mutable.Map.empty - private[Env] val vars: mutable.Map[Symbol, Heap.Addr] = mutable.Map.empty + private[Env] val vals: mutable.Map[Symbol, Value] = valsMap + private[Env] val vars: mutable.Map[Symbol, Heap.Addr] = varsMap private[Env] def getVal(x: Symbol)(using Context): Option[Value] = if x.is(Flags.Param) then params.get(x) @@ -257,8 +280,7 @@ object Objects: vars.get(x) def widen(height: Int)(using Context): Data = - // TODO: locals do not exist for the widened environment - new LocalEnv(params.map(_ -> _.widen(height)), meth, outer.widen(height)) + new LocalEnv(params.map(_ -> _.widen(height)), meth, outer.widen(height))(this.vals, this.vars) def show(using Context) = "owner: " + meth.show + "\n" + @@ -288,7 +310,8 @@ object Objects: * The owner for the local environment for field initializers is the primary constructor of the * enclosing class. */ - def emptyEnv(meth: Symbol)(using Context): Data = new LocalEnv(Map.empty, meth, NoEnv) + def emptyEnv(meth: Symbol)(using Context): Data = + new LocalEnv(Map.empty, meth, NoEnv)(valsMap = mutable.Map.empty, varsMap = mutable.Map.empty) def valValue(x: Symbol)(using data: Data, ctx: Context): Value = data.getVal(x).get @@ -302,7 +325,7 @@ object Objects: val params = ddef.termParamss.flatten.map(_.symbol) assert(args.size == params.size, "arguments = " + args.size + ", params = " + params.size) assert(ddef.symbol.owner.isClass ^ (outer != NoEnv), "ddef.owner = " + ddef.symbol.owner.show + ", outer = " + outer + ", " + ddef.source) - new LocalEnv(params.zip(args).toMap, ddef.symbol, outer) + new LocalEnv(params.zip(args).toMap, ddef.symbol, outer)(valsMap = mutable.Map.empty, varsMap = mutable.Map.empty) def setLocalVal(x: Symbol, value: Value)(using data: Data, ctx: Context): Unit = assert(!x.isOneOf(Flags.Param | Flags.Mutable), "Only local immutable variable allowed") @@ -461,7 +484,7 @@ object Objects: val outer2 = outer.widen(height - 1) val args2 = args.map(_.widen(height - 1)) val env2 = env.widen(height - 1) - OfClass(klass, outer2, init, args2, env2) + ref.widenedCopy(outer2, args2, env2) case _ => a @@ -565,7 +588,7 @@ object Objects: Bottom } - def select(thisV: Value, field: Symbol, receiver: Type, needResolve: Boolean = true): Contextual[Value] = log("select " + field.show + ", this = " + thisV, printer, (_: Value).show) { + def select(thisV: Value, field: Symbol, receiver: Type, needResolve: Boolean = true): Contextual[Value] = log("select " + field.show + ", this = " + thisV.show, printer, (_: Value).show) { thisV match case Cold => report.warning("Using cold alias", Trace.position) From d273c0fe5e237ba7b7fd9cf64c4cf798c502f9fb Mon Sep 17 00:00:00 2001 From: Fengyun Liu Date: Mon, 27 Feb 2023 20:02:39 +0100 Subject: [PATCH 083/113] Fix environment for reading local vars --- .../src/dotty/tools/dotc/transform/init/Objects.scala | 3 ++- tests/init/neg/global-irrelevance3.scala | 9 +++++++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/transform/init/Objects.scala b/compiler/src/dotty/tools/dotc/transform/init/Objects.scala index c7e3b4a34e20..0a2098d3e885 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Objects.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Objects.scala @@ -706,6 +706,7 @@ object Objects: case Some(thisV -> env) => if sym.is(Flags.Mutable) then // Assume forward reference check is doing a good job + given Env.Data = env val addr = Env.varAddr(sym) if addr.owner == State.currentObject then Heap.read(addr) @@ -1262,7 +1263,7 @@ object Objects: def errorReadOtherStaticObject(currentObj: ClassSymbol, otherObj: ClassSymbol)(using Trace, Context) = val msg = "Reading mutable state of " + otherObj.show + " during initialization of " + currentObj.show + ".\n" + - "Reading mutable state of other static objects is discouraged as it breaks initialization-time irrelevance." + + "Reading mutable state of other static objects is discouraged as it breaks initialization-time irrelevance. " + "Calling trace: " + Trace.show report.warning(msg, Trace.position) diff --git a/tests/init/neg/global-irrelevance3.scala b/tests/init/neg/global-irrelevance3.scala index 58eab8f464d2..beb2d62ea778 100644 --- a/tests/init/neg/global-irrelevance3.scala +++ b/tests/init/neg/global-irrelevance3.scala @@ -1,11 +1,16 @@ +import scala.annotation.init + object A: class Pair(val f: Int => Unit, val g: () => Int) val p: Pair = foo() def foo(): Pair = var x = 6 - new Pair(y => x = y, () => x) + new Pair( + y => x = y, + (() => x): @init.expose // error + ) object B: - var y = A.p.g() // error + var y = A.p.g() From 9d9bcb15f90cd1cc1266db15fd0fe9aac769670f Mon Sep 17 00:00:00 2001 From: Fengyun Liu Date: Mon, 27 Feb 2023 20:02:43 +0100 Subject: [PATCH 084/113] Use correct env for writing local vars --- .../src/dotty/tools/dotc/transform/init/Objects.scala | 8 +++++++- tests/init/neg/global-irrelevance4.scala | 10 +++++++--- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/transform/init/Objects.scala b/compiler/src/dotty/tools/dotc/transform/init/Objects.scala index 0a2098d3e885..77870c699cd2 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Objects.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Objects.scala @@ -753,6 +753,7 @@ object Objects: assert(sym.is(Flags.Mutable), "Writing to immutable variable " + sym.show) Env.resolveEnv(sym.enclosingMethod, thisV, summon[Env.Data]) match case Some(thisV -> env) => + given Env.Data = env val addr = Env.varAddr(sym) if addr.owner != State.currentObject then errorMutateOtherStaticObject(State.currentObject, addr.owner) @@ -1092,7 +1093,12 @@ object Objects: // init param fields klass.paramGetters.foreach { acc => val value = paramsMap(acc.name.toTermName) - thisV.initVal(acc, value) + if acc.is(Flags.Mutable) then + val addr = Heap.fieldVarAddr(thisV, acc, State.currentObject) + thisV.initVar(acc, addr) + Heap.write(addr, value) + else + thisV.initVal(acc, value) printer.println(acc.show + " initialized with " + value) } diff --git a/tests/init/neg/global-irrelevance4.scala b/tests/init/neg/global-irrelevance4.scala index c3563062acee..d5c0e6c64497 100644 --- a/tests/init/neg/global-irrelevance4.scala +++ b/tests/init/neg/global-irrelevance4.scala @@ -1,11 +1,15 @@ +import scala.annotation.init + object A: class Pair(val f: Int => Unit, val g: () => Int) val p: Pair = foo() def foo(): Pair = var x = 6 - new Pair(y => x = y, () => x) - + new Pair( + (y => x = y): @init.expose, // error + () => x + ) object B: - A.p.f(10) // error + A.p.f(10) From 58fce3992d59774d3cfab3377eb89620ed41ff24 Mon Sep 17 00:00:00 2001 From: Fengyun Liu Date: Sun, 5 Mar 2023 09:16:21 +0100 Subject: [PATCH 085/113] Primitive support for arrays --- .../tools/dotc/transform/init/Objects.scala | 83 ++++++++++++++----- tests/init/neg/global-irrelevance5.scala | 6 ++ 2 files changed, 70 insertions(+), 19 deletions(-) create mode 100644 tests/init/neg/global-irrelevance5.scala diff --git a/compiler/src/dotty/tools/dotc/transform/init/Objects.scala b/compiler/src/dotty/tools/dotc/transform/init/Objects.scala index 77870c699cd2..54a34910cd85 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Objects.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Objects.scala @@ -22,6 +22,7 @@ import Util.* import scala.collection.immutable.ListSet import scala.collection.mutable import scala.annotation.tailrec +import scala.annotation.constructorOnly /** Check initialization safety of static objects * @@ -119,13 +120,13 @@ object Objects: def show(using Context) = "ObjectRef(" + klass.show + ")" /** - * Rerepsents values that are instances of the specified class + * Rerepsents values that are instances of the specified class. * + * Note that the 2nd parameter block does not take part in the definition of equality. */ case class OfClass private (klass: ClassSymbol, outer: Value, ctor: Symbol, args: List[Value], env: Env.Data) (valsMap: mutable.Map[Symbol, Value], varsMap: mutable.Map[Symbol, Heap.Addr], outersMap: mutable.Map[ClassSymbol, Value]) extends Ref(valsMap, varsMap, outersMap): - def widenedCopy(outer: Value, args: List[Value], env: Env.Data): OfClass = new OfClass(klass, outer, ctor, args, env)(this.valsMap, this.varsMap, outersMap) @@ -138,13 +139,31 @@ object Objects: klass: ClassSymbol, outer: Value, ctor: Symbol, args: List[Value], env: Env.Data)( using Context ): OfClass = - val instance = - new OfClass(klass, outer, ctor, args, env)( - valsMap = mutable.Map.empty, varsMap = mutable.Map.empty, outersMap = mutable.Map.empty - ) + val instance = new OfClass(klass, outer, ctor, args, env)( + valsMap = mutable.Map.empty, varsMap = mutable.Map.empty, outersMap = mutable.Map.empty + ) instance.initOuter(klass, outer) instance + + /** + * Rerepsents arrays. + * + * Note that the 2nd parameter block does not take part in the definition of equality. + * + * Different arrays are distinguished by the context. Currently the default context is the static + * object whose initialization triggers the creation of the array. + * + * In the future, it is possible that we introduce a mechanism for end-users to mark the context. + * + * @param owner The static object whose initialization creates the array. + */ + case class OfArray(owner: ClassSymbol)(using @constructorOnly ctx: Context) + extends Ref(valsMap = mutable.Map.empty, varsMap = mutable.Map.empty, outersMap = mutable.Map.empty): + val klass: ClassSymbol = defn.ArrayClass + val addr: Heap.Addr = Heap.arrayAddr(this, owner) + def show(using Context) = "OfArray(owner = " + owner.show + ")" + /** * Represents a lambda expression */ @@ -426,6 +445,9 @@ object Objects: def fieldVarAddr(ref: Ref, sym: Symbol, owner: ClassSymbol): Addr = FieldAddr(ref, sym, owner) + def arrayAddr(ref: Ref, owner: ClassSymbol): Addr = + FieldAddr(ref, NoSymbol, owner) + def getHeapData()(using mutable: MutableData): Data = mutable.heap /** Cache used to terminate the check */ @@ -502,6 +524,26 @@ object Objects: case Bottom => Bottom + case arr: OfArray => + val target = resolve(defn.ArrayClass, meth) + + if target == defn.Array_apply || target == defn.Array_clone then + if arr.addr.owner == State.currentObject then + Heap.read(arr.addr) + else + errorReadOtherStaticObject(State.currentObject, arr.addr.owner) + Bottom + else if target == defn.Array_update then + assert(args.size == 2, "Incorrect number of arguments for Array update, found = " + args.size) + if arr.addr.owner != State.currentObject then + errorMutateOtherStaticObject(State.currentObject, arr.addr.owner) + else + Heap.write(arr.addr, args.tail.head.value) + Bottom + else + // Array.length is OK + Bottom + case ref: Ref => val isLocal = !meth.owner.isClass val target = @@ -669,24 +711,27 @@ object Objects: def instantiate(outer: Value, klass: ClassSymbol, ctor: Symbol, args: List[ArgInfo]): Contextual[Value] = log("instantiating " + klass.show + ", outer = " + outer + ", args = " + args.map(_.value.show), printer, (_: Value).show) { outer match - case fun: Fun => - report.error("[Internal error] unexpected tree in instantiating a function, fun = " + fun.code.show + Trace.show, Trace.position) + case _ : Fun | _: OfArray => + report.error("[Internal error] unexpected outer in instantiating a class, outer = " + outer.show + ", class = " + klass.show + ", " + Trace.show, Trace.position) Bottom case value: (Bottom.type | ObjectRef | OfClass | Cold.type) => // The outer can be a bottom value for top-level classes. - // Widen the outer to finitize the domain. Arguments already widened in `evalArgs`. - val (outerWidened, envWidened) = - if klass.owner.isClass then - (outer.widen(1), Env.NoEnv) - else - // klass.enclosingMethod returns its primary constructor - Env.resolveEnv(klass.owner.enclosingMethod, outer, summon[Env.Data]).getOrElse(Cold -> Env.NoEnv) + if klass == defn.ArrayClass then + OfArray(State.currentObject) + else + // Widen the outer to finitize the domain. Arguments already widened in `evalArgs`. + val (outerWidened, envWidened) = + if klass.owner.isClass then + (outer.widen(1), Env.NoEnv) + else + // klass.enclosingMethod returns its primary constructor + Env.resolveEnv(klass.owner.enclosingMethod, outer, summon[Env.Data]).getOrElse(Cold -> Env.NoEnv) - val instance = OfClass(klass, outerWidened, ctor, args.map(_.value), envWidened) - callConstructor(instance, ctor, args) - instance + val instance = OfClass(klass, outerWidened, ctor, args.map(_.value), envWidened) + callConstructor(instance, ctor, args) + instance case RefSet(refs) => refs.map(ref => instantiate(ref, klass, ctor, args)).join @@ -730,7 +775,7 @@ object Objects: case Cold => report.warning("Calling cold by-name alias. Call trace: \n" + Trace.show, Trace.position) Bottom - case _: RefSet | _: OfClass | _: ObjectRef => + case _: RefSet | _: Ref => report.warning("[Internal error] Unexpected by-name value " + value.show + ". Calling trace:\n" + Trace.show, Trace.position) Bottom else diff --git a/tests/init/neg/global-irrelevance5.scala b/tests/init/neg/global-irrelevance5.scala new file mode 100644 index 000000000000..fd5bde3032aa --- /dev/null +++ b/tests/init/neg/global-irrelevance5.scala @@ -0,0 +1,6 @@ +object A: + val array: Array[Int] = new Array(1) + array(0) = 10 + +object B: + var y = A.array(0) * 2 // error From 1a9a744940c17f12840f14816874a341eb5f4219 Mon Sep 17 00:00:00 2001 From: Fengyun Liu Date: Sun, 5 Mar 2023 10:16:30 +0100 Subject: [PATCH 086/113] More tests for array --- .../dotty/tools/dotc/transform/init/Objects.scala | 4 +++- tests/init/neg/global-irrelevance6.scala | 11 +++++++++++ tests/init/neg/global-irrelevance7.scala | 12 ++++++++++++ 3 files changed, 26 insertions(+), 1 deletion(-) create mode 100644 tests/init/neg/global-irrelevance6.scala create mode 100644 tests/init/neg/global-irrelevance7.scala diff --git a/compiler/src/dotty/tools/dotc/transform/init/Objects.scala b/compiler/src/dotty/tools/dotc/transform/init/Objects.scala index 54a34910cd85..dbea36c0a022 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Objects.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Objects.scala @@ -719,7 +719,9 @@ object Objects: // The outer can be a bottom value for top-level classes. if klass == defn.ArrayClass then - OfArray(State.currentObject) + val arr = OfArray(State.currentObject) + Heap.write(arr.addr, Bottom) + arr else // Widen the outer to finitize the domain. Arguments already widened in `evalArgs`. val (outerWidened, envWidened) = diff --git a/tests/init/neg/global-irrelevance6.scala b/tests/init/neg/global-irrelevance6.scala new file mode 100644 index 000000000000..5b0daa356086 --- /dev/null +++ b/tests/init/neg/global-irrelevance6.scala @@ -0,0 +1,11 @@ +import scala.annotation.init + +class Box(x: Int): + def foo(): Int = 100 + +object A: + val array: Array[Box] = new Array(1) + val n = array(0).foo() // ok, no crash + +object B: + var y = A.array(0).foo() * 2 // error diff --git a/tests/init/neg/global-irrelevance7.scala b/tests/init/neg/global-irrelevance7.scala new file mode 100644 index 000000000000..e334ec0c67a9 --- /dev/null +++ b/tests/init/neg/global-irrelevance7.scala @@ -0,0 +1,12 @@ +import scala.annotation.init + +class Box(x: Int): + def foo(): Int = 100 + +object A: + val array: Array[Box] = new Array(1) + array(0) = new Box(10): @init.expose + val n = array(0).foo() // ok + +object B: + var y = A.array(0).foo() * 2 // error From 1bde3a57028d8561185392e954bf0faf6ec313c1 Mon Sep 17 00:00:00 2001 From: Fengyun Liu Date: Sun, 26 Mar 2023 13:21:08 +0200 Subject: [PATCH 087/113] Add region context to objects --- .../tools/dotc/transform/init/Objects.scala | 38 ++++++++++++++----- .../tools/dotc/transform/init/Util.scala | 5 +++ 2 files changed, 34 insertions(+), 9 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/transform/init/Objects.scala b/compiler/src/dotty/tools/dotc/transform/init/Objects.scala index dbea36c0a022..0799c5ad1136 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Objects.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Objects.scala @@ -11,6 +11,7 @@ import NameKinds.OuterSelectName import NameKinds.SuperAccessorName import ast.tpd.* +import util.SourcePosition import config.Printers.init as printer import reporting.StoreReporter import reporting.trace as log @@ -122,13 +123,18 @@ object Objects: /** * Rerepsents values that are instances of the specified class. * + * For immutable classes (classes without mutable fields, non-transitive), the parameter regions + * is always empty and `owner` is `NoSymbol`. + * * Note that the 2nd parameter block does not take part in the definition of equality. */ - case class OfClass private (klass: ClassSymbol, outer: Value, ctor: Symbol, args: List[Value], env: Env.Data) - (valsMap: mutable.Map[Symbol, Value], varsMap: mutable.Map[Symbol, Heap.Addr], outersMap: mutable.Map[ClassSymbol, Value]) + case class OfClass private ( + klass: ClassSymbol, outer: Value, ctor: Symbol, args: List[Value], env: Env.Data, + regions: Regions.Data, owner: Symbol)( + valsMap: mutable.Map[Symbol, Value], varsMap: mutable.Map[Symbol, Heap.Addr], outersMap: mutable.Map[ClassSymbol, Value]) extends Ref(valsMap, varsMap, outersMap): def widenedCopy(outer: Value, args: List[Value], env: Env.Data): OfClass = - new OfClass(klass, outer, ctor, args, env)(this.valsMap, this.varsMap, outersMap) + new OfClass(klass, outer, ctor, args, env, regions, owner)(this.valsMap, this.varsMap, outersMap) def show(using Context) = val valFields = vals.map(_.show + " -> " + _.show) @@ -136,16 +142,15 @@ object Objects: object OfClass: def apply( - klass: ClassSymbol, outer: Value, ctor: Symbol, args: List[Value], env: Env.Data)( + klass: ClassSymbol, outer: Value, ctor: Symbol, args: List[Value], env: Env.Data, regions: Regions.Data, owner: Symbol)( using Context ): OfClass = - val instance = new OfClass(klass, outer, ctor, args, env)( + val instance = new OfClass(klass, outer, ctor, args, env, regions, owner)( valsMap = mutable.Map.empty, varsMap = mutable.Map.empty, outersMap = mutable.Map.empty ) instance.initOuter(klass, outer) instance - /** * Rerepsents arrays. * @@ -468,9 +473,20 @@ object Objects: result.value end Cache + /** + * Region context for mutable states + * + * By default, the region context is empty. + */ + object Regions: + opaque type Data = List[SourcePosition] + val empty: Data = Nil + def extend(pos: SourcePosition)(using data: Data): Data = pos :: data + def exists(pos: SourcePosition)(using data: Data): Data = data.indexOf(pos) >= 0 + inline def cache(using c: Cache.Data): Cache.Data = c - type Contextual[T] = (Context, State.Data, Env.Data, Cache.Data, Heap.MutableData, Trace) ?=> T + type Contextual[T] = (Context, State.Data, Env.Data, Cache.Data, Heap.MutableData, Regions.Data, Trace) ?=> T // --------------------------- domain operations ----------------------------- @@ -499,7 +515,7 @@ object Objects: if height == 0 then Cold else Fun(code, thisV.widen(height), klass, env.widen(height)) - case ref @ OfClass(klass, outer, init, args, env) => + case ref @ OfClass(klass, outer, _, args, env, _, _) => if height == 0 then Cold else @@ -731,7 +747,11 @@ object Objects: // klass.enclosingMethod returns its primary constructor Env.resolveEnv(klass.owner.enclosingMethod, outer, summon[Env.Data]).getOrElse(Cold -> Env.NoEnv) - val instance = OfClass(klass, outerWidened, ctor, args.map(_.value), envWidened) + // Immutable objects do not care about owners and context + val owner = if isMutable(klass) then State.currentObject else NoSymbol + val regions = if isMutable(klass) then summon[Regions.Data] else Regions.empty + + val instance = OfClass(klass, outerWidened, ctor, args.map(_.value), envWidened, regions, owner) callConstructor(instance, ctor, args) instance diff --git a/compiler/src/dotty/tools/dotc/transform/init/Util.scala b/compiler/src/dotty/tools/dotc/transform/init/Util.scala index 57fd1bdb7a2d..255f2ff0e78a 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Util.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Util.scala @@ -100,3 +100,8 @@ object Util: // A concrete class may not be instantiated if the self type is not satisfied instantiable && cls.enclosingPackageClass != defn.StdLibPatchesPackage.moduleClass + + /** Whether the class or its super class/trait contains any mutable fields? */ + def isMutable(cls: ClassSymbol)(using Context): Boolean = + cls.classInfo.decls.exists(_.is(Flags.Mutable)) || + cls.parentSyms.exists(parentCls => isMutable(parentCls)) From 9799d50f456430a246c2b01978022dc73fb74777 Mon Sep 17 00:00:00 2001 From: Fengyun Liu Date: Sun, 26 Mar 2023 15:36:42 +0200 Subject: [PATCH 088/113] Context-sensitive closures --- .../tools/dotc/transform/init/Objects.scala | 32 +++++++++---------- .../tools/dotc/transform/init/Util.scala | 2 +- 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/transform/init/Objects.scala b/compiler/src/dotty/tools/dotc/transform/init/Objects.scala index 0799c5ad1136..947a6d822dbe 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Objects.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Objects.scala @@ -163,10 +163,10 @@ object Objects: * * @param owner The static object whose initialization creates the array. */ - case class OfArray(owner: ClassSymbol)(using @constructorOnly ctx: Context) + case class OfArray(owner: ClassSymbol, regions: Regions.Data)(using @constructorOnly ctx: Context) extends Ref(valsMap = mutable.Map.empty, varsMap = mutable.Map.empty, outersMap = mutable.Map.empty): val klass: ClassSymbol = defn.ArrayClass - val addr: Heap.Addr = Heap.arrayAddr(this, owner) + val addr: Heap.Addr = Heap.arrayAddr(regions, owner) def show(using Context) = "OfArray(owner = " + owner.show + ")" /** @@ -214,6 +214,7 @@ object Objects: given Trace = Trace.empty.add(classSym.defTree) given Env.Data = Env.emptyEnv(tpl.constr.symbol) given Heap.MutableData = Heap.empty() + given regions: Regions.Data = Regions.empty // explicit name to avoid naming conflict val obj = ObjectRef(classSym) log("Iteration " + count) { @@ -281,7 +282,6 @@ object Objects: /** Local environments can be deeply nested, therefore we need `outer`. * * For local variables in rhs of class field definitions, the `meth` is the primary constructor. - * */ private case class LocalEnv (private[Env] val params: Map[Symbol, Value], meth: Symbol, outer: Data) @@ -407,10 +407,10 @@ object Objects: def owner: ClassSymbol /** The address for mutable fields of objects. */ - private case class FieldAddr(ref: Ref, field: Symbol, owner: ClassSymbol) extends Addr + private case class FieldAddr(regions: Regions.Data, field: Symbol, owner: ClassSymbol) extends Addr /** The address for mutable local variables . */ - private case class LocalVarAddr(ref: Ref, env: Env.Data, sym: Symbol, owner: ClassSymbol) extends Addr + private case class LocalVarAddr(regions: Regions.Data, sym: Symbol, owner: ClassSymbol) extends Addr /** Immutable heap data used in the cache. * @@ -444,14 +444,14 @@ object Objects: def write(addr: Addr, value: Value)(using mutable: MutableData): Unit = mutable.update(addr, value) - def localVarAddr(ref: Ref, env: Env.Data, sym: Symbol, owner: ClassSymbol): Addr = - LocalVarAddr(ref, env, sym, owner) + def localVarAddr(regions: Regions.Data, sym: Symbol, owner: ClassSymbol): Addr = + LocalVarAddr(regions, sym, owner) - def fieldVarAddr(ref: Ref, sym: Symbol, owner: ClassSymbol): Addr = - FieldAddr(ref, sym, owner) + def fieldVarAddr(regions: Regions.Data, sym: Symbol, owner: ClassSymbol): Addr = + FieldAddr(regions, sym, owner) - def arrayAddr(ref: Ref, owner: ClassSymbol): Addr = - FieldAddr(ref, NoSymbol, owner) + def arrayAddr(regions: Regions.Data, owner: ClassSymbol)(using Context): Addr = + FieldAddr(regions, defn.ArrayClass, owner) def getHeapData()(using mutable: MutableData): Data = mutable.heap @@ -482,7 +482,7 @@ object Objects: opaque type Data = List[SourcePosition] val empty: Data = Nil def extend(pos: SourcePosition)(using data: Data): Data = pos :: data - def exists(pos: SourcePosition)(using data: Data): Data = data.indexOf(pos) >= 0 + def exists(pos: SourcePosition)(using data: Data): Boolean = data.indexOf(pos) >= 0 inline def cache(using c: Cache.Data): Cache.Data = c @@ -735,7 +735,7 @@ object Objects: // The outer can be a bottom value for top-level classes. if klass == defn.ArrayClass then - val arr = OfArray(State.currentObject) + val arr = OfArray(State.currentObject, summon[Regions.Data]) Heap.write(arr.addr, Bottom) arr else @@ -761,7 +761,7 @@ object Objects: def initLocal(ref: Ref, sym: Symbol, value: Value): Contextual[Unit] = log("initialize local " + sym.show + " with " + value.show, printer) { if sym.is(Flags.Mutable) then - val addr = Heap.localVarAddr(ref, summon[Env.Data], sym, State.currentObject) + val addr = Heap.localVarAddr(summon[Regions.Data], sym, State.currentObject) Env.setLocalVar(sym, addr) Heap.write(addr, value) else @@ -1161,7 +1161,7 @@ object Objects: klass.paramGetters.foreach { acc => val value = paramsMap(acc.name.toTermName) if acc.is(Flags.Mutable) then - val addr = Heap.fieldVarAddr(thisV, acc, State.currentObject) + val addr = Heap.fieldVarAddr(summon[Regions.Data], acc, State.currentObject) thisV.initVar(acc, addr) Heap.write(addr, value) else @@ -1256,7 +1256,7 @@ object Objects: val res = eval(vdef.rhs, thisV, klass) val sym = vdef.symbol if sym.is(Flags.Mutable) then - val addr = Heap.fieldVarAddr(thisV, sym, State.currentObject) + val addr = Heap.fieldVarAddr(summon[Regions.Data], sym, State.currentObject) thisV.initVar(sym, addr) Heap.write(addr, res) else diff --git a/compiler/src/dotty/tools/dotc/transform/init/Util.scala b/compiler/src/dotty/tools/dotc/transform/init/Util.scala index 255f2ff0e78a..ad7d2afffbaf 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Util.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Util.scala @@ -104,4 +104,4 @@ object Util: /** Whether the class or its super class/trait contains any mutable fields? */ def isMutable(cls: ClassSymbol)(using Context): Boolean = cls.classInfo.decls.exists(_.is(Flags.Mutable)) || - cls.parentSyms.exists(parentCls => isMutable(parentCls)) + cls.parentSyms.exists(parentCls => isMutable(parentCls.asClass)) From 0a23d2c45e83a2a030af0e79db9f02fc4158623f Mon Sep 17 00:00:00 2001 From: Fengyun Liu Date: Sun, 26 Mar 2023 15:39:58 +0200 Subject: [PATCH 089/113] Simplify: No need to put owner and region in object --- .../tools/dotc/transform/init/Objects.scala | 20 ++++++------------- 1 file changed, 6 insertions(+), 14 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/transform/init/Objects.scala b/compiler/src/dotty/tools/dotc/transform/init/Objects.scala index 947a6d822dbe..6e307d099dbf 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Objects.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Objects.scala @@ -123,18 +123,14 @@ object Objects: /** * Rerepsents values that are instances of the specified class. * - * For immutable classes (classes without mutable fields, non-transitive), the parameter regions - * is always empty and `owner` is `NoSymbol`. - * * Note that the 2nd parameter block does not take part in the definition of equality. */ case class OfClass private ( - klass: ClassSymbol, outer: Value, ctor: Symbol, args: List[Value], env: Env.Data, - regions: Regions.Data, owner: Symbol)( + klass: ClassSymbol, outer: Value, ctor: Symbol, args: List[Value], env: Env.Data)( valsMap: mutable.Map[Symbol, Value], varsMap: mutable.Map[Symbol, Heap.Addr], outersMap: mutable.Map[ClassSymbol, Value]) extends Ref(valsMap, varsMap, outersMap): def widenedCopy(outer: Value, args: List[Value], env: Env.Data): OfClass = - new OfClass(klass, outer, ctor, args, env, regions, owner)(this.valsMap, this.varsMap, outersMap) + new OfClass(klass, outer, ctor, args, env)(this.valsMap, this.varsMap, outersMap) def show(using Context) = val valFields = vals.map(_.show + " -> " + _.show) @@ -142,10 +138,10 @@ object Objects: object OfClass: def apply( - klass: ClassSymbol, outer: Value, ctor: Symbol, args: List[Value], env: Env.Data, regions: Regions.Data, owner: Symbol)( + klass: ClassSymbol, outer: Value, ctor: Symbol, args: List[Value], env: Env.Data)( using Context ): OfClass = - val instance = new OfClass(klass, outer, ctor, args, env, regions, owner)( + val instance = new OfClass(klass, outer, ctor, args, env)( valsMap = mutable.Map.empty, varsMap = mutable.Map.empty, outersMap = mutable.Map.empty ) instance.initOuter(klass, outer) @@ -515,7 +511,7 @@ object Objects: if height == 0 then Cold else Fun(code, thisV.widen(height), klass, env.widen(height)) - case ref @ OfClass(klass, outer, _, args, env, _, _) => + case ref @ OfClass(klass, outer, _, args, env) => if height == 0 then Cold else @@ -747,11 +743,7 @@ object Objects: // klass.enclosingMethod returns its primary constructor Env.resolveEnv(klass.owner.enclosingMethod, outer, summon[Env.Data]).getOrElse(Cold -> Env.NoEnv) - // Immutable objects do not care about owners and context - val owner = if isMutable(klass) then State.currentObject else NoSymbol - val regions = if isMutable(klass) then summon[Regions.Data] else Regions.empty - - val instance = OfClass(klass, outerWidened, ctor, args.map(_.value), envWidened, regions, owner) + val instance = OfClass(klass, outerWidened, ctor, args.map(_.value), envWidened) callConstructor(instance, ctor, args) instance From 25888f3f189df85a8b97df6a418f2dfda2032b3a Mon Sep 17 00:00:00 2001 From: Fengyun Liu Date: Sun, 26 Mar 2023 15:54:43 +0200 Subject: [PATCH 090/113] Support region annotation --- .../dotty/tools/dotc/core/Definitions.scala | 1 + .../tools/dotc/transform/init/Objects.scala | 25 +++++++++++++++--- library/src/scala/annotation/init.scala | 26 ++++++++++++++++++- tests/init/neg/global-region1.scala | 11 ++++++++ tests/init/pos/global-region1.scala | 11 ++++++++ 5 files changed, 70 insertions(+), 4 deletions(-) create mode 100644 tests/init/neg/global-region1.scala create mode 100644 tests/init/pos/global-region1.scala diff --git a/compiler/src/dotty/tools/dotc/core/Definitions.scala b/compiler/src/dotty/tools/dotc/core/Definitions.scala index 42972c8bcc4b..083efdb419bf 100644 --- a/compiler/src/dotty/tools/dotc/core/Definitions.scala +++ b/compiler/src/dotty/tools/dotc/core/Definitions.scala @@ -999,6 +999,7 @@ class Definitions { @tu lazy val ImplicitNotFoundAnnot: ClassSymbol = requiredClass("scala.annotation.implicitNotFound") @tu lazy val InitWidenAnnot: ClassSymbol = requiredClass("scala.annotation.init.widen") @tu lazy val InitExposeAnnot: ClassSymbol = requiredClass("scala.annotation.init.expose") + @tu lazy val InitRegionAnnot: ClassSymbol = requiredClass("scala.annotation.init.region") @tu lazy val InlineParamAnnot: ClassSymbol = requiredClass("scala.annotation.internal.InlineParam") @tu lazy val ErasedParamAnnot: ClassSymbol = requiredClass("scala.annotation.internal.ErasedParam") @tu lazy val InvariantBetweenAnnot: ClassSymbol = requiredClass("scala.annotation.internal.InvariantBetween") diff --git a/compiler/src/dotty/tools/dotc/transform/init/Objects.scala b/compiler/src/dotty/tools/dotc/transform/init/Objects.scala index 6e307d099dbf..c9226c4530ed 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Objects.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Objects.scala @@ -82,6 +82,8 @@ object Objects: protected val vars: mutable.Map[Symbol, Heap.Addr] = varsMap protected val outers: mutable.Map[ClassSymbol, Value] = outersMap + def isObjectRef: Boolean = this.isInstanceOf[ObjectRef] + def klass: ClassSymbol def valValue(sym: Symbol): Value = vals(sym) @@ -479,6 +481,7 @@ object Objects: val empty: Data = Nil def extend(pos: SourcePosition)(using data: Data): Data = pos :: data def exists(pos: SourcePosition)(using data: Data): Boolean = data.indexOf(pos) >= 0 + def show(using data: Data, ctx: Context): String = data.map(_.show).mkString("[", ", ", "]") inline def cache(using c: Cache.Data): Cache.Data = c @@ -663,11 +666,17 @@ object Objects: else errorReadOtherStaticObject(State.currentObject, addr.owner) Bottom + else if ref.isObjectRef then + report.warning("Access uninitialized field " + field.show + ". Call trace: " + Trace.show, Trace.position) + Bottom else // initialization error, reported by the initialization checker Bottom else if ref.hasVal(target) then ref.valValue(target) + else if ref.isObjectRef then + report.warning("Access uninitialized field " + field.show + ". Call trace: " + Trace.show, Trace.position) + Bottom else // initialization error, reported by the initialization checker Bottom @@ -865,7 +874,7 @@ object Objects: * @param klass The enclosing class where the expression is located. * @param cacheResult It is used to reduce the size of the cache. */ - def eval(expr: Tree, thisV: Value, klass: ClassSymbol, cacheResult: Boolean = false): Contextual[Value] = log("evaluating " + expr.show + ", this = " + thisV.show + " in " + klass.show, printer, (_: Value).show) { + def eval(expr: Tree, thisV: Value, klass: ClassSymbol, cacheResult: Boolean = false): Contextual[Value] = log("evaluating " + expr.show + ", this = " + thisV.show + ", regions = " + Regions.show + " in " + klass.show, printer, (_: Value).show) { cache.cachedEval(thisV, expr, cacheResult) { expr => cases(expr, thisV, klass) } } @@ -956,6 +965,14 @@ object Objects: case Typed(expr, tpt) => if tpt.tpe.hasAnnotation(defn.UncheckedAnnot) then Bottom + else if tpt.tpe.hasAnnotation(defn.InitRegionAnnot) then + val regions2 = Regions.extend(tpt.sourcePos) + if Regions.exists(tpt.sourcePos) then + report.warning("Cyclic region detected. Trace: " + Trace.show, tpt.sourcePos) + Bottom + else + given Regions.Data = regions2 + eval(expr, thisV, klass) else eval(expr, thisV, klass) @@ -997,8 +1014,10 @@ object Objects: evalExprs(cond :: thenp :: elsep :: Nil, thisV, klass).join case Annotated(arg, annot) => - if (expr.tpe.hasAnnotation(defn.UncheckedAnnot)) Bottom - else eval(arg, thisV, klass) + if expr.tpe.hasAnnotation(defn.UncheckedAnnot) then + Bottom + else + eval(arg, thisV, klass) case Match(selector, cases) => eval(selector, thisV, klass) diff --git a/library/src/scala/annotation/init.scala b/library/src/scala/annotation/init.scala index 075f7a1dbeaa..9d1e15b36c09 100644 --- a/library/src/scala/annotation/init.scala +++ b/library/src/scala/annotation/init.scala @@ -24,7 +24,7 @@ object init: * def squre(): Int = x*x * * object B: - * val a = build(new A(10): @init.widen(1)) // <-- usage + * val a = build(new A(10): @init.expose) // <-- usage * * def build(o: A) = new A(o.square()) // calling methods on parameter * @@ -36,3 +36,27 @@ object init: * It is semantically equivalent to `@init.widen(1)`. */ final class expose extends StaticAnnotation + + /** Mark a region context. + * + * The same mutable field of objects in the same region have the same shape. The concept of regions is an + * attempt to make context-sensitivity explainable and customizable. + * + * Example: + * + * trait B { def foo(): Int } + * class C(var x: Int) extends B { def foo(): Int = 20 } + * class D(var y: Int) extends B { def foo(): Int = A.m } + * class Box(var value: B) + * + * object A: + * val box1: Box = new Box(new C(5)): @init.region + * val box2: Box = new Box(new D(10)): @init.region + * val m: Int = box1.value.foo() + * + * In the above, without the two region annotation, the two objects `box1` and `box2` are of the same region. + * Therefore, the field `box1.value` and `box2.value` points to both instances of `C` and `D`. Consequently, + * the method call `box1.value.foo()` will be invalid, because it reaches `A.m`, which is not yet initialized. + * The explicit context annotation solves the problem. + */ + final class region extends StaticAnnotation \ No newline at end of file diff --git a/tests/init/neg/global-region1.scala b/tests/init/neg/global-region1.scala new file mode 100644 index 000000000000..423828f153aa --- /dev/null +++ b/tests/init/neg/global-region1.scala @@ -0,0 +1,11 @@ +import scala.annotation.init + +trait B { def foo(): Int } +class C(var x: Int) extends B { def foo(): Int = 20 } +class D(var y: Int) extends B { def foo(): Int = A.m } +class Box(var value: B) + +object A: + val box1: Box = new Box(new C(5): @init.expose) + val box2: Box = new Box(new D(10): @init.expose) + val m: Int = box1.value.foo() // error diff --git a/tests/init/pos/global-region1.scala b/tests/init/pos/global-region1.scala new file mode 100644 index 000000000000..e8af6d7a3d5f --- /dev/null +++ b/tests/init/pos/global-region1.scala @@ -0,0 +1,11 @@ +import scala.annotation.init + +trait B { def foo(): Int } +class C(var x: Int) extends B { def foo(): Int = 20 } +class D(var y: Int) extends B { def foo(): Int = A.m } +class Box(var value: B) + +object A: + val box1: Box = new Box(new C(5): @init.expose): @init.region + val box2: Box = new Box(new D(10): @init.expose): @init.region + val m: Int = box1.value.foo() // ok From 6a6040e17450f433a525a35c2772e0016f115266 Mon Sep 17 00:00:00 2001 From: Fengyun Liu Date: Sun, 26 Mar 2023 19:44:55 +0200 Subject: [PATCH 091/113] Tweak default: widen 1 --- .../tools/dotc/transform/init/Objects.scala | 24 +++++++++---------- tests/init/neg/global-cycle14.scala | 2 +- tests/init/neg/global-cycle7.scala | 2 +- tests/init/neg/global-region1.scala | 4 ++-- 4 files changed, 15 insertions(+), 17 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/transform/init/Objects.scala b/compiler/src/dotty/tools/dotc/transform/init/Objects.scala index c9226c4530ed..b52084a024ac 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Objects.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Objects.scala @@ -504,26 +504,24 @@ object Objects: case (a, b) => RefSet(ListSet(a, b)) def widen(height: Int)(using Context): Value = - a match - case Bottom => Bottom + if height == 0 then Cold + else + a match + case Bottom => Bottom - case RefSet(refs) => - refs.map(ref => ref.widen(height)).join + case RefSet(refs) => + refs.map(ref => ref.widen(height)).join - case Fun(code, thisV, klass, env) => - if height == 0 then Cold - else Fun(code, thisV.widen(height), klass, env.widen(height)) + case Fun(code, thisV, klass, env) => + Fun(code, thisV.widen(height), klass, env.widen(height)) - case ref @ OfClass(klass, outer, _, args, env) => - if height == 0 then - Cold - else + case ref @ OfClass(klass, outer, _, args, env) => val outer2 = outer.widen(height - 1) val args2 = args.map(_.widen(height - 1)) val env2 = env.widen(height - 1) ref.widenedCopy(outer2, args2, env2) - case _ => a + case _ => a extension (values: Iterable[Value]) def join: Value = if values.isEmpty then Bottom else values.reduce { (v1, v2) => v1.join(v2) } @@ -1151,7 +1149,7 @@ object Objects: if arg.tree.tpe.hasAnnotation(defn.InitExposeAnnot) then res.widen(1) else - Cold + res.widen(1) argInfos += TraceValue(widened, trace.add(arg.tree)) } diff --git a/tests/init/neg/global-cycle14.scala b/tests/init/neg/global-cycle14.scala index ab416424478b..bcacbebb74fa 100644 --- a/tests/init/neg/global-cycle14.scala +++ b/tests/init/neg/global-cycle14.scala @@ -10,5 +10,5 @@ object A { // error } object B { - val m: Int = A.n + val m: Int = A.n // error } diff --git a/tests/init/neg/global-cycle7.scala b/tests/init/neg/global-cycle7.scala index 63adc342fd62..aea75726fbf7 100644 --- a/tests/init/neg/global-cycle7.scala +++ b/tests/init/neg/global-cycle7.scala @@ -3,7 +3,7 @@ object A { // error } object B { - val m: Int = A.n + val m: Int = A.n // error } abstract class TokensCommon { diff --git a/tests/init/neg/global-region1.scala b/tests/init/neg/global-region1.scala index 423828f153aa..7d99e66cfd29 100644 --- a/tests/init/neg/global-region1.scala +++ b/tests/init/neg/global-region1.scala @@ -2,10 +2,10 @@ import scala.annotation.init trait B { def foo(): Int } class C(var x: Int) extends B { def foo(): Int = 20 } -class D(var y: Int) extends B { def foo(): Int = A.m } +class D(var y: Int) extends B { def foo(): Int = A.m } // error class Box(var value: B) object A: val box1: Box = new Box(new C(5): @init.expose) val box2: Box = new Box(new D(10): @init.expose) - val m: Int = box1.value.foo() // error + val m: Int = box1.value.foo() From 1c30928e493d13ce297df286675284447aa7d037 Mon Sep 17 00:00:00 2001 From: Fengyun Liu Date: Sun, 26 Mar 2023 20:02:42 +0200 Subject: [PATCH 092/113] Ignore false warnings for Java & Scala 2 --- compiler/src/dotty/tools/dotc/transform/init/Objects.scala | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/transform/init/Objects.scala b/compiler/src/dotty/tools/dotc/transform/init/Objects.scala index b52084a024ac..2540c32d4a42 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Objects.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Objects.scala @@ -664,7 +664,7 @@ object Objects: else errorReadOtherStaticObject(State.currentObject, addr.owner) Bottom - else if ref.isObjectRef then + else if ref.isObjectRef && ref.klass.hasSource then report.warning("Access uninitialized field " + field.show + ". Call trace: " + Trace.show, Trace.position) Bottom else @@ -672,7 +672,7 @@ object Objects: Bottom else if ref.hasVal(target) then ref.valValue(target) - else if ref.isObjectRef then + else if ref.isObjectRef && ref.klass.hasSource then report.warning("Access uninitialized field " + field.show + ". Call trace: " + Trace.show, Trace.position) Bottom else From db976fc6a28ebf66e0c95ae31fb507c1c2602155 Mon Sep 17 00:00:00 2001 From: Fengyun Liu Date: Sun, 26 Mar 2023 20:40:27 +0200 Subject: [PATCH 093/113] Guard the check under a flag --- compiler/src/dotty/tools/dotc/config/ScalaSettings.scala | 2 +- compiler/src/dotty/tools/dotc/transform/init/Checker.scala | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/config/ScalaSettings.scala b/compiler/src/dotty/tools/dotc/config/ScalaSettings.scala index 0ddde23dc39f..731430c175a7 100644 --- a/compiler/src/dotty/tools/dotc/config/ScalaSettings.scala +++ b/compiler/src/dotty/tools/dotc/config/ScalaSettings.scala @@ -381,6 +381,7 @@ private sealed trait YSettings: val YnoKindPolymorphism: Setting[Boolean] = BooleanSetting("-Yno-kind-polymorphism", "Disable kind polymorphism.") val YexplicitNulls: Setting[Boolean] = BooleanSetting("-Yexplicit-nulls", "Make reference types non-nullable. Nullable types can be expressed with unions: e.g. String|Null.") val YcheckInit: Setting[Boolean] = BooleanSetting("-Ysafe-init", "Ensure safe initialization of objects") + val YcheckInitGlobal: Setting[Boolean] = BooleanSetting("-Ysafe-init-global", "Check safe initialization of global objects") val YrequireTargetName: Setting[Boolean] = BooleanSetting("-Yrequire-targetName", "Warn if an operator is defined without a @targetName annotation") val YrecheckTest: Setting[Boolean] = BooleanSetting("-Yrecheck-test", "Run basic rechecking (internal test only)") val YccDebug: Setting[Boolean] = BooleanSetting("-Ycc-debug", "Used in conjunction with captureChecking language import, debug info for captured references") @@ -398,4 +399,3 @@ private sealed trait YSettings: val YforceInlineWhileTyping: Setting[Boolean] = BooleanSetting("-Yforce-inline-while-typing", "Make non-transparent inline methods inline when typing. Emulates the old inlining behavior of 3.0.0-M3.") end YSettings - diff --git a/compiler/src/dotty/tools/dotc/transform/init/Checker.scala b/compiler/src/dotty/tools/dotc/transform/init/Checker.scala index e8f947b3b79a..83251c4aa479 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Checker.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Checker.scala @@ -37,7 +37,9 @@ class Checker extends Phase: val classes = traverser.getClasses() Semantic.checkClasses(classes)(using checkCtx) - Objects.checkClasses(classes)(using checkCtx) + + if ctx.settings.YcheckInitGlobal.value then + Objects.checkClasses(classes)(using checkCtx) units From 4672e4b0683ded75486e998927465d1f67238352 Mon Sep 17 00:00:00 2001 From: Fengyun Liu Date: Fri, 31 Mar 2023 08:05:45 +0200 Subject: [PATCH 094/113] Separate -Ysafe-int and -Ysafe-init-global --- compiler/src/dotty/tools/dotc/transform/init/Checker.scala | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/transform/init/Checker.scala b/compiler/src/dotty/tools/dotc/transform/init/Checker.scala index 83251c4aa479..a8def2bd7f48 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Checker.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Checker.scala @@ -28,7 +28,7 @@ class Checker extends Phase: override val runsAfter = Set(Pickler.name) override def isEnabled(using Context): Boolean = - super.isEnabled && ctx.settings.YcheckInit.value + super.isEnabled && (ctx.settings.YcheckInit.value || ctx.settings.YcheckInitGlobal.value) override def runOn(units: List[CompilationUnit])(using Context): List[CompilationUnit] = val checkCtx = ctx.fresh.setPhase(this.start) @@ -36,7 +36,8 @@ class Checker extends Phase: units.foreach { unit => traverser.traverse(unit.tpdTree) } val classes = traverser.getClasses() - Semantic.checkClasses(classes)(using checkCtx) + if ctx.settings.YcheckInit.value then + Semantic.checkClasses(classes)(using checkCtx) if ctx.settings.YcheckInitGlobal.value then Objects.checkClasses(classes)(using checkCtx) From 5c46833560472dd6c82184c943f8be25cc4a97fd Mon Sep 17 00:00:00 2001 From: David Hua Date: Wed, 5 Apr 2023 19:10:56 -0400 Subject: [PATCH 095/113] Add global init checking test cases --- tests/init/neg/context-sensitivity.scala | 18 ++++++++++++++++++ tests/init/neg/global-cycle5.scala | 23 +++++++++++++++++++++++ tests/init/neg/global-list.scala | 9 +++++++++ tests/init/neg/global-this.scala | 12 ++++++++++++ tests/init/neg/mutable-read1.scala | 10 ++++++++++ tests/init/neg/mutable-read2.scala | 10 ++++++++++ tests/init/neg/mutable-read3.scala | 8 ++++++++ tests/init/neg/mutable-read4.scala | 10 ++++++++++ tests/init/neg/mutable-read5.scala | 9 +++++++++ tests/init/neg/mutable-read6.scala | 15 +++++++++++++++ tests/init/neg/mutable-read7.scala | 13 +++++++++++++ tests/init/neg/mutable-read8.scala | 11 +++++++++++ tests/init/neg/partial-ordering.scala | 8 ++++++++ tests/init/pos/global-instantiation.scala | 7 +++++++ tests/init/pos/global-read.scala | 8 ++++++++ tests/init/pos/global-recursion2.scala | 6 ++++++ tests/init/pos/tree-counter.scala | 8 ++++++++ 17 files changed, 185 insertions(+) create mode 100755 tests/init/neg/context-sensitivity.scala create mode 100755 tests/init/neg/global-cycle5.scala create mode 100755 tests/init/neg/global-list.scala create mode 100755 tests/init/neg/global-this.scala create mode 100755 tests/init/neg/mutable-read1.scala create mode 100755 tests/init/neg/mutable-read2.scala create mode 100755 tests/init/neg/mutable-read3.scala create mode 100755 tests/init/neg/mutable-read4.scala create mode 100755 tests/init/neg/mutable-read5.scala create mode 100755 tests/init/neg/mutable-read6.scala create mode 100755 tests/init/neg/mutable-read7.scala create mode 100755 tests/init/neg/mutable-read8.scala create mode 100755 tests/init/neg/partial-ordering.scala create mode 100755 tests/init/pos/global-instantiation.scala create mode 100755 tests/init/pos/global-read.scala create mode 100755 tests/init/pos/global-recursion2.scala create mode 100755 tests/init/pos/tree-counter.scala diff --git a/tests/init/neg/context-sensitivity.scala b/tests/init/neg/context-sensitivity.scala new file mode 100755 index 000000000000..fd341945510b --- /dev/null +++ b/tests/init/neg/context-sensitivity.scala @@ -0,0 +1,18 @@ +import reflect.Selectable.reflectiveSelectable + +class C(var x: Int) { + def foo(): Int = 20 +} + +class D(var y: Int) { + def foo(): Int = A.m +} + +class Box(var value: { + def foo(): Int +}) + +object A: + val box1: Box = new Box(new C(5)) + val box2: Box = new Box(new D(10)) + val m: Int = box1.value.foo() // error \ No newline at end of file diff --git a/tests/init/neg/global-cycle5.scala b/tests/init/neg/global-cycle5.scala new file mode 100755 index 000000000000..ae9540289797 --- /dev/null +++ b/tests/init/neg/global-cycle5.scala @@ -0,0 +1,23 @@ +class X { + def foo(): Int = 10 +} + +object A { + var a: X = new X() +} + +object B { + val b: Int = A.a.foo() +} + +class Y extends X { + override def foo() = C.c +} + +object C { + val c: Int = B.b +} + +def main = { + A.a = new Y(); C // error +} \ No newline at end of file diff --git a/tests/init/neg/global-list.scala b/tests/init/neg/global-list.scala new file mode 100755 index 000000000000..d0c7bd32088e --- /dev/null +++ b/tests/init/neg/global-list.scala @@ -0,0 +1,9 @@ +case class Foo(name: String) + +object O: + val a = Foo("Apple") + val b = Foo("Banana") + val c = Foo("Cherry") + +object Foo: + val all: List[Foo] = List(O.a, O.b, O.c) // error \ No newline at end of file diff --git a/tests/init/neg/global-this.scala b/tests/init/neg/global-this.scala new file mode 100755 index 000000000000..12570419ad22 --- /dev/null +++ b/tests/init/neg/global-this.scala @@ -0,0 +1,12 @@ +object NameKinds: + abstract class NameKind(val tag: Int): + class Info + class QualifiedNameKind(tag: Int, val separator: String) extends NameKind(tag): + qualifiedNameKinds(tag) = this // error + + + val MAX_TAG = 8 + val qualifiedNameKinds = new Array[QualifiedNameKind](MAX_TAG) + + val QualifiedName: QualifiedNameKind = new QualifiedNameKind(0, ".") + val FlatName: QualifiedNameKind = new QualifiedNameKind(1, "$") \ No newline at end of file diff --git a/tests/init/neg/mutable-read1.scala b/tests/init/neg/mutable-read1.scala new file mode 100755 index 000000000000..507a8b7d74ad --- /dev/null +++ b/tests/init/neg/mutable-read1.scala @@ -0,0 +1,10 @@ +class Box(var value: Int) + +object A: + val box: Box = new Box(4) + +object B: + val boxB: Box = new Box(5) + val boxA: Box = A.box + val m: Int = boxB.value + val n: Int = boxA.value // error \ No newline at end of file diff --git a/tests/init/neg/mutable-read2.scala b/tests/init/neg/mutable-read2.scala new file mode 100755 index 000000000000..e7653c63d8bb --- /dev/null +++ b/tests/init/neg/mutable-read2.scala @@ -0,0 +1,10 @@ +object A: + class Box(var value: Int) { + val initial: Int = value + } + val box: Box = new Box(0) + +object B: + val box: A.Box = A.box + val a: Int = box.initial + val b: Int = box.value // error \ No newline at end of file diff --git a/tests/init/neg/mutable-read3.scala b/tests/init/neg/mutable-read3.scala new file mode 100755 index 000000000000..0eaa56736063 --- /dev/null +++ b/tests/init/neg/mutable-read3.scala @@ -0,0 +1,8 @@ +object A: + class Box(var value: Int) + val box: Box = new Box(0) + +object B: + val boxes: Array[A.Box] = Array(A.box) + val box: A.Box = boxes(0) + val x: Int = box.value // error \ No newline at end of file diff --git a/tests/init/neg/mutable-read4.scala b/tests/init/neg/mutable-read4.scala new file mode 100755 index 000000000000..507a8b7d74ad --- /dev/null +++ b/tests/init/neg/mutable-read4.scala @@ -0,0 +1,10 @@ +class Box(var value: Int) + +object A: + val box: Box = new Box(4) + +object B: + val boxB: Box = new Box(5) + val boxA: Box = A.box + val m: Int = boxB.value + val n: Int = boxA.value // error \ No newline at end of file diff --git a/tests/init/neg/mutable-read5.scala b/tests/init/neg/mutable-read5.scala new file mode 100755 index 000000000000..c166295bf9fa --- /dev/null +++ b/tests/init/neg/mutable-read5.scala @@ -0,0 +1,9 @@ +object Names: + class Name(val start: Int, val length: Int) + var chrs: Array[Char] = new Array[Char](0x20000) + def name(s: String): Name = Name(0, chrs.length) // error + +object StdNames: + val AnyRef: Names.Name = Names.name("AnyRef") + val Array: Names.Name = Names.name("Array") + val List: Names.Name = Names.name("List") \ No newline at end of file diff --git a/tests/init/neg/mutable-read6.scala b/tests/init/neg/mutable-read6.scala new file mode 100755 index 000000000000..8b00eeaf4216 --- /dev/null +++ b/tests/init/neg/mutable-read6.scala @@ -0,0 +1,15 @@ +class SourceFile + +object Contexts: + val NoContext: Context = new Context + class Context: + private var _source: SourceFile = null + final def source: SourceFile = _source // error + def setSource(source: SourceFile) = { + this._source = source + } + +object Implicits: + import Contexts.* + case class SearchFailure(tag: Int, source: SourceFile) + val NoMatchingFailure: SearchFailure = SearchFailure(1, NoContext.source) \ No newline at end of file diff --git a/tests/init/neg/mutable-read7.scala b/tests/init/neg/mutable-read7.scala new file mode 100755 index 000000000000..ad9d154d74f5 --- /dev/null +++ b/tests/init/neg/mutable-read7.scala @@ -0,0 +1,13 @@ +object Positioned: + var debug: Boolean = false + var debugId = Int.MinValue + var nextId: Int = 0 + +abstract class Positioned: + if (Positioned.debug) { // error + println("do debugging") + } + +object Trees: + class Tree extends Positioned + val emptyTree = new Tree \ No newline at end of file diff --git a/tests/init/neg/mutable-read8.scala b/tests/init/neg/mutable-read8.scala new file mode 100755 index 000000000000..e830fa65be73 --- /dev/null +++ b/tests/init/neg/mutable-read8.scala @@ -0,0 +1,11 @@ +object Stats { + var monitored: Boolean = false +} + +class UncachedGroundType { + if (Stats.monitored) println("record stats") // error +} + +class LazyType extends UncachedGroundType + +object NoCompleter extends LazyType \ No newline at end of file diff --git a/tests/init/neg/partial-ordering.scala b/tests/init/neg/partial-ordering.scala new file mode 100755 index 000000000000..f4b661f1c65e --- /dev/null +++ b/tests/init/neg/partial-ordering.scala @@ -0,0 +1,8 @@ +object Names: + val ctorString = "" + val ctorName: MethodName = MethodName.apply(ctorString) + +class MethodName(encoded: String) +object MethodName: + val ctor: MethodName = new MethodName(Names.ctorString) // error + def apply(name: String): MethodName = new MethodName(name) \ No newline at end of file diff --git a/tests/init/pos/global-instantiation.scala b/tests/init/pos/global-instantiation.scala new file mode 100755 index 000000000000..6964901e964e --- /dev/null +++ b/tests/init/pos/global-instantiation.scala @@ -0,0 +1,7 @@ +class A(x: Int) { + def foo(): Int = B.m +} + +object B: + val m: Int = 20 + val n: Int = new A(10).foo() \ No newline at end of file diff --git a/tests/init/pos/global-read.scala b/tests/init/pos/global-read.scala new file mode 100755 index 000000000000..5f2386d12c2c --- /dev/null +++ b/tests/init/pos/global-read.scala @@ -0,0 +1,8 @@ +object A: + val a: Int = 10 + val b: Int = 20 + +object B: + var n: Int = A.a * A.b + +@main def entry() = println(B.n) \ No newline at end of file diff --git a/tests/init/pos/global-recursion2.scala b/tests/init/pos/global-recursion2.scala new file mode 100755 index 000000000000..5a9e3edfad14 --- /dev/null +++ b/tests/init/pos/global-recursion2.scala @@ -0,0 +1,6 @@ +class A(val a: A) + +object B: + val a: A = loop(ofA()) + def ofA(): A = ofA().a + def loop(a: A): A = loop(new A(a)) \ No newline at end of file diff --git a/tests/init/pos/tree-counter.scala b/tests/init/pos/tree-counter.scala new file mode 100755 index 000000000000..2201911af608 --- /dev/null +++ b/tests/init/pos/tree-counter.scala @@ -0,0 +1,8 @@ +object Trees: + private var counter = 0 + class Tree { + counter += 1 + } + + class EmptyTree extends Tree + val theEmptyTree = new EmptyTree \ No newline at end of file From fb2186293313cf3b101a65dd4ad0cf01ca08d371 Mon Sep 17 00:00:00 2001 From: Fengyun Liu Date: Thu, 6 Apr 2023 07:58:12 +0200 Subject: [PATCH 096/113] Separate object init tests from class init tests --- .../test/dotty/tools/dotc/CompilationTests.scala | 8 ++++++++ .../neg/context-sensitivity.scala | 0 tests/{init => init-global}/neg/global-by-name.scala | 0 tests/{init => init-global}/neg/global-cycle1.check | 0 tests/{init => init-global}/neg/global-cycle1.scala | 0 tests/{init => init-global}/neg/global-cycle14.scala | 0 tests/{init => init-global}/neg/global-cycle5.scala | 0 tests/{init => init-global}/neg/global-cycle6.scala | 0 tests/{init => init-global}/neg/global-cycle7.scala | 0 tests/{init => init-global}/neg/global-cycle8.scala | 0 tests/{init => init-global}/neg/global-fun.scala | 0 .../neg/global-irrelevance1.scala | 0 .../neg/global-irrelevance2.scala | 0 .../neg/global-irrelevance3.scala | 0 .../neg/global-irrelevance4.scala | 0 .../neg/global-irrelevance5.scala | 0 .../neg/global-irrelevance6.scala | 0 .../neg/global-irrelevance7.scala | 0 tests/{init => init-global}/neg/global-list.scala | 0 tests/{init => init-global}/neg/global-region1.scala | 0 tests/{init => init-global}/neg/global-this.scala | 0 tests/{init => init-global}/neg/i11262.scala | 0 tests/{init => init-global}/neg/i12544b.scala | 0 tests/{init => init-global}/neg/i9176.scala | 0 tests/{init => init-global}/neg/mutable-read1.scala | 0 tests/{init => init-global}/neg/mutable-read2.scala | 0 tests/{init => init-global}/neg/mutable-read3.scala | 0 tests/{init => init-global}/neg/mutable-read4.scala | 0 tests/{init => init-global}/neg/mutable-read5.scala | 0 tests/{init => init-global}/neg/mutable-read6.scala | 0 tests/{init => init-global}/neg/mutable-read7.scala | 0 tests/{init => init-global}/neg/mutable-read8.scala | 0 .../{init => init-global}/neg/partial-ordering.scala | 0 tests/{init => init-global}/neg/t5366.scala | 0 tests/{init => init-global}/neg/t9115.scala | 0 tests/{init => init-global}/neg/t9261.scala | 0 tests/{init => init-global}/neg/t9312.scala | 0 tests/{init => init-global}/neg/t9360.scala | 0 tests/{init => init-global}/pos/global-by-name.scala | 0 tests/{init => init-global}/pos/global-cycle10.scala | 0 tests/{init => init-global}/pos/global-cycle11.scala | 0 tests/{init => init-global}/pos/global-cycle12.scala | 0 tests/{init => init-global}/pos/global-cycle2.scala | 0 tests/{init => init-global}/pos/global-cycle3.scala | 0 tests/{init => init-global}/pos/global-cycle4.scala | 0 tests/{init => init-global}/pos/global-cycle9.scala | 0 tests/{init => init-global}/pos/global-fun.scala | 0 .../pos/global-instantiation.scala | 0 .../{init => init-global}/pos/global-local-var.scala | 0 tests/{init => init-global}/pos/global-read.scala | 0 .../{init => init-global}/pos/global-recursion.scala | 0 .../pos/global-recursion2.scala | 0 tests/{init => init-global}/pos/global-region1.scala | 0 .../pos/global-trivial-cycle.scala | 0 .../{init => init-global}/pos/global-val-owner.scala | 0 .../pos/global-val-owner2.scala | 0 tests/{init => init-global}/pos/tree-counter.scala | 0 tests/init/neg/i12544.scala | 2 +- tests/init/pos/i12544.scala | 12 ++++++++++++ 59 files changed, 21 insertions(+), 1 deletion(-) rename tests/{init => init-global}/neg/context-sensitivity.scala (100%) rename tests/{init => init-global}/neg/global-by-name.scala (100%) rename tests/{init => init-global}/neg/global-cycle1.check (100%) rename tests/{init => init-global}/neg/global-cycle1.scala (100%) rename tests/{init => init-global}/neg/global-cycle14.scala (100%) rename tests/{init => init-global}/neg/global-cycle5.scala (100%) rename tests/{init => init-global}/neg/global-cycle6.scala (100%) rename tests/{init => init-global}/neg/global-cycle7.scala (100%) rename tests/{init => init-global}/neg/global-cycle8.scala (100%) rename tests/{init => init-global}/neg/global-fun.scala (100%) rename tests/{init => init-global}/neg/global-irrelevance1.scala (100%) rename tests/{init => init-global}/neg/global-irrelevance2.scala (100%) rename tests/{init => init-global}/neg/global-irrelevance3.scala (100%) rename tests/{init => init-global}/neg/global-irrelevance4.scala (100%) rename tests/{init => init-global}/neg/global-irrelevance5.scala (100%) rename tests/{init => init-global}/neg/global-irrelevance6.scala (100%) rename tests/{init => init-global}/neg/global-irrelevance7.scala (100%) rename tests/{init => init-global}/neg/global-list.scala (100%) rename tests/{init => init-global}/neg/global-region1.scala (100%) rename tests/{init => init-global}/neg/global-this.scala (100%) rename tests/{init => init-global}/neg/i11262.scala (100%) rename tests/{init => init-global}/neg/i12544b.scala (100%) rename tests/{init => init-global}/neg/i9176.scala (100%) rename tests/{init => init-global}/neg/mutable-read1.scala (100%) rename tests/{init => init-global}/neg/mutable-read2.scala (100%) rename tests/{init => init-global}/neg/mutable-read3.scala (100%) rename tests/{init => init-global}/neg/mutable-read4.scala (100%) rename tests/{init => init-global}/neg/mutable-read5.scala (100%) rename tests/{init => init-global}/neg/mutable-read6.scala (100%) rename tests/{init => init-global}/neg/mutable-read7.scala (100%) rename tests/{init => init-global}/neg/mutable-read8.scala (100%) rename tests/{init => init-global}/neg/partial-ordering.scala (100%) rename tests/{init => init-global}/neg/t5366.scala (100%) rename tests/{init => init-global}/neg/t9115.scala (100%) rename tests/{init => init-global}/neg/t9261.scala (100%) rename tests/{init => init-global}/neg/t9312.scala (100%) rename tests/{init => init-global}/neg/t9360.scala (100%) rename tests/{init => init-global}/pos/global-by-name.scala (100%) rename tests/{init => init-global}/pos/global-cycle10.scala (100%) rename tests/{init => init-global}/pos/global-cycle11.scala (100%) rename tests/{init => init-global}/pos/global-cycle12.scala (100%) rename tests/{init => init-global}/pos/global-cycle2.scala (100%) rename tests/{init => init-global}/pos/global-cycle3.scala (100%) rename tests/{init => init-global}/pos/global-cycle4.scala (100%) rename tests/{init => init-global}/pos/global-cycle9.scala (100%) rename tests/{init => init-global}/pos/global-fun.scala (100%) rename tests/{init => init-global}/pos/global-instantiation.scala (100%) rename tests/{init => init-global}/pos/global-local-var.scala (100%) rename tests/{init => init-global}/pos/global-read.scala (100%) rename tests/{init => init-global}/pos/global-recursion.scala (100%) rename tests/{init => init-global}/pos/global-recursion2.scala (100%) rename tests/{init => init-global}/pos/global-region1.scala (100%) rename tests/{init => init-global}/pos/global-trivial-cycle.scala (100%) rename tests/{init => init-global}/pos/global-val-owner.scala (100%) rename tests/{init => init-global}/pos/global-val-owner2.scala (100%) rename tests/{init => init-global}/pos/tree-counter.scala (100%) create mode 100644 tests/init/pos/i12544.scala diff --git a/compiler/test/dotty/tools/dotc/CompilationTests.scala b/compiler/test/dotty/tools/dotc/CompilationTests.scala index 4e86a3b83383..cab318b3201e 100644 --- a/compiler/test/dotty/tools/dotc/CompilationTests.scala +++ b/compiler/test/dotty/tools/dotc/CompilationTests.scala @@ -274,6 +274,14 @@ class CompilationTests { compileFilesInDir("tests/explicit-nulls/run", explicitNullsOptions) }.checkRuns() + // initialization tests + @Test def checkInitGlobal: Unit = { + implicit val testGroup: TestGroup = TestGroup("checkInitGlobal") + val options = defaultOptions.and("-Ysafe-init-global", "-Xfatal-warnings") + compileFilesInDir("tests/init-global/neg", options).checkExpectedErrors() + compileFilesInDir("tests/init-global/pos", options).checkCompile() + } + // initialization tests @Test def checkInit: Unit = { implicit val testGroup: TestGroup = TestGroup("checkInit") diff --git a/tests/init/neg/context-sensitivity.scala b/tests/init-global/neg/context-sensitivity.scala similarity index 100% rename from tests/init/neg/context-sensitivity.scala rename to tests/init-global/neg/context-sensitivity.scala diff --git a/tests/init/neg/global-by-name.scala b/tests/init-global/neg/global-by-name.scala similarity index 100% rename from tests/init/neg/global-by-name.scala rename to tests/init-global/neg/global-by-name.scala diff --git a/tests/init/neg/global-cycle1.check b/tests/init-global/neg/global-cycle1.check similarity index 100% rename from tests/init/neg/global-cycle1.check rename to tests/init-global/neg/global-cycle1.check diff --git a/tests/init/neg/global-cycle1.scala b/tests/init-global/neg/global-cycle1.scala similarity index 100% rename from tests/init/neg/global-cycle1.scala rename to tests/init-global/neg/global-cycle1.scala diff --git a/tests/init/neg/global-cycle14.scala b/tests/init-global/neg/global-cycle14.scala similarity index 100% rename from tests/init/neg/global-cycle14.scala rename to tests/init-global/neg/global-cycle14.scala diff --git a/tests/init/neg/global-cycle5.scala b/tests/init-global/neg/global-cycle5.scala similarity index 100% rename from tests/init/neg/global-cycle5.scala rename to tests/init-global/neg/global-cycle5.scala diff --git a/tests/init/neg/global-cycle6.scala b/tests/init-global/neg/global-cycle6.scala similarity index 100% rename from tests/init/neg/global-cycle6.scala rename to tests/init-global/neg/global-cycle6.scala diff --git a/tests/init/neg/global-cycle7.scala b/tests/init-global/neg/global-cycle7.scala similarity index 100% rename from tests/init/neg/global-cycle7.scala rename to tests/init-global/neg/global-cycle7.scala diff --git a/tests/init/neg/global-cycle8.scala b/tests/init-global/neg/global-cycle8.scala similarity index 100% rename from tests/init/neg/global-cycle8.scala rename to tests/init-global/neg/global-cycle8.scala diff --git a/tests/init/neg/global-fun.scala b/tests/init-global/neg/global-fun.scala similarity index 100% rename from tests/init/neg/global-fun.scala rename to tests/init-global/neg/global-fun.scala diff --git a/tests/init/neg/global-irrelevance1.scala b/tests/init-global/neg/global-irrelevance1.scala similarity index 100% rename from tests/init/neg/global-irrelevance1.scala rename to tests/init-global/neg/global-irrelevance1.scala diff --git a/tests/init/neg/global-irrelevance2.scala b/tests/init-global/neg/global-irrelevance2.scala similarity index 100% rename from tests/init/neg/global-irrelevance2.scala rename to tests/init-global/neg/global-irrelevance2.scala diff --git a/tests/init/neg/global-irrelevance3.scala b/tests/init-global/neg/global-irrelevance3.scala similarity index 100% rename from tests/init/neg/global-irrelevance3.scala rename to tests/init-global/neg/global-irrelevance3.scala diff --git a/tests/init/neg/global-irrelevance4.scala b/tests/init-global/neg/global-irrelevance4.scala similarity index 100% rename from tests/init/neg/global-irrelevance4.scala rename to tests/init-global/neg/global-irrelevance4.scala diff --git a/tests/init/neg/global-irrelevance5.scala b/tests/init-global/neg/global-irrelevance5.scala similarity index 100% rename from tests/init/neg/global-irrelevance5.scala rename to tests/init-global/neg/global-irrelevance5.scala diff --git a/tests/init/neg/global-irrelevance6.scala b/tests/init-global/neg/global-irrelevance6.scala similarity index 100% rename from tests/init/neg/global-irrelevance6.scala rename to tests/init-global/neg/global-irrelevance6.scala diff --git a/tests/init/neg/global-irrelevance7.scala b/tests/init-global/neg/global-irrelevance7.scala similarity index 100% rename from tests/init/neg/global-irrelevance7.scala rename to tests/init-global/neg/global-irrelevance7.scala diff --git a/tests/init/neg/global-list.scala b/tests/init-global/neg/global-list.scala similarity index 100% rename from tests/init/neg/global-list.scala rename to tests/init-global/neg/global-list.scala diff --git a/tests/init/neg/global-region1.scala b/tests/init-global/neg/global-region1.scala similarity index 100% rename from tests/init/neg/global-region1.scala rename to tests/init-global/neg/global-region1.scala diff --git a/tests/init/neg/global-this.scala b/tests/init-global/neg/global-this.scala similarity index 100% rename from tests/init/neg/global-this.scala rename to tests/init-global/neg/global-this.scala diff --git a/tests/init/neg/i11262.scala b/tests/init-global/neg/i11262.scala similarity index 100% rename from tests/init/neg/i11262.scala rename to tests/init-global/neg/i11262.scala diff --git a/tests/init/neg/i12544b.scala b/tests/init-global/neg/i12544b.scala similarity index 100% rename from tests/init/neg/i12544b.scala rename to tests/init-global/neg/i12544b.scala diff --git a/tests/init/neg/i9176.scala b/tests/init-global/neg/i9176.scala similarity index 100% rename from tests/init/neg/i9176.scala rename to tests/init-global/neg/i9176.scala diff --git a/tests/init/neg/mutable-read1.scala b/tests/init-global/neg/mutable-read1.scala similarity index 100% rename from tests/init/neg/mutable-read1.scala rename to tests/init-global/neg/mutable-read1.scala diff --git a/tests/init/neg/mutable-read2.scala b/tests/init-global/neg/mutable-read2.scala similarity index 100% rename from tests/init/neg/mutable-read2.scala rename to tests/init-global/neg/mutable-read2.scala diff --git a/tests/init/neg/mutable-read3.scala b/tests/init-global/neg/mutable-read3.scala similarity index 100% rename from tests/init/neg/mutable-read3.scala rename to tests/init-global/neg/mutable-read3.scala diff --git a/tests/init/neg/mutable-read4.scala b/tests/init-global/neg/mutable-read4.scala similarity index 100% rename from tests/init/neg/mutable-read4.scala rename to tests/init-global/neg/mutable-read4.scala diff --git a/tests/init/neg/mutable-read5.scala b/tests/init-global/neg/mutable-read5.scala similarity index 100% rename from tests/init/neg/mutable-read5.scala rename to tests/init-global/neg/mutable-read5.scala diff --git a/tests/init/neg/mutable-read6.scala b/tests/init-global/neg/mutable-read6.scala similarity index 100% rename from tests/init/neg/mutable-read6.scala rename to tests/init-global/neg/mutable-read6.scala diff --git a/tests/init/neg/mutable-read7.scala b/tests/init-global/neg/mutable-read7.scala similarity index 100% rename from tests/init/neg/mutable-read7.scala rename to tests/init-global/neg/mutable-read7.scala diff --git a/tests/init/neg/mutable-read8.scala b/tests/init-global/neg/mutable-read8.scala similarity index 100% rename from tests/init/neg/mutable-read8.scala rename to tests/init-global/neg/mutable-read8.scala diff --git a/tests/init/neg/partial-ordering.scala b/tests/init-global/neg/partial-ordering.scala similarity index 100% rename from tests/init/neg/partial-ordering.scala rename to tests/init-global/neg/partial-ordering.scala diff --git a/tests/init/neg/t5366.scala b/tests/init-global/neg/t5366.scala similarity index 100% rename from tests/init/neg/t5366.scala rename to tests/init-global/neg/t5366.scala diff --git a/tests/init/neg/t9115.scala b/tests/init-global/neg/t9115.scala similarity index 100% rename from tests/init/neg/t9115.scala rename to tests/init-global/neg/t9115.scala diff --git a/tests/init/neg/t9261.scala b/tests/init-global/neg/t9261.scala similarity index 100% rename from tests/init/neg/t9261.scala rename to tests/init-global/neg/t9261.scala diff --git a/tests/init/neg/t9312.scala b/tests/init-global/neg/t9312.scala similarity index 100% rename from tests/init/neg/t9312.scala rename to tests/init-global/neg/t9312.scala diff --git a/tests/init/neg/t9360.scala b/tests/init-global/neg/t9360.scala similarity index 100% rename from tests/init/neg/t9360.scala rename to tests/init-global/neg/t9360.scala diff --git a/tests/init/pos/global-by-name.scala b/tests/init-global/pos/global-by-name.scala similarity index 100% rename from tests/init/pos/global-by-name.scala rename to tests/init-global/pos/global-by-name.scala diff --git a/tests/init/pos/global-cycle10.scala b/tests/init-global/pos/global-cycle10.scala similarity index 100% rename from tests/init/pos/global-cycle10.scala rename to tests/init-global/pos/global-cycle10.scala diff --git a/tests/init/pos/global-cycle11.scala b/tests/init-global/pos/global-cycle11.scala similarity index 100% rename from tests/init/pos/global-cycle11.scala rename to tests/init-global/pos/global-cycle11.scala diff --git a/tests/init/pos/global-cycle12.scala b/tests/init-global/pos/global-cycle12.scala similarity index 100% rename from tests/init/pos/global-cycle12.scala rename to tests/init-global/pos/global-cycle12.scala diff --git a/tests/init/pos/global-cycle2.scala b/tests/init-global/pos/global-cycle2.scala similarity index 100% rename from tests/init/pos/global-cycle2.scala rename to tests/init-global/pos/global-cycle2.scala diff --git a/tests/init/pos/global-cycle3.scala b/tests/init-global/pos/global-cycle3.scala similarity index 100% rename from tests/init/pos/global-cycle3.scala rename to tests/init-global/pos/global-cycle3.scala diff --git a/tests/init/pos/global-cycle4.scala b/tests/init-global/pos/global-cycle4.scala similarity index 100% rename from tests/init/pos/global-cycle4.scala rename to tests/init-global/pos/global-cycle4.scala diff --git a/tests/init/pos/global-cycle9.scala b/tests/init-global/pos/global-cycle9.scala similarity index 100% rename from tests/init/pos/global-cycle9.scala rename to tests/init-global/pos/global-cycle9.scala diff --git a/tests/init/pos/global-fun.scala b/tests/init-global/pos/global-fun.scala similarity index 100% rename from tests/init/pos/global-fun.scala rename to tests/init-global/pos/global-fun.scala diff --git a/tests/init/pos/global-instantiation.scala b/tests/init-global/pos/global-instantiation.scala similarity index 100% rename from tests/init/pos/global-instantiation.scala rename to tests/init-global/pos/global-instantiation.scala diff --git a/tests/init/pos/global-local-var.scala b/tests/init-global/pos/global-local-var.scala similarity index 100% rename from tests/init/pos/global-local-var.scala rename to tests/init-global/pos/global-local-var.scala diff --git a/tests/init/pos/global-read.scala b/tests/init-global/pos/global-read.scala similarity index 100% rename from tests/init/pos/global-read.scala rename to tests/init-global/pos/global-read.scala diff --git a/tests/init/pos/global-recursion.scala b/tests/init-global/pos/global-recursion.scala similarity index 100% rename from tests/init/pos/global-recursion.scala rename to tests/init-global/pos/global-recursion.scala diff --git a/tests/init/pos/global-recursion2.scala b/tests/init-global/pos/global-recursion2.scala similarity index 100% rename from tests/init/pos/global-recursion2.scala rename to tests/init-global/pos/global-recursion2.scala diff --git a/tests/init/pos/global-region1.scala b/tests/init-global/pos/global-region1.scala similarity index 100% rename from tests/init/pos/global-region1.scala rename to tests/init-global/pos/global-region1.scala diff --git a/tests/init/pos/global-trivial-cycle.scala b/tests/init-global/pos/global-trivial-cycle.scala similarity index 100% rename from tests/init/pos/global-trivial-cycle.scala rename to tests/init-global/pos/global-trivial-cycle.scala diff --git a/tests/init/pos/global-val-owner.scala b/tests/init-global/pos/global-val-owner.scala similarity index 100% rename from tests/init/pos/global-val-owner.scala rename to tests/init-global/pos/global-val-owner.scala diff --git a/tests/init/pos/global-val-owner2.scala b/tests/init-global/pos/global-val-owner2.scala similarity index 100% rename from tests/init/pos/global-val-owner2.scala rename to tests/init-global/pos/global-val-owner2.scala diff --git a/tests/init/pos/tree-counter.scala b/tests/init-global/pos/tree-counter.scala similarity index 100% rename from tests/init/pos/tree-counter.scala rename to tests/init-global/pos/tree-counter.scala diff --git a/tests/init/neg/i12544.scala b/tests/init/neg/i12544.scala index 41e032d35c9e..2692c27134e0 100644 --- a/tests/init/neg/i12544.scala +++ b/tests/init/neg/i12544.scala @@ -5,7 +5,7 @@ enum Enum: def g(b: Enum.B): Int = b.foo() object Enum: - object nested: // error + object nested: val a: Enum = Case val b: Enum = f(nested.a) diff --git a/tests/init/pos/i12544.scala b/tests/init/pos/i12544.scala new file mode 100644 index 000000000000..e5e3c9a2ade2 --- /dev/null +++ b/tests/init/pos/i12544.scala @@ -0,0 +1,12 @@ +enum Enum: + case Case + +object Enum: + object nested: + val a: Enum = Case + + val b: Enum = f(nested.a) + + def f(e: Enum): Enum = e + +@main def main(): Unit = println(Enum.b) From 557fbc7e3af8f019e62d8703b00d571de0965232 Mon Sep 17 00:00:00 2001 From: Fengyun Liu Date: Mon, 1 May 2023 16:01:32 +0200 Subject: [PATCH 097/113] Fix tests --- compiler/src/dotty/tools/dotc/core/Symbols.scala | 3 ++- tests/init-global/neg/context-sensitivity.scala | 15 +++++++-------- tests/init-global/neg/global-by-name.scala | 11 ----------- tests/init-global/neg/global-cycle1.check | 12 ++++++++++-- tests/init-global/neg/global-cycle1.scala | 2 +- tests/init-global/neg/global-cycle2.scala | 7 +++++++ tests/init-global/neg/global-cycle3.scala | 7 +++++++ .../init-global/{pos => neg}/global-cycle4.scala | 4 ++-- tests/init-global/neg/global-cycle5.scala | 4 ++-- tests/init-global/neg/global-cycle8.scala | 2 +- tests/init-global/neg/global-fun.scala | 12 ------------ tests/init-global/neg/global-irrelevance3.scala | 4 +--- tests/init-global/neg/global-irrelevance4.scala | 4 +--- tests/init-global/neg/global-irrelevance6.scala | 2 -- tests/init-global/neg/global-irrelevance7.scala | 4 +--- tests/init-global/neg/global-list.scala | 4 ++-- .../{pos => neg}/global-local-var.scala | 2 +- tests/init-global/neg/global-region1.scala | 6 ++---- tests/init-global/neg/i11262.scala | 2 +- tests/init-global/neg/i12544b.scala | 2 +- tests/init-global/neg/mutable-read3.scala | 3 ++- tests/init-global/neg/partial-ordering.scala | 4 ++-- tests/init-global/pos/global-by-name.scala | 4 +--- tests/init-global/pos/global-cycle2.scala | 7 ------- tests/init-global/pos/global-cycle3.scala | 7 ------- tests/init-global/pos/global-fun.scala | 8 +++----- tests/init-global/pos/global-region1.scala | 4 ++-- tests/init-global/{neg => pos}/global-this.scala | 3 +-- 28 files changed, 60 insertions(+), 89 deletions(-) delete mode 100644 tests/init-global/neg/global-by-name.scala create mode 100644 tests/init-global/neg/global-cycle2.scala create mode 100644 tests/init-global/neg/global-cycle3.scala rename tests/init-global/{pos => neg}/global-cycle4.scala (74%) delete mode 100644 tests/init-global/neg/global-fun.scala rename tests/init-global/{pos => neg}/global-local-var.scala (85%) delete mode 100644 tests/init-global/pos/global-cycle2.scala delete mode 100644 tests/init-global/pos/global-cycle3.scala rename tests/init-global/{neg => pos}/global-this.scala (99%) diff --git a/compiler/src/dotty/tools/dotc/core/Symbols.scala b/compiler/src/dotty/tools/dotc/core/Symbols.scala index 07ac2be90819..c13591fb9f5a 100644 --- a/compiler/src/dotty/tools/dotc/core/Symbols.scala +++ b/compiler/src/dotty/tools/dotc/core/Symbols.scala @@ -83,7 +83,8 @@ object Symbols { ctx.settings.YretainTrees.value || denot.owner.isTerm || // no risk of leaking memory after a run for these denot.isOneOf(InlineOrProxy) || // need to keep inline info - ctx.settings.YcheckInit.value // initialization check + ctx.settings.YcheckInit.value || // initialization check + ctx.settings.YcheckInitGlobal.value /** The last denotation of this symbol */ private var lastDenot: SymDenotation = _ diff --git a/tests/init-global/neg/context-sensitivity.scala b/tests/init-global/neg/context-sensitivity.scala index fd341945510b..626fd41bb43f 100755 --- a/tests/init-global/neg/context-sensitivity.scala +++ b/tests/init-global/neg/context-sensitivity.scala @@ -1,18 +1,17 @@ -import reflect.Selectable.reflectiveSelectable +trait Foo: + def foo(): Int -class C(var x: Int) { +class C(var x: Int) extends Foo { def foo(): Int = 20 } -class D(var y: Int) { - def foo(): Int = A.m +class D(var y: Int) extends Foo { + def foo(): Int = A.m // error } -class Box(var value: { - def foo(): Int -}) +class Box(var value: Foo) object A: val box1: Box = new Box(new C(5)) val box2: Box = new Box(new D(10)) - val m: Int = box1.value.foo() // error \ No newline at end of file + val m: Int = box1.value.foo() diff --git a/tests/init-global/neg/global-by-name.scala b/tests/init-global/neg/global-by-name.scala deleted file mode 100644 index a7227650ccf5..000000000000 --- a/tests/init-global/neg/global-by-name.scala +++ /dev/null @@ -1,11 +0,0 @@ -def time[A](f: => A): A = - val start = System.nanoTime - val res = f // error - val elapsed = (System.nanoTime - start) - res - -case class Foo(data: Int) - -object o: - val foo = time(Foo(3)) - println(foo.data) diff --git a/tests/init-global/neg/global-cycle1.check b/tests/init-global/neg/global-cycle1.check index 9c6febbb1171..e0125630c077 100644 --- a/tests/init-global/neg/global-cycle1.check +++ b/tests/init-global/neg/global-cycle1.check @@ -1,4 +1,4 @@ --- Error: tests/init/neg/global-cycle1.scala:1:7 ----------------------------------------------------------------------- +-- Error: tests/init-global/neg/global-cycle1.scala:1:7 ---------------------------------------------------------------- 1 |object A { // error |^ |Cyclic initialization: object A -> object B -> object A. Calling trace: @@ -8,7 +8,15 @@ | ^ |-> object B { [ global-cycle1.scala:5 ] | ^ - |-> val b: Int = A.a [ global-cycle1.scala:6 ] + |-> val b: Int = A.a // error [ global-cycle1.scala:6 ] | ^ 2 | val a: Int = B.b 3 |} +-- Error: tests/init-global/neg/global-cycle1.scala:6:17 --------------------------------------------------------------- +6 | val b: Int = A.a // error + | ^^^ + | Access uninitialized field value a. Call trace: + | -> object B { [ global-cycle1.scala:5 ] + | ^ + | -> val b: Int = A.a // error [ global-cycle1.scala:6 ] + | ^^^ diff --git a/tests/init-global/neg/global-cycle1.scala b/tests/init-global/neg/global-cycle1.scala index ee74b08d4a06..592f5a652dc8 100644 --- a/tests/init-global/neg/global-cycle1.scala +++ b/tests/init-global/neg/global-cycle1.scala @@ -3,7 +3,7 @@ object A { // error } object B { - val b: Int = A.a + val b: Int = A.a // error } @main diff --git a/tests/init-global/neg/global-cycle2.scala b/tests/init-global/neg/global-cycle2.scala new file mode 100644 index 000000000000..d86fe09fb0fa --- /dev/null +++ b/tests/init-global/neg/global-cycle2.scala @@ -0,0 +1,7 @@ +object A { + val a: Int = B.foo() +} + +object B { + def foo(): Int = A.a * 2 // error +} diff --git a/tests/init-global/neg/global-cycle3.scala b/tests/init-global/neg/global-cycle3.scala new file mode 100644 index 000000000000..405e007b044a --- /dev/null +++ b/tests/init-global/neg/global-cycle3.scala @@ -0,0 +1,7 @@ +class A(x: Int) { + def foo(): Int = B.a + 10 // error +} + +object B { + val a: Int = A(4).foo() +} diff --git a/tests/init-global/pos/global-cycle4.scala b/tests/init-global/neg/global-cycle4.scala similarity index 74% rename from tests/init-global/pos/global-cycle4.scala rename to tests/init-global/neg/global-cycle4.scala index be8232f25379..8c1627afeae4 100644 --- a/tests/init-global/pos/global-cycle4.scala +++ b/tests/init-global/neg/global-cycle4.scala @@ -7,13 +7,13 @@ class B extends A { } class C extends A { - def foo(): Int = O.a + 10 + def foo(): Int = O.a + 10 // error } class D(x: Int) { def bar(): A = if x > 0 then new B else new C } -object O { // error +object O { val a: Int = D(5).bar().foo() } diff --git a/tests/init-global/neg/global-cycle5.scala b/tests/init-global/neg/global-cycle5.scala index ae9540289797..1ba3435d3830 100755 --- a/tests/init-global/neg/global-cycle5.scala +++ b/tests/init-global/neg/global-cycle5.scala @@ -7,7 +7,7 @@ object A { } object B { - val b: Int = A.a.foo() + val b: Int = A.a.foo() // error } class Y extends X { @@ -19,5 +19,5 @@ object C { } def main = { - A.a = new Y(); C // error + A.a = new Y(); C } \ No newline at end of file diff --git a/tests/init-global/neg/global-cycle8.scala b/tests/init-global/neg/global-cycle8.scala index 2090b53d9894..344dc3241395 100644 --- a/tests/init-global/neg/global-cycle8.scala +++ b/tests/init-global/neg/global-cycle8.scala @@ -12,7 +12,7 @@ object O { // error } object P { - val m = Q.bar(new B: @annotation.init.expose) + val m = Q.bar(new B) } object Q { diff --git a/tests/init-global/neg/global-fun.scala b/tests/init-global/neg/global-fun.scala deleted file mode 100644 index 1b750f1b05cc..000000000000 --- a/tests/init-global/neg/global-fun.scala +++ /dev/null @@ -1,12 +0,0 @@ -class C: - def double(x: Int): Int = x * 2 - -object A: - val f: C => Int = foo(10) - - def foo(x: Int): C => Int = - c => c.double(x) // error - - -object B: - var y = A.f(new C) diff --git a/tests/init-global/neg/global-irrelevance3.scala b/tests/init-global/neg/global-irrelevance3.scala index beb2d62ea778..2f36d65d917e 100644 --- a/tests/init-global/neg/global-irrelevance3.scala +++ b/tests/init-global/neg/global-irrelevance3.scala @@ -1,5 +1,3 @@ -import scala.annotation.init - object A: class Pair(val f: Int => Unit, val g: () => Int) val p: Pair = foo() @@ -8,7 +6,7 @@ object A: var x = 6 new Pair( y => x = y, - (() => x): @init.expose // error + (() => x) // error ) diff --git a/tests/init-global/neg/global-irrelevance4.scala b/tests/init-global/neg/global-irrelevance4.scala index d5c0e6c64497..7a2a778814b2 100644 --- a/tests/init-global/neg/global-irrelevance4.scala +++ b/tests/init-global/neg/global-irrelevance4.scala @@ -1,5 +1,3 @@ -import scala.annotation.init - object A: class Pair(val f: Int => Unit, val g: () => Int) val p: Pair = foo() @@ -7,7 +5,7 @@ object A: def foo(): Pair = var x = 6 new Pair( - (y => x = y): @init.expose, // error + (y => x = y), // error () => x ) diff --git a/tests/init-global/neg/global-irrelevance6.scala b/tests/init-global/neg/global-irrelevance6.scala index 5b0daa356086..78699b6988b6 100644 --- a/tests/init-global/neg/global-irrelevance6.scala +++ b/tests/init-global/neg/global-irrelevance6.scala @@ -1,5 +1,3 @@ -import scala.annotation.init - class Box(x: Int): def foo(): Int = 100 diff --git a/tests/init-global/neg/global-irrelevance7.scala b/tests/init-global/neg/global-irrelevance7.scala index e334ec0c67a9..2c860cbc4259 100644 --- a/tests/init-global/neg/global-irrelevance7.scala +++ b/tests/init-global/neg/global-irrelevance7.scala @@ -1,11 +1,9 @@ -import scala.annotation.init - class Box(x: Int): def foo(): Int = 100 object A: val array: Array[Box] = new Array(1) - array(0) = new Box(10): @init.expose + array(0) = new Box(10) val n = array(0).foo() // ok object B: diff --git a/tests/init-global/neg/global-list.scala b/tests/init-global/neg/global-list.scala index d0c7bd32088e..cdef6dbf3bbe 100755 --- a/tests/init-global/neg/global-list.scala +++ b/tests/init-global/neg/global-list.scala @@ -1,9 +1,9 @@ case class Foo(name: String) -object O: +object O: // error val a = Foo("Apple") val b = Foo("Banana") val c = Foo("Cherry") object Foo: - val all: List[Foo] = List(O.a, O.b, O.c) // error \ No newline at end of file + val all: List[Foo] = List(O.a, O.b, O.c) // error // error // error \ No newline at end of file diff --git a/tests/init-global/pos/global-local-var.scala b/tests/init-global/neg/global-local-var.scala similarity index 85% rename from tests/init-global/pos/global-local-var.scala rename to tests/init-global/neg/global-local-var.scala index 57c180342c1f..6965a42bd37f 100644 --- a/tests/init-global/pos/global-local-var.scala +++ b/tests/init-global/neg/global-local-var.scala @@ -7,7 +7,7 @@ class A(x: Int) { sum += i i += 1 - B.a + 10 + sum + B.a + 10 + sum // error } } diff --git a/tests/init-global/neg/global-region1.scala b/tests/init-global/neg/global-region1.scala index 7d99e66cfd29..48473717b5b5 100644 --- a/tests/init-global/neg/global-region1.scala +++ b/tests/init-global/neg/global-region1.scala @@ -1,11 +1,9 @@ -import scala.annotation.init - trait B { def foo(): Int } class C(var x: Int) extends B { def foo(): Int = 20 } class D(var y: Int) extends B { def foo(): Int = A.m } // error class Box(var value: B) object A: - val box1: Box = new Box(new C(5): @init.expose) - val box2: Box = new Box(new D(10): @init.expose) + val box1: Box = new Box(new C(5)) + val box2: Box = new Box(new D(10)) val m: Int = box1.value.foo() diff --git a/tests/init-global/neg/i11262.scala b/tests/init-global/neg/i11262.scala index 5703dbe3394a..c1c01f6aad8c 100644 --- a/tests/init-global/neg/i11262.scala +++ b/tests/init-global/neg/i11262.scala @@ -1,2 +1,2 @@ object A { val x: String = B.y } // error -object B { val y: String = A.x } +object B { val y: String = A.x } // error diff --git a/tests/init-global/neg/i12544b.scala b/tests/init-global/neg/i12544b.scala index eaa6130e0894..586b88df04bd 100644 --- a/tests/init-global/neg/i12544b.scala +++ b/tests/init-global/neg/i12544b.scala @@ -5,7 +5,7 @@ object Enum: object nested: // error val a: Enum = Case - val b: Enum = f(nested.a) + val b: Enum = f(nested.a) // error def f(e: Enum): Enum = e diff --git a/tests/init-global/neg/mutable-read3.scala b/tests/init-global/neg/mutable-read3.scala index 0eaa56736063..d103e112f372 100755 --- a/tests/init-global/neg/mutable-read3.scala +++ b/tests/init-global/neg/mutable-read3.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: Array[A.Box] = new Array(1) + boxes(0) = A.box val box: A.Box = boxes(0) val x: Int = box.value // error \ No newline at end of file diff --git a/tests/init-global/neg/partial-ordering.scala b/tests/init-global/neg/partial-ordering.scala index f4b661f1c65e..1bc1b251fb72 100755 --- a/tests/init-global/neg/partial-ordering.scala +++ b/tests/init-global/neg/partial-ordering.scala @@ -1,8 +1,8 @@ -object Names: +object Names: // error val ctorString = "" val ctorName: MethodName = MethodName.apply(ctorString) class MethodName(encoded: String) object MethodName: - val ctor: MethodName = new MethodName(Names.ctorString) // error + val ctor: MethodName = new MethodName(Names.ctorString) def apply(name: String): MethodName = new MethodName(name) \ No newline at end of file diff --git a/tests/init-global/pos/global-by-name.scala b/tests/init-global/pos/global-by-name.scala index bdb3c24350f8..623d7af8335c 100644 --- a/tests/init-global/pos/global-by-name.scala +++ b/tests/init-global/pos/global-by-name.scala @@ -1,5 +1,3 @@ -import scala.annotation.init - def time[A](f: => A): A = val start = System.nanoTime val res = f @@ -9,5 +7,5 @@ def time[A](f: => A): A = case class Foo(data: Int) object o: - val foo = time(Foo(3): @init.expose) + val foo = time(Foo(3)) println(foo.data) diff --git a/tests/init-global/pos/global-cycle2.scala b/tests/init-global/pos/global-cycle2.scala deleted file mode 100644 index cfb29bf43ec0..000000000000 --- a/tests/init-global/pos/global-cycle2.scala +++ /dev/null @@ -1,7 +0,0 @@ -object A { // error - val a: Int = B.foo() -} - -object B { - def foo(): Int = A.a * 2 -} diff --git a/tests/init-global/pos/global-cycle3.scala b/tests/init-global/pos/global-cycle3.scala deleted file mode 100644 index 3d3b96fc6de0..000000000000 --- a/tests/init-global/pos/global-cycle3.scala +++ /dev/null @@ -1,7 +0,0 @@ -class A(x: Int) { - def foo(): Int = B.a + 10 -} - -object B { // error - val a: Int = A(4).foo() -} diff --git a/tests/init-global/pos/global-fun.scala b/tests/init-global/pos/global-fun.scala index 31a09416df9b..97c3cb394e81 100644 --- a/tests/init-global/pos/global-fun.scala +++ b/tests/init-global/pos/global-fun.scala @@ -1,15 +1,13 @@ -import scala.annotation.init - class C: def double(x: Int): Int = x * 2 object A: val n: Int = 10 - val f: C => Int = foo(n: @init.expose) + val f: C => Int = foo(n) def foo(x: Int): C => Int = - c => c.double(x: @init.expose) + c => c.double(x) object B: - var y = A.f(new C: @init.expose) + var y = A.f(new C) diff --git a/tests/init-global/pos/global-region1.scala b/tests/init-global/pos/global-region1.scala index e8af6d7a3d5f..b28b01d9b6da 100644 --- a/tests/init-global/pos/global-region1.scala +++ b/tests/init-global/pos/global-region1.scala @@ -6,6 +6,6 @@ class D(var y: Int) extends B { def foo(): Int = A.m } class Box(var value: B) object A: - val box1: Box = new Box(new C(5): @init.expose): @init.region - val box2: Box = new Box(new D(10): @init.expose): @init.region + val box1: Box = new Box(new C(5)): @init.region + val box2: Box = new Box(new D(10)): @init.region val m: Int = box1.value.foo() // ok diff --git a/tests/init-global/neg/global-this.scala b/tests/init-global/pos/global-this.scala similarity index 99% rename from tests/init-global/neg/global-this.scala rename to tests/init-global/pos/global-this.scala index 12570419ad22..b6807416bbd1 100755 --- a/tests/init-global/neg/global-this.scala +++ b/tests/init-global/pos/global-this.scala @@ -3,10 +3,9 @@ object NameKinds: class Info class QualifiedNameKind(tag: Int, val separator: String) extends NameKind(tag): qualifiedNameKinds(tag) = this // error - val MAX_TAG = 8 val qualifiedNameKinds = new Array[QualifiedNameKind](MAX_TAG) val QualifiedName: QualifiedNameKind = new QualifiedNameKind(0, ".") - val FlatName: QualifiedNameKind = new QualifiedNameKind(1, "$") \ No newline at end of file + val FlatName: QualifiedNameKind = new QualifiedNameKind(1, "$") From 4d4518a2ea334814a26a2d66755f4f93ad2612ab Mon Sep 17 00:00:00 2001 From: Fengyun Liu Date: Mon, 1 May 2023 16:53:35 +0200 Subject: [PATCH 098/113] Align syntax with paper --- .../dotty/tools/dotc/core/Definitions.scala | 8 +++-- .../src/dotty/tools/dotc/core/Types.scala | 9 +++++ .../tools/dotc/transform/init/Objects.scala | 35 ++++++++++++------- library/src/scala/annotation/init.scala | 33 ++++++----------- tests/init-global/pos/global-region1.scala | 6 ++-- 5 files changed, 50 insertions(+), 41 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/core/Definitions.scala b/compiler/src/dotty/tools/dotc/core/Definitions.scala index 083efdb419bf..ca92364c5b9b 100644 --- a/compiler/src/dotty/tools/dotc/core/Definitions.scala +++ b/compiler/src/dotty/tools/dotc/core/Definitions.scala @@ -997,9 +997,6 @@ class Definitions { @tu lazy val DeprecatedOverridingAnnot: ClassSymbol = requiredClass("scala.deprecatedOverriding") @tu lazy val ImplicitAmbiguousAnnot: ClassSymbol = requiredClass("scala.annotation.implicitAmbiguous") @tu lazy val ImplicitNotFoundAnnot: ClassSymbol = requiredClass("scala.annotation.implicitNotFound") - @tu lazy val InitWidenAnnot: ClassSymbol = requiredClass("scala.annotation.init.widen") - @tu lazy val InitExposeAnnot: ClassSymbol = requiredClass("scala.annotation.init.expose") - @tu lazy val InitRegionAnnot: ClassSymbol = requiredClass("scala.annotation.init.region") @tu lazy val InlineParamAnnot: ClassSymbol = requiredClass("scala.annotation.internal.InlineParam") @tu lazy val ErasedParamAnnot: ClassSymbol = requiredClass("scala.annotation.internal.ErasedParam") @tu lazy val InvariantBetweenAnnot: ClassSymbol = requiredClass("scala.annotation.internal.InvariantBetween") @@ -1050,6 +1047,11 @@ class Definitions { @tu lazy val JavaRepeatableAnnot: ClassSymbol = requiredClass("java.lang.annotation.Repeatable") + // Initialization annotations + @tu lazy val InitModule: Symbol = requiredModule("scala.annotation.init") + @tu lazy val InitWidenAnnot: ClassSymbol = InitModule.requiredClass("widen") + @tu lazy val InitRegionMethod: Symbol = InitModule.requiredMethod("region") + // A list of meta-annotations that are relevant for fields and accessors @tu lazy val NonBeanMetaAnnots: Set[Symbol] = Set(FieldMetaAnnot, GetterMetaAnnot, ParamMetaAnnot, SetterMetaAnnot, CompanionClassMetaAnnot, CompanionMethodMetaAnnot) diff --git a/compiler/src/dotty/tools/dotc/core/Types.scala b/compiler/src/dotty/tools/dotc/core/Types.scala index bbec037ebef1..6db7cdfcdd23 100644 --- a/compiler/src/dotty/tools/dotc/core/Types.scala +++ b/compiler/src/dotty/tools/dotc/core/Types.scala @@ -376,6 +376,15 @@ object Types { case _ => false } + /** Returns the annoation that is an instance of `cls` carried by the type. */ + @tailrec final def getAnnotation(cls: ClassSymbol)(using Context): Option[Annotation] = stripTypeVar match { + case AnnotatedType(tp, annot) => + if annot.matches(cls) then Some(annot) + else tp.getAnnotation(cls) + case _ => + None + } + /** Does this type have a supertype with an annotation satisfying given predicate `p`? */ def derivesAnnotWith(p: Annotation => Boolean)(using Context): Boolean = this match { case tp: AnnotatedType => p(tp.annot) || tp.parent.derivesAnnotWith(p) diff --git a/compiler/src/dotty/tools/dotc/transform/init/Objects.scala b/compiler/src/dotty/tools/dotc/transform/init/Objects.scala index 2540c32d4a42..76dcb1e5c91a 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Objects.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Objects.scala @@ -911,6 +911,15 @@ object Objects: instantiate(outer, cls, ctor, args) } + case Apply(ref, arg :: Nil) if ref.symbol == defn.InitRegionMethod => + val regions2 = Regions.extend(expr.sourcePos) + if Regions.exists(expr.sourcePos) then + report.warning("Cyclic region detected. Trace: " + Trace.show, expr) + Bottom + else + given Regions.Data = regions2 + eval(arg, thisV, klass) + case Call(ref, argss) => // check args val args = evalArgs(argss.flatten, thisV, klass) @@ -963,14 +972,6 @@ object Objects: case Typed(expr, tpt) => if tpt.tpe.hasAnnotation(defn.UncheckedAnnot) then Bottom - else if tpt.tpe.hasAnnotation(defn.InitRegionAnnot) then - val regions2 = Regions.extend(tpt.sourcePos) - if Regions.exists(tpt.sourcePos) then - report.warning("Cyclic region detected. Trace: " + Trace.show, tpt.sourcePos) - Bottom - else - given Regions.Data = regions2 - eval(expr, thisV, klass) else eval(expr, thisV, klass) @@ -1144,11 +1145,21 @@ object Objects: else eval(arg.tree, thisV, klass) - // TODO: handle @widen(n) val widened = - if arg.tree.tpe.hasAnnotation(defn.InitExposeAnnot) then - res.widen(1) - else + arg.tree.tpe.getAnnotation(defn.InitWidenAnnot) match + case Some(annot) => + annot.argument(0).get match + case arg @ Literal(c: Constants.Constant) => + val height = c.intValue + if height < 0 then + report.error("The argument should be positive", arg) + res.widen(1) + else + res.widen(c.intValue) + case arg => + report.error("The argument should be a constant integer value", arg) + res.widen(1) + case _ => res.widen(1) argInfos += TraceValue(widened, trace.add(arg.tree)) diff --git a/library/src/scala/annotation/init.scala b/library/src/scala/annotation/init.scala index 9d1e15b36c09..7a452abcea92 100644 --- a/library/src/scala/annotation/init.scala +++ b/library/src/scala/annotation/init.scala @@ -8,15 +8,6 @@ package scala.annotation object init: /** Widen the abstract value of the argument so that its height is below the specified height. - * - * This is an advanced version of `@init.expose`, where we tell the compiler the annotated method or - * constructor argument are not cold aliases and its abstract value may have a maximum height of the - * specified value. - */ - final class widen(hight: Int) extends StaticAnnotation - - /** Expose the method argument or constructor argument so that it may be actively used during the - * initialization of static objects. * * It can be used to mark method or constructor arguments, as the following example shows: * @@ -28,19 +19,15 @@ object init: * * def build(o: A) = new A(o.square()) // calling methods on parameter * - * By default, method and constructor arguments are cold aliases, which means they may not be actively - * used --- calling methods or accessing fields on them are forbidden. By using `@expose` to the - * method argument, we tell the compiler that the argument is not a cold alias. As a result, we may - * call methods on the parameter. - * - * It is semantically equivalent to `@init.widen(1)`. + * By default, method and constructor arguments are widened to height 1. */ - final class expose extends StaticAnnotation + final class widen(hight: Int) extends StaticAnnotation - /** Mark a region context. + /** Introduce a region context. + * + * The same mutable field in the same region have the same abstract representation. * - * The same mutable field of objects in the same region have the same shape. The concept of regions is an - * attempt to make context-sensitivity explainable and customizable. + * The concept of regions is intended to make context-sensitivity tuable for complex use cases. * * Example: * @@ -50,13 +37,13 @@ object init: * class Box(var value: B) * * object A: - * val box1: Box = new Box(new C(5)): @init.region - * val box2: Box = new Box(new D(10)): @init.region + * val box1: Box = region { new Box(new C(5)) } + * val box2: Box = region { new Box(new D(10)) } * val m: Int = box1.value.foo() * - * In the above, without the two region annotation, the two objects `box1` and `box2` are of the same region. + * In the above, without the two region annotation, the two objects `box1` and `box2` are in the same region. * Therefore, the field `box1.value` and `box2.value` points to both instances of `C` and `D`. Consequently, * the method call `box1.value.foo()` will be invalid, because it reaches `A.m`, which is not yet initialized. * The explicit context annotation solves the problem. */ - final class region extends StaticAnnotation \ No newline at end of file + def region[T](v: T): T = v diff --git a/tests/init-global/pos/global-region1.scala b/tests/init-global/pos/global-region1.scala index b28b01d9b6da..db56fe45e1a4 100644 --- a/tests/init-global/pos/global-region1.scala +++ b/tests/init-global/pos/global-region1.scala @@ -1,4 +1,4 @@ -import scala.annotation.init +import scala.annotation.init.region trait B { def foo(): Int } class C(var x: Int) extends B { def foo(): Int = 20 } @@ -6,6 +6,6 @@ class D(var y: Int) extends B { def foo(): Int = A.m } class Box(var value: B) object A: - val box1: Box = new Box(new C(5)): @init.region - val box2: Box = new Box(new D(10)): @init.region + val box1: Box = region { new Box(new C(5)) } + val box2: Box = region { new Box(new D(10)) } val m: Int = box1.value.foo() // ok From 3e64c6b853824955a0c933a29248dea9e0cb50a3 Mon Sep 17 00:00:00 2001 From: Fengyun Liu Date: Tue, 2 May 2023 07:30:42 +0200 Subject: [PATCH 099/113] Fix accidental changes to community build --- community-build/community-projects/scala-parallel-collections | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/community-build/community-projects/scala-parallel-collections b/community-build/community-projects/scala-parallel-collections index a6bd648bb188..7d0e41ae4d09 160000 --- a/community-build/community-projects/scala-parallel-collections +++ b/community-build/community-projects/scala-parallel-collections @@ -1 +1 @@ -Subproject commit a6bd648bb188a65ab36be07e956e52fe25f64d67 +Subproject commit 7d0e41ae4d09e1ddf063651e377921ec493fc5bf From 1aacd59623c558e1350eeec246a19921bacef064 Mon Sep 17 00:00:00 2001 From: Fengyun Liu Date: Mon, 22 May 2023 07:10:27 +0200 Subject: [PATCH 100/113] Fix typos MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: EnzeXing <58994529+EnzeXing@users.noreply.github.com> Co-authored-by: Ondřej Lhoták --- compiler/src/dotty/tools/dotc/core/Types.scala | 2 +- .../src/dotty/tools/dotc/transform/init/Objects.scala | 8 ++++---- library/src/scala/annotation/init.scala | 6 +++--- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/core/Types.scala b/compiler/src/dotty/tools/dotc/core/Types.scala index 6db7cdfcdd23..adf375344537 100644 --- a/compiler/src/dotty/tools/dotc/core/Types.scala +++ b/compiler/src/dotty/tools/dotc/core/Types.scala @@ -376,7 +376,7 @@ object Types { case _ => false } - /** Returns the annoation that is an instance of `cls` carried by the type. */ + /** Returns the annotation that is an instance of `cls` carried by the type. */ @tailrec final def getAnnotation(cls: ClassSymbol)(using Context): Option[Annotation] = stripTypeVar match { case AnnotatedType(tp, annot) => if annot.matches(cls) then Some(annot) diff --git a/compiler/src/dotty/tools/dotc/transform/init/Objects.scala b/compiler/src/dotty/tools/dotc/transform/init/Objects.scala index 76dcb1e5c91a..155100507762 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Objects.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Objects.scala @@ -123,7 +123,7 @@ object Objects: def show(using Context) = "ObjectRef(" + klass.show + ")" /** - * Rerepsents values that are instances of the specified class. + * Represents values that are instances of the specified class. * * Note that the 2nd parameter block does not take part in the definition of equality. */ @@ -150,7 +150,7 @@ object Objects: instance /** - * Rerepsents arrays. + * Represents arrays. * * Note that the 2nd parameter block does not take part in the definition of equality. * @@ -289,7 +289,7 @@ object Objects: val level = outer.level + 1 if (level > 3) - report.warning("[Internal error] Deeply nested environemnt, level = " + level + ", " + meth.show + " in " + meth.enclosingClass.show, meth.defTree) + report.warning("[Internal error] Deeply nested environment, level = " + level + ", " + meth.show + " in " + meth.enclosingClass.show, meth.defTree) private[Env] val vals: mutable.Map[Symbol, Value] = valsMap private[Env] val vars: mutable.Map[Symbol, Heap.Addr] = varsMap @@ -418,7 +418,7 @@ object Objects: */ opaque type Data = Map[Addr, Value] - /** Store the heap as a mutable field to avoid thread through it in the program. */ + /** Store the heap as a mutable field to avoid threading it through the program. */ class MutableData(private[Heap] var heap: Data): private[Heap] def update(addr: Addr, value: Value): Unit = heap.get(addr) match diff --git a/library/src/scala/annotation/init.scala b/library/src/scala/annotation/init.scala index 7a452abcea92..9535dd052f8e 100644 --- a/library/src/scala/annotation/init.scala +++ b/library/src/scala/annotation/init.scala @@ -21,13 +21,13 @@ object init: * * By default, method and constructor arguments are widened to height 1. */ - final class widen(hight: Int) extends StaticAnnotation + final class widen(height: Int) extends StaticAnnotation /** Introduce a region context. * * The same mutable field in the same region have the same abstract representation. * - * The concept of regions is intended to make context-sensitivity tuable for complex use cases. + * The concept of regions is intended to make context-sensitivity tunable for complex use cases. * * Example: * @@ -41,7 +41,7 @@ object init: * val box2: Box = region { new Box(new D(10)) } * val m: Int = box1.value.foo() * - * In the above, without the two region annotation, the two objects `box1` and `box2` are in the same region. + * In the above, without the two region annotations, the two objects `box1` and `box2` are in the same region. * Therefore, the field `box1.value` and `box2.value` points to both instances of `C` and `D`. Consequently, * the method call `box1.value.foo()` will be invalid, because it reaches `A.m`, which is not yet initialized. * The explicit context annotation solves the problem. From a8d0b7f57f689137037d26099d3f9bc721a0b6a9 Mon Sep 17 00:00:00 2001 From: Fengyun Liu Date: Mon, 22 May 2023 07:23:55 +0200 Subject: [PATCH 101/113] Fix typo MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Ondřej Lhoták --- compiler/src/dotty/tools/dotc/transform/init/Objects.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/compiler/src/dotty/tools/dotc/transform/init/Objects.scala b/compiler/src/dotty/tools/dotc/transform/init/Objects.scala index 155100507762..ef12d9bcdfc6 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Objects.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Objects.scala @@ -1330,7 +1330,7 @@ object Objects: Bottom } - /** Compute the outer value that correspond to `tref.prefix` + /** Compute the outer value that corresponds to `tref.prefix` * * @param tref The type whose prefix is to be evaluated. * @param thisV The value for `C.this` where `C` is represented by the parameter `klass`. From 47bbeba976bc0243461b2ccc5102e783ca50372c Mon Sep 17 00:00:00 2001 From: Fengyun Liu Date: Mon, 22 May 2023 07:26:11 +0200 Subject: [PATCH 102/113] Fix typo MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Ondřej Lhoták --- library/src/scala/annotation/init.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/src/scala/annotation/init.scala b/library/src/scala/annotation/init.scala index 9535dd052f8e..a1ff37b78e32 100644 --- a/library/src/scala/annotation/init.scala +++ b/library/src/scala/annotation/init.scala @@ -12,7 +12,7 @@ object init: * It can be used to mark method or constructor arguments, as the following example shows: * * class A(x: Int): - * def squre(): Int = x*x + * def square(): Int = x*x * * object B: * val a = build(new A(10): @init.expose) // <-- usage From d79c1ec07cbcd7ec08f0fc2e000e8e3266aeb78e Mon Sep 17 00:00:00 2001 From: Fengyun Liu Date: Mon, 22 May 2023 07:59:24 +0200 Subject: [PATCH 103/113] Add documentation to code --- .../tools/dotc/transform/init/Objects.scala | 66 +++++++++++++++++-- library/src/scala/annotation/init.scala | 12 +++- 2 files changed, 70 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 ef12d9bcdfc6..e9c00c2307a6 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Objects.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Objects.scala @@ -132,7 +132,7 @@ object Objects: valsMap: mutable.Map[Symbol, Value], varsMap: mutable.Map[Symbol, Heap.Addr], outersMap: mutable.Map[ClassSymbol, Value]) extends Ref(valsMap, varsMap, outersMap): def widenedCopy(outer: Value, args: List[Value], env: Env.Data): OfClass = - new OfClass(klass, outer, ctor, args, env)(this.valsMap, this.varsMap, outersMap) + new OfClass(klass, outer, ctor, args, env)(this.valsMap, this.varsMap, this.outersMap) def show(using Context) = val valFields = vals.map(_.show + " -> " + _.show) @@ -379,6 +379,8 @@ object Objects: * @param meth The method which owns the environment * @param thisV The value for `this` of the enclosing class where the local variable is referenced. * @param env The local environment where the local variable is referenced. + * + * @return the environment and value for `this` owned by the given method. */ def resolveEnv(meth: Symbol, thisV: Value, env: Data)(using Context): Option[(Value, Data)] = log("Resolving env for " + meth.show + ", this = " + thisV.show + ", env = " + env.show, printer) { env match @@ -528,6 +530,15 @@ object Objects: def widen(height: Int): Contextual[List[Value]] = values.map(_.widen(height)).toList + /** Handle method calls `e.m(args)`. + * + * @param value The value for the receiver. + * @param meth The symbol of the target method (could be virtual or abstract method). + * @param args Arguments of the method call (all parameter blocks flatten to a list). + * @param receiver The type of the receiver. + * @param superType The type of the super in a super call. NoType for non-super calls. + * @param needResolve Whether the target of the call needs resolution? + */ def call(value: Value, meth: Symbol, args: List[ArgInfo], receiver: Type, superType: Type, needResolve: Boolean = true): Contextual[Value] = log("call " + meth.show + ", args = " + args.map(_.value.show), printer, (_: Value).show) { value match case Cold => @@ -618,6 +629,12 @@ object Objects: vs.map(v => call(v, meth, args, receiver, superType)).join } + /** Handle constructor calls `(args)`. + * + * @param thisV The value for the receiver. + * @param ctor The symbol of the target method. + * @param args Arguments of the constructor call (all parameter blocks flatten to a list). + */ def callConstructor(thisV: Value, ctor: Symbol, args: List[ArgInfo]): Contextual[Value] = log("call " + ctor.show + ", args = " + args.map(_.value.show), printer, (_: Value).show) { thisV match @@ -643,6 +660,13 @@ object Objects: Bottom } + /** Handle selection `e.f`. + * + * @param value The value for the receiver. + * @param field The symbol of the target field (could be virtual or abstract). + * @param receiver The type of the receiver. + * @param needResolve Whether the target of the selection needs resolution? + */ def select(thisV: Value, field: Symbol, receiver: Type, needResolve: Boolean = true): Contextual[Value] = log("select " + field.show + ", this = " + thisV.show, printer, (_: Value).show) { thisV match case Cold => @@ -700,8 +724,15 @@ object Objects: refs.map(ref => select(ref, field, receiver)).join } - def assign(receiver: Value, field: Symbol, rhs: Value, rhsTyp: Type): Contextual[Value] = log("Assign" + field.show + " of " + receiver.show + ", rhs = " + rhs.show, printer, (_: Value).show) { - receiver match + /** Handle assignment `lhs.f = rhs`. + * + * @param lhs The value of the object to be mutated. + * @param field The symbol of the target field. + * @param lhs The value to be assigned. + * @param rhsTyp The type of the right-hand side. + */ + def assign(lhs: Value, field: Symbol, rhs: Value, rhsTyp: Type): Contextual[Value] = log("Assign" + field.show + " of " + lhs.show + ", rhs = " + rhs.show, printer, (_: Value).show) { + lhs match case fun: Fun => report.error("[Internal error] unexpected tree in assignment, fun = " + fun.code.show + Trace.show, Trace.position) @@ -727,6 +758,13 @@ object Objects: Bottom } + /** Handle new expression `new p.C(args)`. + * + * @param outer The value for `p`. + * @param klass The symbol of the class `C`. + * @param ctor The symbol of the target constructor. + * @param args The arguments passsed to the constructor. + */ def instantiate(outer: Value, klass: ClassSymbol, ctor: Symbol, args: List[ArgInfo]): Contextual[Value] = log("instantiating " + klass.show + ", outer = " + outer + ", args = " + args.map(_.value.show), printer, (_: Value).show) { outer match @@ -758,6 +796,12 @@ object Objects: refs.map(ref => instantiate(ref, klass, ctor, args)).join } + /** Handle local variable definition, `val x = e` or `var x = e`. + * + * @param ref The value for `this` where the variable is defined. + * @param sym The symbol of the variable. + * @param value The value of the initializer. + */ def initLocal(ref: Ref, sym: Symbol, value: Value): Contextual[Unit] = log("initialize local " + sym.show + " with " + value.show, printer) { if sym.is(Flags.Mutable) then val addr = Heap.localVarAddr(summon[Regions.Data], sym, State.currentObject) @@ -767,6 +811,11 @@ object Objects: Env.setLocalVal(sym, value) } + /** Read local variable `x`. + * + * @param thisV The value for `this` where the variable is used. + * @param sym The symbol of the variable. + */ def readLocal(thisV: Value, sym: Symbol): Contextual[Value] = log("reading local " + sym.show, printer, (_: Value).show) { Env.resolveEnv(sym.enclosingMethod, thisV, summon[Env.Data]) match case Some(thisV -> env) => @@ -814,6 +863,12 @@ object Objects: Cold } + /** Handle local variable assignmenbt, `x = e`. + * + * @param thisV The value for `this` where the assignment locates. + * @param sym The symbol of the variable. + * @param value The value of the rhs of the assignment. + */ def writeLocal(thisV: Value, sym: Symbol, value: Value): Contextual[Value] = log("write local " + sym.show + " with " + value.show, printer, (_: Value).show) { assert(sym.is(Flags.Mutable), "Writing to immutable variable " + sym.show) @@ -1010,7 +1065,8 @@ object Objects: eval(expr, thisV, klass) case If(cond, thenp, elsep) => - evalExprs(cond :: thenp :: elsep :: Nil, thisV, klass).join + eval(cond, thisV, klass) + evalExprs(thenp :: elsep :: Nil, thisV, klass).join case Annotated(arg, annot) => if expr.tpe.hasAnnotation(defn.UncheckedAnnot) then @@ -1309,7 +1365,7 @@ object Objects: Bottom else if target.isStaticObject then val res = ObjectRef(target.moduleClass.asClass) - if target == klass || elideObjectAccess then res + if elideObjectAccess then res else accessObject(target) else thisV match diff --git a/library/src/scala/annotation/init.scala b/library/src/scala/annotation/init.scala index a1ff37b78e32..62c875fc12e4 100644 --- a/library/src/scala/annotation/init.scala +++ b/library/src/scala/annotation/init.scala @@ -14,12 +14,18 @@ object init: * class A(x: Int): * def square(): Int = x*x * + * class C(val a: A) + * * object B: - * val a = build(new A(10): @init.expose) // <-- usage + * val a = build(new C(new A(10)): @widen(2)) // <-- usage * - * def build(o: A) = new A(o.square()) // calling methods on parameter + * def build(c: C) = new A(c.a.square()) // calling methods on parameter * - * By default, method and constructor arguments are widened to height 1. + * By default, method and constructor arguments are widened to height 1. In + * the code above, without using `@widen(2)` we will have the abstract value + * `C(a = Cold)` for the argument `c` of the method `build`. Consequently, + * the checker will issue a warning for the method call `c.a.square()` because + * it is forbidden to call methods or access fields on cold values. */ final class widen(height: Int) extends StaticAnnotation From 81e05623711f1863224390767b6b7220cec478cc Mon Sep 17 00:00:00 2001 From: Fengyun Liu Date: Tue, 23 May 2023 20:13:27 +0200 Subject: [PATCH 104/113] Apply suggestions from code review Co-authored-by: q-ata <24601033+q-ata@users.noreply.github.com> --- compiler/src/dotty/tools/dotc/transform/init/Objects.scala | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/transform/init/Objects.scala b/compiler/src/dotty/tools/dotc/transform/init/Objects.scala index e9c00c2307a6..9ec9a23d76ce 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Objects.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Objects.scala @@ -100,7 +100,7 @@ object Objects: def initVal(field: Symbol, value: Value)(using Context) = log("Initialize " + field.show + " = " + value + " for " + this, printer) { assert(!field.is(Flags.Mutable), "Field is mutable: " + field.show) - assert(!vals.contains(field), "Field already set " + field.show) + assert(!vals.contains(field), "Field already set: " + field.show) vals(field) = value } @@ -111,7 +111,7 @@ object Objects: } def initOuter(cls: ClassSymbol, value: Value)(using Context) = log("Initialize outer " + cls.show + " = " + value + " for " + this, printer) { - assert(!outers.contains(cls), "Outer already set " + cls) + assert(!outers.contains(cls), "Outer already set: " + cls) outers(cls) = value } @@ -1355,7 +1355,7 @@ object Objects: * @param klass The enclosing class where the type `C.this` is located. * @param elideObjectAccess Whether object access should be omitted. * - * Object access elission happens when the object access is used as a prefix + * Object access elision happens when the object access is used as a prefix * in `new o.C` and `C` does not need an outer. */ def resolveThis(target: ClassSymbol, thisV: Value, klass: ClassSymbol, elideObjectAccess: Boolean = false): Contextual[Value] = log("resolveThis target = " + target.show + ", this = " + thisV.show, printer, (_: Value).show) { From 4cdfa545bba136b23115540013439626a4bb0078 Mon Sep 17 00:00:00 2001 From: Fengyun Liu Date: Sat, 27 May 2023 07:56:24 +0200 Subject: [PATCH 105/113] Address review: factor out isByNameParam --- .../src/dotty/tools/dotc/transform/init/Objects.scala | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/transform/init/Objects.scala b/compiler/src/dotty/tools/dotc/transform/init/Objects.scala index 9ec9a23d76ce..fb2865244867 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Objects.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Objects.scala @@ -648,7 +648,6 @@ object Objects: if ctor.isPrimaryConstructor then val tpl = cls.defTree.asInstanceOf[TypeDef].rhs.asInstanceOf[Template] extendTrace(cls.defTree) { eval(tpl, ref, cls, cacheResult = true) } - ref else extendTrace(ddef) { eval(ddef.rhs, ref, cls, cacheResult = true) } else @@ -817,6 +816,7 @@ object Objects: * @param sym The symbol of the variable. */ def readLocal(thisV: Value, sym: Symbol): Contextual[Value] = log("reading local " + sym.show, printer, (_: Value).show) { + def isByNameParam(sym: Symbol) = sym.is(Flags.Param) && sym.info.isInstanceOf[ExprType] Env.resolveEnv(sym.enclosingMethod, thisV, summon[Env.Data]) match case Some(thisV -> env) => if sym.is(Flags.Mutable) then @@ -837,7 +837,7 @@ object Objects: try // Assume forward reference check is doing a good job val value = Env.valValue(sym) - if sym.is(Flags.Param) && sym.info.isInstanceOf[ExprType] then + if isByNameParam(sym) then value match case fun: Fun => given Env.Data = fun.env @@ -856,7 +856,7 @@ object Objects: Bottom case _ => - if sym.is(Flags.Param) && sym.info.isInstanceOf[ExprType] then + if isByNameParam(sym) then report.warning("Calling cold by-name alias. Call trace: \n" + Trace.show, Trace.position) Bottom else @@ -1089,10 +1089,10 @@ object Objects: eval(expr, thisV, klass) case Try(block, cases, finalizer) => - eval(block, thisV, klass) + val res = evalExprs(block :: cases.map(_.body), thisV, klass).join if !finalizer.isEmpty then eval(finalizer, thisV, klass) - evalExprs(cases.map(_.body), thisV, klass).join + res case SeqLiteral(elems, elemtpt) => evalExprs(elems, thisV, klass).join From b5d85313f7eb162c4831b37d5b5a23b33d032e16 Mon Sep 17 00:00:00 2001 From: Fengyun Liu Date: Sat, 27 May 2023 08:11:07 +0200 Subject: [PATCH 106/113] Add comment to code --- compiler/src/dotty/tools/dotc/transform/init/Objects.scala | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/transform/init/Objects.scala b/compiler/src/dotty/tools/dotc/transform/init/Objects.scala index fb2865244867..c0f0607ee7d4 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Objects.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Objects.scala @@ -1179,6 +1179,7 @@ object Objects: if sym.is(Flags.Package) then Bottom else if sym.isStaticObject && sym != klass then + // The typer may use ThisType to refer to an object outside its definition. if elideObjectAccess then ObjectRef(sym.moduleClass.asClass) else @@ -1404,7 +1405,7 @@ object Objects: def errorMutateOtherStaticObject(currentObj: ClassSymbol, otherObj: ClassSymbol)(using Trace, Context) = val msg = s"Mutating ${otherObj.show} during initialization of ${currentObj.show}.\n" + - "Mutating other static objects during the initialization of one static object is discouraged. " + + "Mutating other static objects during the initialization of one static object is forbidden. " + "Calling trace:\n" + Trace.show report.warning(msg, Trace.position) @@ -1412,7 +1413,7 @@ object Objects: def errorReadOtherStaticObject(currentObj: ClassSymbol, otherObj: ClassSymbol)(using Trace, Context) = val msg = "Reading mutable state of " + otherObj.show + " during initialization of " + currentObj.show + ".\n" + - "Reading mutable state of other static objects is discouraged as it breaks initialization-time irrelevance. " + + "Reading mutable state of other static objects is forbidden as it breaks initialization-time irrelevance. " + "Calling trace: " + Trace.show report.warning(msg, Trace.position) From 06384584baa13f45966a1db8a1874604befbb65a Mon Sep 17 00:00:00 2001 From: Fengyun Liu Date: Mon, 5 Jun 2023 20:29:58 +0200 Subject: [PATCH 107/113] Fix typo in doc Co-authored-by: EnzeXing <58994529+EnzeXing@users.noreply.github.com> --- compiler/src/dotty/tools/dotc/transform/init/Objects.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/compiler/src/dotty/tools/dotc/transform/init/Objects.scala b/compiler/src/dotty/tools/dotc/transform/init/Objects.scala index c0f0607ee7d4..53e81d9d2581 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Objects.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Objects.scala @@ -727,7 +727,7 @@ object Objects: * * @param lhs The value of the object to be mutated. * @param field The symbol of the target field. - * @param lhs The value to be assigned. + * @param rhs The value to be assigned. * @param rhsTyp The type of the right-hand side. */ def assign(lhs: Value, field: Symbol, rhs: Value, rhsTyp: Type): Contextual[Value] = log("Assign" + field.show + " of " + lhs.show + ", rhs = " + rhs.show, printer, (_: Value).show) { From daf12de846082e292ebec9df953b16d7cd928c20 Mon Sep 17 00:00:00 2001 From: Fengyun Liu Date: Sat, 10 Jun 2023 16:19:45 +0200 Subject: [PATCH 108/113] Handle return properly --- .../tools/dotc/transform/init/Objects.scala | 40 +++++++++++++++++-- tests/init-global/neg/return.scala | 10 +++++ tests/init-global/neg/return2.scala | 13 ++++++ 3 files changed, 60 insertions(+), 3 deletions(-) create mode 100755 tests/init-global/neg/return.scala create mode 100755 tests/init-global/neg/return2.scala diff --git a/compiler/src/dotty/tools/dotc/transform/init/Objects.scala b/compiler/src/dotty/tools/dotc/transform/init/Objects.scala index 53e81d9d2581..25e46191fc63 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Objects.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Objects.scala @@ -212,6 +212,7 @@ object Objects: given Trace = Trace.empty.add(classSym.defTree) given Env.Data = Env.emptyEnv(tpl.constr.symbol) given Heap.MutableData = Heap.empty() + given returns: Returns.Data = Returns.empty() given regions: Regions.Data = Regions.empty // explicit name to avoid naming conflict val obj = ObjectRef(classSym) @@ -487,7 +488,33 @@ object Objects: inline def cache(using c: Cache.Data): Cache.Data = c - type Contextual[T] = (Context, State.Data, Env.Data, Cache.Data, Heap.MutableData, Regions.Data, Trace) ?=> T + + /** + * Handle return statements in methods and non-local returns in functions. + */ + object Returns: + private class ReturnData(val method: Symbol, val values: mutable.ArrayBuffer[Value]) + opaque type Data = mutable.ArrayBuffer[ReturnData] + + def empty(): Data = mutable.ArrayBuffer() + + def installHandler(meth: Symbol)(using data: Data): Unit = + data.addOne(ReturnData(meth, mutable.ArrayBuffer())) + + def popHandler(meth: Symbol)(using data: Data): Value = + val returnData = data.remove(data.size - 1) + assert(returnData.method == meth, "Symbol mismatch in return handlers, expect = " + meth + ", found = " + returnData.method) + returnData.values.join + + def handle(meth: Symbol, value: Value)(using data: Data, trace: Trace, ctx: Context): Unit = + data.findLast(_.method == meth) match + case Some(returnData) => + returnData.values.addOne(value) + + case None => + report.error("[Internal error] Unhandled return for method " + meth + " in " + meth.owner.show + ". Trace:\n" + Trace.show, Trace.position) + + type Contextual[T] = (Context, State.Data, Env.Data, Cache.Data, Heap.MutableData, Regions.Data, Returns.Data, Trace) ?=> T // --------------------------- domain operations ----------------------------- @@ -595,7 +622,13 @@ object Objects: val env2 = Env.of(ddef, args.map(_.value), outerEnv) extendTrace(ddef) { given Env.Data = env2 - eval(ddef.rhs, ref, cls, cacheResult = true) + // eval(ddef.rhs, ref, cls, cacheResult = true) + cache.cachedEval(ref, ddef.rhs, cacheResult = true) { expr => + Returns.installHandler(meth) + val res = cases(expr, thisV, cls) + val returns = Returns.popHandler(meth) + res.join(returns) + } } else Bottom @@ -1079,7 +1112,8 @@ object Objects: evalExprs(cases.map(_.body), thisV, klass).join case Return(expr, from) => - eval(expr, thisV, klass) + Returns.handle(from.symbol, eval(expr, thisV, klass)) + Bottom case WhileDo(cond, body) => evalExprs(cond :: body :: Nil, thisV, klass) diff --git a/tests/init-global/neg/return.scala b/tests/init-global/neg/return.scala new file mode 100755 index 000000000000..5cbf6915fc0e --- /dev/null +++ b/tests/init-global/neg/return.scala @@ -0,0 +1,10 @@ +object A: + def foo(x: Int): Int => Int = + if x <= 0 then + return (a: Int) => a + B.n // error + + (a: Int) => a * a + x + +object B: + val n = A.foo(-10)(20) + diff --git a/tests/init-global/neg/return2.scala b/tests/init-global/neg/return2.scala new file mode 100755 index 000000000000..6a4dec50c2dd --- /dev/null +++ b/tests/init-global/neg/return2.scala @@ -0,0 +1,13 @@ +object A: + def foo(x: Int): Int => Int = + val f = (a: Int) => a + B.n // error + var i = 0 + + val g = () => return f + + if x <= 0 then g() + + (a: Int) => a * a + x + +object B: + val n = A.foo(-10)(20) From ccb79837f01bd811da07dd76167f6a01b47baefd Mon Sep 17 00:00:00 2001 From: Fengyun Liu Date: Mon, 12 Jun 2023 19:55:26 +0200 Subject: [PATCH 109/113] Use warning instead of error for error state of init check --- .../tools/dotc/transform/init/Objects.scala | 24 +++++++++---------- .../tools/dotc/transform/init/Semantic.scala | 24 +++++++++---------- 2 files changed, 24 insertions(+), 24 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/transform/init/Objects.scala b/compiler/src/dotty/tools/dotc/transform/init/Objects.scala index 25e46191fc63..fe5827251816 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Objects.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Objects.scala @@ -512,7 +512,7 @@ object Objects: returnData.values.addOne(value) case None => - report.error("[Internal error] Unhandled return for method " + meth + " in " + meth.owner.show + ". Trace:\n" + Trace.show, Trace.position) + report.warning("[Internal error] Unhandled return for method " + meth + " in " + meth.owner.show + ". Trace:\n" + Trace.show, Trace.position) type Contextual[T] = (Context, State.Data, Env.Data, Cache.Data, Heap.MutableData, Regions.Data, Returns.Data, Trace) ?=> T @@ -636,7 +636,7 @@ object Objects: select(ref, target, receiver, needResolve = false) else if ref.klass.isSubClass(receiver.widenSingleton.classSymbol) then - report.error("[Internal error] Unexpected resolution failure: ref.klass = " + ref.klass.show + ", meth = " + meth.show + Trace.show, Trace.position) + report.warning("[Internal error] Unexpected resolution failure: ref.klass = " + ref.klass.show + ", meth = " + meth.show + Trace.show, Trace.position) Bottom else // This is possible due to incorrect type cast. @@ -688,7 +688,7 @@ object Objects: Bottom case _ => - report.error("[Internal error] unexpected constructor call, meth = " + ctor + ", this = " + thisV + Trace.show, Trace.position) + report.warning("[Internal error] unexpected constructor call, meth = " + ctor + ", this = " + thisV + Trace.show, Trace.position) Bottom } @@ -737,7 +737,7 @@ object Objects: else if ref.klass.isSubClass(receiver.widenSingleton.classSymbol) then - report.error("[Internal error] Unexpected resolution failure: ref.klass = " + ref.klass.show + ", field = " + field.show + Trace.show, Trace.position) + report.warning("[Internal error] Unexpected resolution failure: ref.klass = " + ref.klass.show + ", field = " + field.show + Trace.show, Trace.position) Bottom else // This is possible due to incorrect type cast. @@ -745,7 +745,7 @@ object Objects: Bottom case fun: Fun => - report.error("[Internal error] unexpected tree in selecting a function, fun = " + fun.code.show + Trace.show, fun.code) + report.warning("[Internal error] unexpected tree in selecting a function, fun = " + fun.code.show + Trace.show, fun.code) Bottom case Bottom => @@ -766,7 +766,7 @@ object Objects: def assign(lhs: Value, field: Symbol, rhs: Value, rhsTyp: Type): Contextual[Value] = log("Assign" + field.show + " of " + lhs.show + ", rhs = " + rhs.show, printer, (_: Value).show) { lhs match case fun: Fun => - report.error("[Internal error] unexpected tree in assignment, fun = " + fun.code.show + Trace.show, Trace.position) + report.warning("[Internal error] unexpected tree in assignment, fun = " + fun.code.show + Trace.show, Trace.position) case Cold => report.warning("Assigning to cold aliases is forbidden", Trace.position) @@ -801,7 +801,7 @@ object Objects: outer match case _ : Fun | _: OfArray => - report.error("[Internal error] unexpected outer in instantiating a class, outer = " + outer.show + ", class = " + klass.show + ", " + Trace.show, Trace.position) + report.warning("[Internal error] unexpected outer in instantiating a class, outer = " + outer.show + ", class = " + klass.show + ", " + Trace.show, Trace.position) Bottom case value: (Bottom.type | ObjectRef | OfClass | Cold.type) => @@ -1161,7 +1161,7 @@ object Objects: init(tpl, thisV.asInstanceOf[Ref], klass) case _ => - report.error("[Internal error] unexpected tree: " + expr + "\n" + Trace.show, expr) + report.warning("[Internal error] unexpected tree: " + expr + "\n" + Trace.show, expr) Bottom } @@ -1243,12 +1243,12 @@ object Objects: case arg @ Literal(c: Constants.Constant) => val height = c.intValue if height < 0 then - report.error("The argument should be positive", arg) + report.warning("The argument should be positive", arg) res.widen(1) else res.widen(c.intValue) case arg => - report.error("The argument should be a constant integer value", arg) + report.warning("The argument should be a constant integer value", arg) res.widen(1) case _ => res.widen(1) @@ -1410,14 +1410,14 @@ object Objects: val outerCls = klass.owner.lexicallyEnclosingClass.asClass if !ref.hasOuter(klass) then val error = "[Internal error] outer not yet initialized, target = " + target + ", klass = " + klass + Trace.show - report.error(error, Trace.position) + report.warning(error, Trace.position) Bottom else resolveThis(target, ref.outerValue(klass), outerCls) case RefSet(refs) => refs.map(ref => resolveThis(target, ref, klass)).join case fun: Fun => - report.error("[Internal error] unexpected thisV = " + thisV + ", target = " + target.show + ", klass = " + klass.show + Trace.show, Trace.position) + report.warning("[Internal error] unexpected thisV = " + thisV + ", target = " + target.show + ", klass = " + klass.show + Trace.show, Trace.position) Bottom } diff --git a/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala b/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala index f04b4d59f72f..261577cb718f 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala @@ -589,7 +589,7 @@ object Semantic: Hot else if ref.klass.isSubClass(receiver.widenSingleton.classSymbol) then - report.error("[Internal error] Unexpected resolution failure: ref.klass = " + ref.klass.show + ", field = " + field.show + Trace.show, Trace.position) + report.warning("[Internal error] Unexpected resolution failure: ref.klass = " + ref.klass.show + ", field = " + field.show + Trace.show, Trace.position) Hot else // This is possible due to incorrect type cast. @@ -597,7 +597,7 @@ object Semantic: Hot case fun: Fun => - report.error("[Internal error] unexpected tree in selecting a function, fun = " + fun.expr.show + Trace.show, fun.expr) + report.warning("[Internal error] unexpected tree in selecting a function, fun = " + fun.expr.show + Trace.show, fun.expr) Hot case RefSet(refs) => @@ -725,7 +725,7 @@ object Semantic: value.select(target, receiver, needResolve = false) else if ref.klass.isSubClass(receiver.widenSingleton.classSymbol) then - report.error("[Internal error] Unexpected resolution failure: ref.klass = " + ref.klass.show + ", meth = " + meth.show + Trace.show, Trace.position) + report.warning("[Internal error] Unexpected resolution failure: ref.klass = " + ref.klass.show + ", meth = " + meth.show + Trace.show, Trace.position) Hot else // This is possible due to incorrect type cast. @@ -755,7 +755,7 @@ object Semantic: value match { case Hot | Cold | _: RefSet | _: Fun => - report.error("[Internal error] unexpected constructor call, meth = " + ctor + ", value = " + value + Trace.show, Trace.position) + report.warning("[Internal error] unexpected constructor call, meth = " + ctor + ", value = " + value + Trace.show, Trace.position) Hot case ref: Warm if ref.isPopulatingParams => @@ -862,7 +862,7 @@ object Semantic: warm case Fun(body, thisV, klass) => - report.error("[Internal error] unexpected tree in instantiating a function, fun = " + body.show + Trace.show, Trace.position) + report.warning("[Internal error] unexpected tree in instantiating a function, fun = " + body.show + Trace.show, Trace.position) Hot case RefSet(refs) => @@ -882,7 +882,7 @@ object Semantic: case Hot => Hot case ref: Ref => ref.objekt.field(sym) case _ => - report.error("[Internal error] unexpected this value accessing local variable, sym = " + sym.show + ", thisValue = " + thisValue2.show + Trace.show, Trace.position) + report.warning("[Internal error] unexpected this value accessing local variable, sym = " + sym.show + ", thisValue = " + thisValue2.show + Trace.show, Trace.position) Hot else if sym.is(Flags.Param) then Hot @@ -900,7 +900,7 @@ object Semantic: case ref: Ref => eval(vdef.rhs, ref, enclosingClass, cacheResult = sym.is(Flags.Lazy)) case _ => - report.error("[Internal error] unexpected this value when accessing local variable, sym = " + sym.show + ", thisValue = " + thisValue2.show + Trace.show, Trace.position) + report.warning("[Internal error] unexpected this value when accessing local variable, sym = " + sym.show + ", thisValue = " + thisValue2.show + Trace.show, Trace.position) Hot end match @@ -1040,7 +1040,7 @@ object Semantic: // // This invariant holds because of the Scala/Java/JVM restriction that we cannot use `this` in super constructor calls. if subClassSegmentHot && !isHotSegment then - report.error("[Internal error] Expect current segment to be transitively initialized (Hot) in promotion, current klass = " + klass.show + + report.warning("[Internal error] Expect current segment to be transitively initialized (Hot) in promotion, current klass = " + klass.show + ", subclass = " + subClass.show + Trace.show, Trace.position) // If the outer and parameters of a class are all hot, then accessing fields and methods of the current @@ -1374,7 +1374,7 @@ object Semantic: Hot case _ => - report.error("[Internal error] unexpected tree" + Trace.show, expr) + report.warning("[Internal error] unexpected tree" + Trace.show, expr) Hot /** Handle semantics of leaf nodes @@ -1418,7 +1418,7 @@ object Semantic: Hot case _ => - report.error("[Internal error] unexpected type " + tp + Trace.show, Trace.position) + report.warning("[Internal error] unexpected type " + tp + Trace.show, Trace.position) Hot } @@ -1439,14 +1439,14 @@ object Semantic: val outerCls = klass.owner.lexicallyEnclosingClass.asClass if !obj.hasOuter(klass) then val error = "[Internal error] outer not yet initialized, target = " + target + ", klass = " + klass + ", object = " + obj + Trace.show - report.error(error, Trace.position) + report.warning(error, Trace.position) Hot else resolveThis(target, obj.outer(klass), outerCls) case RefSet(refs) => refs.map(ref => resolveThis(target, ref, klass)).join case fun: Fun => - report.error("[Internal error] unexpected thisV = " + thisV + ", target = " + target.show + ", klass = " + klass.show + Trace.show, Trace.position) + report.warning("[Internal error] unexpected thisV = " + thisV + ", target = " + target.show + ", klass = " + klass.show + Trace.show, Trace.position) Cold case Cold => Cold From cade1c72ebf3a1c65d1c3b07404a5407d6bfc19d Mon Sep 17 00:00:00 2001 From: Fengyun Liu Date: Mon, 12 Jun 2023 19:57:32 +0200 Subject: [PATCH 110/113] Add experimental to lib changes --- library/src/scala/annotation/init.scala | 3 +++ 1 file changed, 3 insertions(+) diff --git a/library/src/scala/annotation/init.scala b/library/src/scala/annotation/init.scala index 62c875fc12e4..7dea418664e6 100644 --- a/library/src/scala/annotation/init.scala +++ b/library/src/scala/annotation/init.scala @@ -5,6 +5,7 @@ package scala.annotation * Programmers usually do not need to use any annotations. They are intended for complex initialization * code in static objects. */ +@experimental object init: /** Widen the abstract value of the argument so that its height is below the specified height. @@ -27,6 +28,7 @@ object init: * the checker will issue a warning for the method call `c.a.square()` because * it is forbidden to call methods or access fields on cold values. */ + @experimental final class widen(height: Int) extends StaticAnnotation /** Introduce a region context. @@ -52,4 +54,5 @@ object init: * the method call `box1.value.foo()` will be invalid, because it reaches `A.m`, which is not yet initialized. * The explicit context annotation solves the problem. */ + @experimental def region[T](v: T): T = v From 8e9b194c742d075545902ce589491d97cb2c098d Mon Sep 17 00:00:00 2001 From: Fengyun Liu Date: Mon, 12 Jun 2023 21:04:04 +0200 Subject: [PATCH 111/113] Fix experimental definition test --- .../tasty-inspector/stdlibExperimentalDefinitions.scala | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tests/run-custom-args/tasty-inspector/stdlibExperimentalDefinitions.scala b/tests/run-custom-args/tasty-inspector/stdlibExperimentalDefinitions.scala index 1eb2ce3beee0..4860d19dbc8b 100644 --- a/tests/run-custom-args/tasty-inspector/stdlibExperimentalDefinitions.scala +++ b/tests/run-custom-args/tasty-inspector/stdlibExperimentalDefinitions.scala @@ -63,6 +63,12 @@ val experimentalDefinitionInLibrary = Set( //// New feature: Macro annotations "scala.annotation.MacroAnnotation", + //// New feature: -Ysafe-init-global + "scala.annotation.init", + "scala.annotation.init$", + "scala.annotation.init$.widen", + "scala.annotation.init$.region", + //// New APIs: Quotes // Should be stabilized in 3.4.0 "scala.quoted.Quotes.reflectModule.defnModule.FunctionClass", From 473e765f8eb39236d024cc718b0f7c837f9619e5 Mon Sep 17 00:00:00 2001 From: Fengyun Liu Date: Thu, 15 Jun 2023 08:20:08 +0200 Subject: [PATCH 112/113] Show more context for warnings --- .../src/dotty/tools/dotc/transform/init/Objects.scala | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/transform/init/Objects.scala b/compiler/src/dotty/tools/dotc/transform/init/Objects.scala index fe5827251816..0826f4211711 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Objects.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Objects.scala @@ -569,7 +569,7 @@ object Objects: def call(value: Value, meth: Symbol, args: List[ArgInfo], receiver: Type, superType: Type, needResolve: Boolean = true): Contextual[Value] = log("call " + meth.show + ", args = " + args.map(_.value.show), printer, (_: Value).show) { value match case Cold => - report.warning("Using cold alias", Trace.position) + report.warning("Using cold alias. Calling trace:\n" + Trace.show, Trace.position) Bottom case Bottom => @@ -769,7 +769,7 @@ object Objects: report.warning("[Internal error] unexpected tree in assignment, fun = " + fun.code.show + Trace.show, Trace.position) case Cold => - report.warning("Assigning to cold aliases is forbidden", Trace.position) + report.warning("Assigning to cold aliases is forbidden. Calling trace:\n" + Trace.show, Trace.position) case Bottom => @@ -784,7 +784,7 @@ object Objects: else Heap.write(addr, rhs) else - report.warning("Mutating a field before its initialization: " + field.show, Trace.position) + report.warning("Mutating a field before its initialization: " + field.show + ". Calling trace:\n" + Trace.show, Trace.position) end match Bottom @@ -915,7 +915,7 @@ object Objects: Heap.write(addr, value) case _ => - report.warning("Assigning to variables in outer scope", Trace.position) + report.warning("Assigning to variables in outer scope. Calling trace:\n" + Trace.show, Trace.position) Bottom } @@ -1002,7 +1002,7 @@ object Objects: case Apply(ref, arg :: Nil) if ref.symbol == defn.InitRegionMethod => val regions2 = Regions.extend(expr.sourcePos) if Regions.exists(expr.sourcePos) then - report.warning("Cyclic region detected. Trace: " + Trace.show, expr) + report.warning("Cyclic region detected. Trace:\n" + Trace.show, expr) Bottom else given Regions.Data = regions2 From ce093cbd76c053b7e6918af23d80d93b28a94e0e Mon Sep 17 00:00:00 2001 From: Fengyun Liu Date: Thu, 15 Jun 2023 10:13:11 +0200 Subject: [PATCH 113/113] Report more warnings for pattern match --- compiler/src/dotty/tools/dotc/transform/init/Objects.scala | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/compiler/src/dotty/tools/dotc/transform/init/Objects.scala b/compiler/src/dotty/tools/dotc/transform/init/Objects.scala index 0826f4211711..c271067ff0db 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Objects.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Objects.scala @@ -1109,7 +1109,9 @@ object Objects: case Match(selector, cases) => eval(selector, thisV, klass) - evalExprs(cases.map(_.body), thisV, klass).join + // TODO: handle pattern match properly + report.warning("[initChecker] Pattern match is skipped. Trace:\n" + Trace.show, expr) + Bottom case Return(expr, from) => Returns.handle(from.symbol, eval(expr, thisV, klass))