From c8fe7e8f27f116aef582303ac3ff00e2640dc3b7 Mon Sep 17 00:00:00 2001 From: Liu Fengyun Date: Sun, 9 May 2021 16:24:02 +0200 Subject: [PATCH 01/50] Add Domain and Semantic --- .../tools/dotc/transform/init/Domain.scala | 42 +++++++++++++++ .../tools/dotc/transform/init/Semantic.scala | 52 +++++++++++++++++++ 2 files changed, 94 insertions(+) create mode 100644 compiler/src/dotty/tools/dotc/transform/init/Domain.scala create mode 100644 compiler/src/dotty/tools/dotc/transform/init/Semantic.scala diff --git a/compiler/src/dotty/tools/dotc/transform/init/Domain.scala b/compiler/src/dotty/tools/dotc/transform/init/Domain.scala new file mode 100644 index 000000000000..25859b0e5cc6 --- /dev/null +++ b/compiler/src/dotty/tools/dotc/transform/init/Domain.scala @@ -0,0 +1,42 @@ +package dotty.tools.dotc +package transform +package init + +import core._ +import Symbols._ + +import ast.tpd._ +import util.SourcePosition + +trait Domain { + /** Abstract values */ + type Value + + /** The bottom value */ + val bottom: Value + + /** Addresses to the abstract heap */ + type Addr + + /** Abstract object in the heap */ + type Objekt + + /** Abstract heap stores abstract objects */ + type Heap = Map[Addr, Objekt] + + /** Interpreter configuration + * + * The (abstract) interpreter can be seen as a push-down automaton that + * transits between the configurations where the stack is the implicit call + * stack of the meta-language. + * + * It's important that the configuration is finite for the analysis to + * terminate. + * + * For soundness, we need to compute fixed point of the cache, which maps + * configuration to evaluation result. + * + */ + type Config + def makeConfig(expr: Tree, thisV: Value, heap: Heap): Config +} diff --git a/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala b/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala new file mode 100644 index 000000000000..0a7fe517a02b --- /dev/null +++ b/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala @@ -0,0 +1,52 @@ +package dotty.tools.dotc +package transform +package init + +import core._ +import Symbols._ + +import ast.tpd._ +import util.SourcePosition + +import scala.collection.mutable + +trait Semantic { self: Domain => + /** Cache used to terminate the analysis + * + * A finitary configuration is not enough for the analysis to + * terminate. We need to use cache to let the interpreter "know" + * that it can terminate. + */ + type Cache <: mutable.Map[Config, Value] + val cache: Cache + + /** Errors in the program */ + type Error + + /** Result of abstract interpretation */ + case class Result(value: Value, heap: Heap, errors: List[Error]) + + /** Evaluate an expression with the given value for `this` in a given class `klass` + * + * This method only handles cache logic and delegates the work to `cases`. + */ + def eval(expr: Tree, thisV: Value, klass: ClassSymbol, heap: Heap): Result = + val cfg = makeConfig(expr, thisV, heap) + if (cache.contains(cfg)) Result(cache(cfg), heap, Nil) + else { + // no need to compute fix-point, because + // 1. the result is decided by `cfg` for a legal program + // (heap change is irrelevant thanks to monotonicity) + // 2. errors will have been reported for an illegal program + cache(cfg) = bottom + val res = cases(expr, thisV, klass, heap) + cache(cfg) = res.value + res + } + + /** Handles the evaluation of different expressions */ + def cases(expr: Tree, thisV: Value, klass: ClassSymbol, heap: Heap): Result = ??? + + /** Initialize an abstract object */ + def init(klass: Symbol, thisV: Addr, heap: Heap): Result = ??? +} From 006f714beae4a7bccbb8b5f45cef882dfd99c4ee Mon Sep 17 00:00:00 2001 From: Liu Fengyun Date: Sun, 9 May 2021 16:57:11 +0200 Subject: [PATCH 02/50] Add evaluator skeleton --- .../tools/dotc/transform/init/Semantic.scala | 137 +++++++++++++++++- 1 file changed, 133 insertions(+), 4 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala b/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala index 0a7fe517a02b..fac9fb5a378e 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala @@ -3,10 +3,15 @@ package transform package init import core._ +import Contexts._ import Symbols._ +import Types._ +import StdNames._ import ast.tpd._ import util.SourcePosition +import config.Printers.{ init => printer } +import reporting.trace import scala.collection.mutable @@ -24,15 +29,19 @@ trait Semantic { self: Domain => type Error /** Result of abstract interpretation */ - case class Result(value: Value, heap: Heap, errors: List[Error]) + case class Result(value: Value, heap: Heap, errors: Vector[Error]) { + def show(using Context) = ??? + } + + val noErrors = Vector.empty[Error] /** Evaluate an expression with the given value for `this` in a given class `klass` * * This method only handles cache logic and delegates the work to `cases`. */ - def eval(expr: Tree, thisV: Value, klass: ClassSymbol, heap: Heap): Result = + def eval(expr: Tree, thisV: Value, klass: ClassSymbol, heap: Heap)(using Context): Result = trace("evaluating " + expr.show, printer, res => res.asInstanceOf[Result].show) { val cfg = makeConfig(expr, thisV, heap) - if (cache.contains(cfg)) Result(cache(cfg), heap, Nil) + if (cache.contains(cfg)) Result(cache(cfg), heap, noErrors) else { // no need to compute fix-point, because // 1. the result is decided by `cfg` for a legal program @@ -43,9 +52,129 @@ trait Semantic { self: Domain => cache(cfg) = res.value res } + } /** Handles the evaluation of different expressions */ - def cases(expr: Tree, thisV: Value, klass: ClassSymbol, heap: Heap): Result = ??? + def cases(expr: Tree, thisV: Value, klass: ClassSymbol, heap: Heap)(using Context): Result = + expr match { + case Ident(nme.WILDCARD) => + // TODO: disallow `var x: T = _` + Result(bottom, heap, noErrors) + + case Ident(name) => + assert(name.isTermName, "type trees should not reach here") + cases(expr.tpe, thisV, klass, heap) + + // case supert: Super => Handled in Apply case + + case Apply(fun, args) => + ??? + + case TypeApply(fun, _) => + ??? + + case Select(qualifier, name) => + ??? + + case _: This => + ??? + + case Literal(_) => + Result(bottom, heap, noErrors) + + case New(tpt) => + ??? + + case Typed(expr, tpt) => + if (tpt.tpe.hasAnnotation(defn.UncheckedAnnot)) Result(bottom, heap, noErrors) + else ??? + + case NamedArg(name, arg) => + cases(arg, thisV, klass, heap) + + case Assign(lhs, rhs) => + ??? + + case closureDef(ddef) => + ??? + + case Block(stats, expr) => + ??? + + case If(cond, thenp, elsep) => + ??? + + case Annotated(arg, annot) => + if (expr.tpe.hasAnnotation(defn.UncheckedAnnot)) Result(bottom, heap, noErrors) + else ??? + + case Match(selector, cases) => + ??? + + case Return(expr, from) => + ??? + + case WhileDo(cond, body) => + ??? + + case Labeled(_, expr) => + ??? + + case Try(block, cases, finalizer) => + ??? + + case SeqLiteral(elems, elemtpt) => + ??? + + case Inlined(call, bindings, expansion) => + ??? + + case vdef : ValDef => + ??? + + case Thicket(List()) => + // possible in try/catch/finally, see tests/crash/i6914.scala + Result(bottom, heap, noErrors) + + case ddef : DefDef => + ??? + + case tdef: TypeDef => + ??? + + case _: Import | _: Export => + Result(bottom, heap, noErrors) + + case _ => + throw new Exception("unexpected tree: " + expr.show) + } + + /** Handle semantics of leaf nodes */ + def cases(tp: Type, thisV: Value, klass: ClassSymbol, heap: Heap)(using Context): Result = trace("evaluating " + tp.show, printer, res => res.asInstanceOf[Result].show) { + tp match { + case _: ConstantType => + Result(bottom, heap, noErrors) + + case tmref: TermRef if tmref.prefix == NoPrefix => + Result(bottom, heap, noErrors) + + case tmref: TermRef => + ??? + + case ThisType(tref) => + ??? + + case SuperType(thisTp, superTp) => + ??? + + case _: TermParamRef | _: RecThis => + // possible from checking effects of types + Result(bottom, heap, noErrors) + + case _ => + throw new Exception("unexpected type: " + tp) + } + } /** Initialize an abstract object */ def init(klass: Symbol, thisV: Addr, heap: Heap): Result = ??? From 5373188c3488e6a1076517ba50f4c6bea1519150 Mon Sep 17 00:00:00 2001 From: Liu Fengyun Date: Wed, 12 May 2021 17:14:37 +0200 Subject: [PATCH 03/50] Add concrete domain definitions --- .../tools/dotc/transform/init/Semantic.scala | 92 ++++++++++++++++--- 1 file changed, 78 insertions(+), 14 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala b/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala index fac9fb5a378e..74d7fffec90a 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala @@ -15,15 +15,79 @@ import reporting.trace import scala.collection.mutable -trait Semantic { self: Domain => +class Semantic { + /** Locations are finite for any givn program */ + type Loc = SourcePosition + + /** Abstract values + * + * Value = Hot | Cold | Addr | Fun | RefSet + * + * `Warm` and `This` will be addresses refer to the abstract heap + */ + trait Value + + /** A transitively initialized object */ + case object Hot extends Value + + /** An object with unknown initialization status */ + case object Cold extends Value + + /** Addresses to the abstract heap + * + * Addresses determine abstractions of objects. Objects created + * with same address are represented with the same abstraction. + * + * Nested addresses may lead to infinite domain, thus widen is + * needed to finitize addresses. E.g. OOPSLA 2020 paper restricts + * args to be either `Hot` or `Cold` + */ + case class Addr(klass: Symbol, args: List[Value]) extends Value + + /** A function value */ + case class Fun(expr: Tree, thisV: Addr, klass: Symbol) extends Value + + /** A value which represents a set of addresses + * + * It comes from `if` expressions. + */ + case class RefSet(refs: List[Addr | Fun]) extends Value + + /** Object stores abstract values for all fields and the outer. */ + case class Objekt(klass: Symbol, fields: Map[Symbol, Value], outer: Value) + + /** Abstract heap stores abstract objects + * + * As in the OOPSLA paper, the abstract heap is monotonistic + */ + type Heap = Map[Addr, Objekt] + + /** Interpreter configuration + * + * The (abstract) interpreter can be seen as a push-down automaton + * that transits between the configurations where the stack is the + * implicit call stack of the meta-language. + * + * It's important that the configuration is finite for the analysis + * to terminate. + * + * For soundness, we need to compute fixed point of the cache, which + * maps configuration to evaluation result. + * + * Thanks to heap monotonicity, heap is not part of the configuration. + * Which also avoid computing fix-point on the cache, as the cache is + * immutable. + */ + case class Config(thisV: Value, loc: Loc) + /** Cache used to terminate the analysis * * A finitary configuration is not enough for the analysis to * terminate. We need to use cache to let the interpreter "know" * that it can terminate. */ - type Cache <: mutable.Map[Config, Value] - val cache: Cache + type Cache = mutable.Map[Config, Value] + val cache: Cache = mutable.Map.empty[Config, Value] /** Errors in the program */ type Error @@ -40,14 +104,14 @@ trait Semantic { self: Domain => * This method only handles cache logic and delegates the work to `cases`. */ def eval(expr: Tree, thisV: Value, klass: ClassSymbol, heap: Heap)(using Context): Result = trace("evaluating " + expr.show, printer, res => res.asInstanceOf[Result].show) { - val cfg = makeConfig(expr, thisV, heap) + val cfg = Config(thisV, expr.sourcePos) if (cache.contains(cfg)) Result(cache(cfg), heap, noErrors) else { // no need to compute fix-point, because // 1. the result is decided by `cfg` for a legal program // (heap change is irrelevant thanks to monotonicity) // 2. errors will have been reported for an illegal program - cache(cfg) = bottom + cache(cfg) = Hot val res = cases(expr, thisV, klass, heap) cache(cfg) = res.value res @@ -59,7 +123,7 @@ trait Semantic { self: Domain => expr match { case Ident(nme.WILDCARD) => // TODO: disallow `var x: T = _` - Result(bottom, heap, noErrors) + Result(Hot, heap, noErrors) case Ident(name) => assert(name.isTermName, "type trees should not reach here") @@ -80,13 +144,13 @@ trait Semantic { self: Domain => ??? case Literal(_) => - Result(bottom, heap, noErrors) + Result(Hot, heap, noErrors) case New(tpt) => ??? case Typed(expr, tpt) => - if (tpt.tpe.hasAnnotation(defn.UncheckedAnnot)) Result(bottom, heap, noErrors) + if (tpt.tpe.hasAnnotation(defn.UncheckedAnnot)) Result(Hot, heap, noErrors) else ??? case NamedArg(name, arg) => @@ -105,7 +169,7 @@ trait Semantic { self: Domain => ??? case Annotated(arg, annot) => - if (expr.tpe.hasAnnotation(defn.UncheckedAnnot)) Result(bottom, heap, noErrors) + if (expr.tpe.hasAnnotation(defn.UncheckedAnnot)) Result(Hot, heap, noErrors) else ??? case Match(selector, cases) => @@ -134,7 +198,7 @@ trait Semantic { self: Domain => case Thicket(List()) => // possible in try/catch/finally, see tests/crash/i6914.scala - Result(bottom, heap, noErrors) + Result(Hot, heap, noErrors) case ddef : DefDef => ??? @@ -143,7 +207,7 @@ trait Semantic { self: Domain => ??? case _: Import | _: Export => - Result(bottom, heap, noErrors) + Result(Hot, heap, noErrors) case _ => throw new Exception("unexpected tree: " + expr.show) @@ -153,10 +217,10 @@ trait Semantic { self: Domain => def cases(tp: Type, thisV: Value, klass: ClassSymbol, heap: Heap)(using Context): Result = trace("evaluating " + tp.show, printer, res => res.asInstanceOf[Result].show) { tp match { case _: ConstantType => - Result(bottom, heap, noErrors) + Result(Hot, heap, noErrors) case tmref: TermRef if tmref.prefix == NoPrefix => - Result(bottom, heap, noErrors) + Result(Hot, heap, noErrors) case tmref: TermRef => ??? @@ -169,7 +233,7 @@ trait Semantic { self: Domain => case _: TermParamRef | _: RecThis => // possible from checking effects of types - Result(bottom, heap, noErrors) + Result(Hot, heap, noErrors) case _ => throw new Exception("unexpected type: " + tp) From b31bbda9a8691b3566219eb80f7fcede60ef5067 Mon Sep 17 00:00:00 2001 From: Liu Fengyun Date: Wed, 12 May 2021 18:05:00 +0200 Subject: [PATCH 04/50] Define join --- .../tools/dotc/transform/init/Semantic.scala | 87 ++++++++++++++++--- 1 file changed, 74 insertions(+), 13 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala b/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala index 74d7fffec90a..a89aea65655e 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala @@ -13,9 +13,14 @@ import util.SourcePosition import config.Printers.{ init => printer } import reporting.trace +import Errors._ + import scala.collection.mutable class Semantic { + +// ----- Domain definitions -------------------------------- + /** Locations are finite for any givn program */ type Loc = SourcePosition @@ -89,15 +94,68 @@ class Semantic { type Cache = mutable.Map[Config, Value] val cache: Cache = mutable.Map.empty[Config, Value] - /** Errors in the program */ - type Error - /** Result of abstract interpretation */ - case class Result(value: Value, heap: Heap, errors: Vector[Error]) { + case class Result(value: Value, heap: Heap, errors: List[Error]) { def show(using Context) = ??? + + def ++(errors: Seq[Error]): Result = this.copy(errors = this.errors ++ errors) + + def fieldAccess(f: Symbol, source: Tree)(using Context): Result = + value.fieldAccess(f, heap, source) ++ errors } - val noErrors = Vector.empty[Error] + val noErrors = Nil + +// ----- Operations on domains ----------------------------- + extension (a: Value) + def join(b: Value): Value = + (a, b) match + case (Hot, _) => b + case (_, Hot) => a + + case (Cold, _) => Cold + case (_, Cold) => Cold + + case (a: (Fun | Addr), b: (Fun | Addr)) => RefSet(a :: b :: Nil) + + case (a: (Fun | Addr), RefSet(refs)) => RefSet(a :: refs) + case (RefSet(refs), b: (Fun | Addr)) => RefSet(b :: refs) + + case (RefSet(refs1), RefSet(refs2)) => RefSet(refs1 ++ refs2) + + extension (values: Seq[Value]) + def join: Value = values.reduce { (v1, v2) => v1.join(v2) } + + extension (value: Value) + def fieldAccess(f: Symbol, heap: Heap, source: Tree)(using Context): Result = + value match { + case Hot => + Result(Hot, heap, noErrors) + + case Cold => + val error = AccessCold(f, source, Vector(source)) + Result(Hot, heap, error :: Nil) + + case addr: Addr => + val obj = heap(addr) + if obj.fields.contains(f) then + Result(obj.fields(f), heap, Nil) + else + val error = AccessNonInit(f, Vector(source)) + Result(Hot, heap, error :: Nil) + + case _: Fun => + ??? + + case RefSet(refs) => + val resList = refs.map(_.fieldAccess(f, heap, source)) + val value2 = resList.map(_.value).join + val errors = resList.flatMap(_.errors) + Result(value2, heap, errors) + } + + +// ----- Semantic definition -------------------------------- /** Evaluate an expression with the given value for `this` in a given class `klass` * @@ -127,7 +185,7 @@ class Semantic { case Ident(name) => assert(name.isTermName, "type trees should not reach here") - cases(expr.tpe, thisV, klass, heap) + cases(expr.tpe, thisV, klass, heap, expr) // case supert: Super => Handled in Apply case @@ -214,7 +272,7 @@ class Semantic { } /** Handle semantics of leaf nodes */ - def cases(tp: Type, thisV: Value, klass: ClassSymbol, heap: Heap)(using Context): Result = trace("evaluating " + tp.show, printer, res => res.asInstanceOf[Result].show) { + def cases(tp: Type, thisV: Value, klass: ClassSymbol, heap: Heap, source: Tree)(using Context): Result = trace("evaluating " + tp.show, printer, res => res.asInstanceOf[Result].show) { tp match { case _: ConstantType => Result(Hot, heap, noErrors) @@ -223,13 +281,11 @@ class Semantic { Result(Hot, heap, noErrors) case tmref: TermRef => - ??? - - case ThisType(tref) => - ??? + cases(tmref.prefix, thisV, klass, heap, source).fieldAccess(tmref.symbol, source) - case SuperType(thisTp, superTp) => - ??? + case tp @ ThisType(tref) => + if tref.symbol.is(Flags.Package) then Result(Hot, heap, noErrors) + else resolveThis(tp, thisV: Value, klass: ClassSymbol, heap: Heap) case _: TermParamRef | _: RecThis => // possible from checking effects of types @@ -240,6 +296,11 @@ class Semantic { } } + /** Resolve C.this that appear in `klass` */ + def resolveThis(tp: Type, thisV: Value, klass: ClassSymbol, heap: Heap)(using Context): Result = trace("resolving " + tp.show, printer, res => res.asInstanceOf[Result].show) { + ??? + } + /** Initialize an abstract object */ def init(klass: Symbol, thisV: Addr, heap: Heap): Result = ??? } From d955d26069d05244ba268f3af5b37ebe95c757d1 Mon Sep 17 00:00:00 2001 From: Liu Fengyun Date: Wed, 12 May 2021 19:17:40 +0200 Subject: [PATCH 05/50] Implement this resolution --- .../tools/dotc/transform/init/Semantic.scala | 38 +++++++++++++++---- 1 file changed, 31 insertions(+), 7 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala b/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala index a89aea65655e..e809142770b1 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala @@ -30,7 +30,9 @@ class Semantic { * * `Warm` and `This` will be addresses refer to the abstract heap */ - trait Value + trait Value { + def show: String = this.toString() + } /** A transitively initialized object */ case object Hot extends Value @@ -47,7 +49,7 @@ class Semantic { * needed to finitize addresses. E.g. OOPSLA 2020 paper restricts * args to be either `Hot` or `Cold` */ - case class Addr(klass: Symbol, args: List[Value]) extends Value + case class Addr(klass: Symbol, args: List[Value], outer: Value) extends Value /** A function value */ case class Fun(expr: Tree, thisV: Addr, klass: Symbol) extends Value @@ -58,8 +60,15 @@ class Semantic { */ case class RefSet(refs: List[Addr | Fun]) extends Value - /** Object stores abstract values for all fields and the outer. */ - case class Objekt(klass: Symbol, fields: Map[Symbol, Value], outer: Value) + /** Object stores abstract values for all fields and the outer. + * + * Theoretically we only need to store the outer for the concrete class, + * as all other outers are determined. + * + * From performance reasons, we cache the immediate outer for all classes + * in the inheritance hierarchy. + */ + case class Objekt(klass: Symbol, fields: Map[Symbol, Value], outers: Map[ClassSymbol, Value]) /** Abstract heap stores abstract objects * @@ -285,7 +294,14 @@ class Semantic { case tp @ ThisType(tref) => if tref.symbol.is(Flags.Package) then Result(Hot, heap, noErrors) - else resolveThis(tp, thisV: Value, klass: ClassSymbol, heap: Heap) + else + val value = + thisV match + case Hot => Hot + case addr: Addr => + resolveThis(tp, addr, klass, heap, source) + case _ => ??? + Result(value, heap, noErrors) case _: TermParamRef | _: RecThis => // possible from checking effects of types @@ -297,8 +313,16 @@ class Semantic { } /** Resolve C.this that appear in `klass` */ - def resolveThis(tp: Type, thisV: Value, klass: ClassSymbol, heap: Heap)(using Context): Result = trace("resolving " + tp.show, printer, res => res.asInstanceOf[Result].show) { - ??? + def resolveThis(tp: ThisType, thisV: Value, klass: ClassSymbol, heap: Heap, source: Tree)(using Context): Value = trace("resolving " + tp.show + ", this = " + thisV.show + " in " + klass.show, printer, res => res.asInstanceOf[Value].show) { + if tp.classSymbol == klass then thisV + else + thisV match + case Hot => Hot + case thisV: Addr => + val outer = heap(thisV).outers.getOrElse(klass, Hot) + val outerCls = klass.owner.enclosingClass.asClass + resolveThis(tp, outer, outerCls, heap, source) + case _ => ??? } /** Initialize an abstract object */ From b3ebab08c19993928be819500441cf12f13789a1 Mon Sep 17 00:00:00 2001 From: Liu Fengyun Date: Wed, 12 May 2021 20:45:28 +0200 Subject: [PATCH 06/50] Add utility extractors --- .../tools/dotc/transform/init/Semantic.scala | 40 +++++++++++++++---- 1 file changed, 32 insertions(+), 8 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala b/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala index e809142770b1..82a90e2a342c 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala @@ -196,26 +196,23 @@ class Semantic { assert(name.isTermName, "type trees should not reach here") cases(expr.tpe, thisV, klass, heap, expr) - // case supert: Super => Handled in Apply case - - case Apply(fun, args) => + case NewExpr(newExpr, ctor, argss) => ??? - case TypeApply(fun, _) => + // case supert: Super => Handled in Apply case + + case Call(ref, argss) => ??? case Select(qualifier, name) => ??? case _: This => - ??? + cases(expr.tpe, thisV, klass, heap, expr) case Literal(_) => Result(Hot, heap, noErrors) - case New(tpt) => - ??? - case Typed(expr, tpt) => if (tpt.tpe.hasAnnotation(defn.UncheckedAnnot)) Result(Hot, heap, noErrors) else ??? @@ -327,4 +324,31 @@ class Semantic { /** Initialize an abstract object */ def init(klass: Symbol, thisV: Addr, heap: Heap): Result = ??? + +// ----- Utility methods and extractors -------------------------------- + + object Call { + def unapply(tree: Tree)(using Context): Option[(Tree, List[List[Tree]])] = + tree match + case Apply(fn, args) => + unapply(fn) match + case Some((ref, args0)) => Some((ref, args0 :+ args)) + case None => None + + case TypeApply(fn, targs) => + unapply(fn) match + case Some((ref, args)) => Some((ref, args :+ targs)) + case None => None + + case ref: RefTree if ref.symbol.is(Flags.Method) => + Some((ref, Nil)) + } + + object NewExpr { + def unapply(tree: Tree)(using Context): Option[(Tree, Symbol, List[List[Tree]])] = + tree match + case Call(fn @ Select(newTree: New, init), argss) if init == nme.CONSTRUCTOR => + Some((newTree, fn.symbol, argss)) + case _ => None + } } From ea7185fb4a286023514190287d5a5bb0f2c61564 Mon Sep 17 00:00:00 2001 From: Liu Fengyun Date: Wed, 12 May 2021 20:59:23 +0200 Subject: [PATCH 07/50] Handle Select node --- .../dotty/tools/dotc/transform/init/Semantic.scala | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala b/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala index 82a90e2a342c..106788b60f2c 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala @@ -109,8 +109,8 @@ class Semantic { def ++(errors: Seq[Error]): Result = this.copy(errors = this.errors ++ errors) - def fieldAccess(f: Symbol, source: Tree)(using Context): Result = - value.fieldAccess(f, heap, source) ++ errors + def select(f: Symbol, source: Tree)(using Context): Result = + value.select(f, heap, source) ++ errors } val noErrors = Nil @@ -136,7 +136,7 @@ class Semantic { def join: Value = values.reduce { (v1, v2) => v1.join(v2) } extension (value: Value) - def fieldAccess(f: Symbol, heap: Heap, source: Tree)(using Context): Result = + def select(f: Symbol, heap: Heap, source: Tree)(using Context): Result = value match { case Hot => Result(Hot, heap, noErrors) @@ -157,7 +157,7 @@ class Semantic { ??? case RefSet(refs) => - val resList = refs.map(_.fieldAccess(f, heap, source)) + val resList = refs.map(_.select(f, heap, source)) val value2 = resList.map(_.value).join val errors = resList.flatMap(_.errors) Result(value2, heap, errors) @@ -205,7 +205,7 @@ class Semantic { ??? case Select(qualifier, name) => - ??? + cases(qualifier, thisV, klass, heap).select(expr.symbol, expr) case _: This => cases(expr.tpe, thisV, klass, heap, expr) @@ -287,7 +287,7 @@ class Semantic { Result(Hot, heap, noErrors) case tmref: TermRef => - cases(tmref.prefix, thisV, klass, heap, source).fieldAccess(tmref.symbol, source) + cases(tmref.prefix, thisV, klass, heap, source).select(tmref.symbol, source) case tp @ ThisType(tref) => if tref.symbol.is(Flags.Package) then Result(Hot, heap, noErrors) From 021b58ef07402fdeb1c4bbdbbdcbcbe308a7d48f Mon Sep 17 00:00:00 2001 From: Liu Fengyun Date: Wed, 12 May 2021 23:25:56 +0200 Subject: [PATCH 08/50] Handle more expressions --- .../tools/dotc/transform/init/Semantic.scala | 102 ++++++++++++++---- 1 file changed, 80 insertions(+), 22 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala b/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala index 106788b60f2c..457c14c67a02 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala @@ -104,11 +104,19 @@ class Semantic { val cache: Cache = mutable.Map.empty[Config, Value] /** Result of abstract interpretation */ - case class Result(value: Value, heap: Heap, errors: List[Error]) { + case class Result(value: Value, heap: Heap, errors: Seq[Error]) { def show(using Context) = ??? def ++(errors: Seq[Error]): Result = this.copy(errors = this.errors ++ errors) + def +(error: Error): Result = this.copy(errors = this.errors :+ error) + + def ensureHot(msg: String, source: Tree): Result = + if value == Hot then this + else + // TODO: define a new error + this + PromoteCold(source, Vector(source)) + def select(f: Symbol, source: Tree)(using Context): Result = value.select(f, heap, source) ++ errors } @@ -185,7 +193,23 @@ class Semantic { } } - /** Handles the evaluation of different expressions */ + /** Evaluate a list of expressions */ + def eval(exprs: List[Tree], thisV: Value, klass: ClassSymbol, heap: Heap)(using Context): (List[Value], List[Error], Heap) = { + val errors = new mutable.ArrayBuffer[Error] + var heap2 = heap + val values = exprs.map { binding => + val res = eval(binding, thisV, klass, heap2) + heap2 = res.heap + errors ++= res.errors + res.value + } + (values, errors.toList, heap2) + } + + /** Handles the evaluation of different expressions + * + * Note: Recursive call should go to `eval` instead of `cases`. + */ def cases(expr: Tree, thisV: Value, klass: ClassSymbol, heap: Heap)(using Context): Result = expr match { case Ident(nme.WILDCARD) => @@ -205,7 +229,7 @@ class Semantic { ??? case Select(qualifier, name) => - cases(qualifier, thisV, klass, heap).select(expr.symbol, expr) + eval(qualifier, thisV, klass, heap).select(expr.symbol, expr) case _: This => cases(expr.tpe, thisV, klass, heap, expr) @@ -215,60 +239,94 @@ class Semantic { case Typed(expr, tpt) => if (tpt.tpe.hasAnnotation(defn.UncheckedAnnot)) Result(Hot, heap, noErrors) - else ??? + else eval(expr, thisV, klass, heap) case NamedArg(name, arg) => - cases(arg, thisV, klass, heap) + eval(arg, thisV, klass, heap) case Assign(lhs, rhs) => - ??? + lhs match + case Select(qual, _) => + val res = eval(qual, thisV, klass, heap) + eval(rhs, thisV, klass, res.heap).ensureHot("May only assign initialized value", rhs) ++ res.errors + case id: Ident => + eval(rhs, thisV, klass, heap).ensureHot("May only assign initialized value", rhs) case closureDef(ddef) => - ??? + thisV match + case addr: Addr => + val value = Fun(ddef.rhs, addr, klass) + Result(value, heap, Nil) + case _ => + ??? // impossible case Block(stats, expr) => - ??? + val (_, errors, heap2) = eval(stats, thisV, klass, heap) + eval(expr, thisV, klass, heap2) ++ errors case If(cond, thenp, elsep) => - ??? + val (_ :: values, errors, heap2) = eval(cond :: thenp :: elsep :: Nil, thisV, klass, heap) + Result(values.join, heap2, errors) case Annotated(arg, annot) => if (expr.tpe.hasAnnotation(defn.UncheckedAnnot)) Result(Hot, heap, noErrors) - else ??? + else eval(arg, thisV, klass, heap) case Match(selector, cases) => - ??? + val res1 = eval(selector, thisV, klass, heap) + val (values, errors, heap2) = eval(cases.map(_.body), thisV, klass, res1.heap) + Result(values.join, heap2, res1.errors ++ errors) case Return(expr, from) => - ??? + eval(expr, thisV, klass, heap).ensureHot("return expression may only be initialized value", expr) case WhileDo(cond, body) => - ??? + val (_, errors, heap2) = eval(cond :: body :: Nil, thisV, klass, heap) + Result(Hot, heap2, errors) case Labeled(_, expr) => - ??? + eval(expr, thisV, klass, heap) case Try(block, cases, finalizer) => - ??? + val res1 = eval(block, thisV, klass, heap) + val (values, errors, heap2) = eval(cases.map(_.body), thisV, klass, res1.heap) + val resValue = values.join + if finalizer.isEmpty then + Result(resValue, heap2, res1.errors ++ errors) + else + val res2 = eval(finalizer, thisV, klass, heap2) + Result(resValue, res2.heap, res1.errors ++ errors ++ res2.errors) case SeqLiteral(elems, elemtpt) => - ??? + val (values, errors, heap2) = eval(elems, thisV, klass, heap) + val errors2 = new mutable.ArrayBuffer[Error] + elems.zip(values).foreach { (elem, value) => + if value != Hot then + // TODO: define a new error + errors2 += PromoteCold(elem, Vector(elem)) + } + Result(Hot, heap2, errors2.toList ++ errors) case Inlined(call, bindings, expansion) => - ??? - - case vdef : ValDef => - ??? + val (_, errors, heap2) = eval(bindings, thisV, klass, heap) + eval(expansion, thisV, klass, heap2) ++ errors case Thicket(List()) => // possible in try/catch/finally, see tests/crash/i6914.scala Result(Hot, heap, noErrors) + case vdef : ValDef => + // local val definition + // TODO: support explicit @cold annotation for local definitions + eval(vdef.rhs, thisV, klass, heap).ensureHot("Local definitions may only hold initialized values", vdef) + case ddef : DefDef => - ??? + // local method + Result(Hot, heap, noErrors) case tdef: TypeDef => - ??? + // local type definition + Result(Hot, heap, noErrors) case _: Import | _: Export => Result(Hot, heap, noErrors) From 34e476ab8cf687a209384d8207c02a9d9203e2ec Mon Sep 17 00:00:00 2001 From: Liu Fengyun Date: Thu, 13 May 2021 00:33:19 +0200 Subject: [PATCH 09/50] Handle method call --- .../tools/dotc/transform/init/Semantic.scala | 53 +++++++++++++++---- 1 file changed, 43 insertions(+), 10 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala b/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala index 457c14c67a02..920d113ae38d 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala @@ -119,6 +119,10 @@ class Semantic { def select(f: Symbol, source: Tree)(using Context): Result = value.select(f, heap, source) ++ errors + + def call(meth: Symbol, source: Tree)(using Context): Result = ??? + + def superCall(meth: Symbol, supertpe: Type, source: Tree)(using Context): Result = ??? } val noErrors = Nil @@ -223,10 +227,41 @@ class Semantic { case NewExpr(newExpr, ctor, argss) => ??? - // case supert: Super => Handled in Apply case - case Call(ref, argss) => - ??? + // check args + val args = argss.flatten + val (values, errors, heap2) = eval(args, thisV, klass, heap) + val errors2 = new mutable.ArrayBuffer[Error] + args.zip(values).foreach { (arg, value) => + if value != Hot then + // TODO: define a new error + errors2 += PromoteCold(arg, Vector(arg)) + } + + ref match + case Select(supert: Super, _) => + val SuperType(thisTp, superTp) = supert.tpe + val thisValue2 = resolveThis(thisTp.classSymbol.asClass, thisV, klass, heap2) + Result(thisValue2, heap2, errors ++ errors2.toList).superCall(ref.symbol, superTp, expr) + + case Select(qual, _) => + val res = eval(qual, thisV, klass, heap2) ++ errors ++ errors2.toList + res.call(ref.symbol, expr) + + case id: Ident => + id.tpe match + case TermRef(NoPrefix, _) => + // resolve this for the local method + val enclosingClass = id.symbol.owner.enclosingClass.asClass + val thisValue2 = resolveThis(enclosingClass, thisV, klass, heap) + thisValue2 match + case Hot => Result(Hot, heap2, errors) + case _ => + val rhs = id.symbol.defTree.asInstanceOf[DefDef].rhs + eval(rhs, thisValue2, enclosingClass, heap2) + case TermRef(prefix, _) => + val res = cases(prefix, thisV, klass, heap2, id) ++ errors ++ errors2.toList + res.call(id.symbol, expr) case Select(qualifier, name) => eval(qualifier, thisV, klass, heap).select(expr.symbol, expr) @@ -354,7 +389,7 @@ class Semantic { thisV match case Hot => Hot case addr: Addr => - resolveThis(tp, addr, klass, heap, source) + resolveThis(tp.classSymbol.asClass, addr, klass, heap) case _ => ??? Result(value, heap, noErrors) @@ -368,15 +403,15 @@ class Semantic { } /** Resolve C.this that appear in `klass` */ - def resolveThis(tp: ThisType, thisV: Value, klass: ClassSymbol, heap: Heap, source: Tree)(using Context): Value = trace("resolving " + tp.show + ", this = " + thisV.show + " in " + klass.show, printer, res => res.asInstanceOf[Value].show) { - if tp.classSymbol == klass then thisV + def resolveThis(target: ClassSymbol, thisV: Value, klass: ClassSymbol, heap: Heap)(using Context): Value = trace("resolving " + target.show + ", this = " + thisV.show + " in " + klass.show, printer, res => res.asInstanceOf[Value].show) { + if target == klass then thisV else thisV match case Hot => Hot case thisV: Addr => val outer = heap(thisV).outers.getOrElse(klass, Hot) val outerCls = klass.owner.enclosingClass.asClass - resolveThis(tp, outer, outerCls, heap, source) + resolveThis(target, outer, outerCls, heap) case _ => ??? } @@ -394,9 +429,7 @@ class Semantic { case None => None case TypeApply(fn, targs) => - unapply(fn) match - case Some((ref, args)) => Some((ref, args :+ targs)) - case None => None + unapply(fn) case ref: RefTree if ref.symbol.is(Flags.Method) => Some((ref, Nil)) From ab926d94211d6d9b0a2a213b563d834c64d280a3 Mon Sep 17 00:00:00 2001 From: Liu Fengyun Date: Thu, 13 May 2021 11:10:13 +0200 Subject: [PATCH 10/50] Handle method call in abstract semantics --- .../tools/dotc/transform/init/Semantic.scala | 62 ++++++++++++++++--- 1 file changed, 53 insertions(+), 9 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala b/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala index 920d113ae38d..2c8c4f1ae7b0 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala @@ -14,6 +14,7 @@ import config.Printers.{ init => printer } import reporting.trace import Errors._ +import Util._ import scala.collection.mutable @@ -49,10 +50,10 @@ class Semantic { * needed to finitize addresses. E.g. OOPSLA 2020 paper restricts * args to be either `Hot` or `Cold` */ - case class Addr(klass: Symbol, args: List[Value], outer: Value) extends Value + case class Addr(klass: ClassSymbol, args: List[Value], outer: Value) extends Value /** A function value */ - case class Fun(expr: Tree, thisV: Addr, klass: Symbol) extends Value + case class Fun(expr: Tree, thisV: Addr, klass: ClassSymbol) extends Value /** A value which represents a set of addresses * @@ -68,7 +69,7 @@ class Semantic { * From performance reasons, we cache the immediate outer for all classes * in the inheritance hierarchy. */ - case class Objekt(klass: Symbol, fields: Map[Symbol, Value], outers: Map[ClassSymbol, Value]) + case class Objekt(klass: ClassSymbol, fields: Map[Symbol, Value], outers: Map[ClassSymbol, Value]) /** Abstract heap stores abstract objects * @@ -120,9 +121,8 @@ class Semantic { def select(f: Symbol, source: Tree)(using Context): Result = value.select(f, heap, source) ++ errors - def call(meth: Symbol, source: Tree)(using Context): Result = ??? - - def superCall(meth: Symbol, supertpe: Type, source: Tree)(using Context): Result = ??? + def call(meth: Symbol, superType: Type, source: Tree)(using Context): Result = + value.call(meth, superType, heap, source) ++ errors } val noErrors = Nil @@ -175,6 +175,50 @@ class Semantic { Result(value2, heap, errors) } + def call(meth: Symbol, superType: Type, heap: Heap, source: Tree)(using Context): Result = + value match { + case Hot => + Result(Hot, heap, noErrors) + + case Cold => + val error = CallCold(meth, source, Vector(source)) + Result(Hot, heap, error :: Nil) + + case addr: Addr => + val obj = heap(addr) + val target = + if superType.exists then + // TODO: superType could be A & B when there is self-annotation + resolveSuper(obj.klass, superType.classSymbol.asClass, meth) + else + resolve(obj.klass, meth) + if (target.isOneOf(Flags.Method | Flags.Lazy)) + if target.hasSource then + val rhs = target.defTree.asInstanceOf[DefDef].rhs + eval(rhs, addr, target.owner.asClass, heap) + else + val error = CallUnknown(target, source, Vector(source)) + Result(Hot, heap, error :: Nil) + else + if obj.fields.contains(target) then + Result(obj.fields(target), heap, Nil) + else + val error = AccessNonInit(target, Vector(source)) + Result(Hot, heap, error :: Nil) + + case Fun(body, thisV, klass) => + if meth.name == nme.apply then eval(body, thisV, klass, heap) + else if meth.name.toString == "tupled" then Result(value, heap, Nil) + else Result(Hot, heap, Nil) // TODO: refine + + case RefSet(refs) => + val resList = refs.map(_.call(meth, superType, heap, source)) + val value2 = resList.map(_.value).join + val errors = resList.flatMap(_.errors) + Result(value2, heap, errors) + } + end extension + // ----- Semantic definition -------------------------------- @@ -242,11 +286,11 @@ class Semantic { case Select(supert: Super, _) => val SuperType(thisTp, superTp) = supert.tpe val thisValue2 = resolveThis(thisTp.classSymbol.asClass, thisV, klass, heap2) - Result(thisValue2, heap2, errors ++ errors2.toList).superCall(ref.symbol, superTp, expr) + Result(thisValue2, heap2, errors ++ errors2.toList).call(ref.symbol, superTp, expr) case Select(qual, _) => val res = eval(qual, thisV, klass, heap2) ++ errors ++ errors2.toList - res.call(ref.symbol, expr) + res.call(ref.symbol, superType = NoType, source = expr) case id: Ident => id.tpe match @@ -261,7 +305,7 @@ class Semantic { eval(rhs, thisValue2, enclosingClass, heap2) case TermRef(prefix, _) => val res = cases(prefix, thisV, klass, heap2, id) ++ errors ++ errors2.toList - res.call(id.symbol, expr) + res.call(id.symbol, superType = NoType, source = expr) case Select(qualifier, name) => eval(qualifier, thisV, klass, heap).select(expr.symbol, expr) From 21cfb0f5c166e1d330258a2bf597b6fa9ae35a93 Mon Sep 17 00:00:00 2001 From: Liu Fengyun Date: Thu, 13 May 2021 15:24:52 +0200 Subject: [PATCH 11/50] Don't thread-through heap thanks to monotonicity --- .../tools/dotc/transform/init/Semantic.scala | 184 +++++++++--------- 1 file changed, 94 insertions(+), 90 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala b/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala index 2c8c4f1ae7b0..7505170a0161 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala @@ -75,7 +75,13 @@ class Semantic { * * As in the OOPSLA paper, the abstract heap is monotonistic */ - type Heap = Map[Addr, Objekt] + type Heap = mutable.Map[Addr, Objekt] + + /** The heap for abstract objects + * + * As the heap is monotonistic, we can avoid passing it around. + */ + val heap: Heap = mutable.Map.empty[Addr, Objekt] /** Interpreter configuration * @@ -105,7 +111,7 @@ class Semantic { val cache: Cache = mutable.Map.empty[Config, Value] /** Result of abstract interpretation */ - case class Result(value: Value, heap: Heap, errors: Seq[Error]) { + case class Result(value: Value, errors: Seq[Error]) { def show(using Context) = ??? def ++(errors: Seq[Error]): Result = this.copy(errors = this.errors ++ errors) @@ -119,10 +125,10 @@ class Semantic { this + PromoteCold(source, Vector(source)) def select(f: Symbol, source: Tree)(using Context): Result = - value.select(f, heap, source) ++ errors + value.select(f, source) ++ errors def call(meth: Symbol, superType: Type, source: Tree)(using Context): Result = - value.call(meth, superType, heap, source) ++ errors + value.call(meth, superType, source) ++ errors } val noErrors = Nil @@ -148,41 +154,41 @@ class Semantic { def join: Value = values.reduce { (v1, v2) => v1.join(v2) } extension (value: Value) - def select(f: Symbol, heap: Heap, source: Tree)(using Context): Result = + def select(f: Symbol, source: Tree)(using Context): Result = value match { case Hot => - Result(Hot, heap, noErrors) + Result(Hot, noErrors) case Cold => val error = AccessCold(f, source, Vector(source)) - Result(Hot, heap, error :: Nil) + Result(Hot, error :: Nil) case addr: Addr => val obj = heap(addr) if obj.fields.contains(f) then - Result(obj.fields(f), heap, Nil) + Result(obj.fields(f), Nil) else val error = AccessNonInit(f, Vector(source)) - Result(Hot, heap, error :: Nil) + Result(Hot, error :: Nil) case _: Fun => ??? case RefSet(refs) => - val resList = refs.map(_.select(f, heap, source)) + val resList = refs.map(_.select(f, source)) val value2 = resList.map(_.value).join val errors = resList.flatMap(_.errors) - Result(value2, heap, errors) + Result(value2, errors) } - def call(meth: Symbol, superType: Type, heap: Heap, source: Tree)(using Context): Result = + def call(meth: Symbol, superType: Type, source: Tree)(using Context): Result = value match { case Hot => - Result(Hot, heap, noErrors) + Result(Hot, noErrors) case Cold => val error = CallCold(meth, source, Vector(source)) - Result(Hot, heap, error :: Nil) + Result(Hot, error :: Nil) case addr: Addr => val obj = heap(addr) @@ -195,27 +201,27 @@ class Semantic { if (target.isOneOf(Flags.Method | Flags.Lazy)) if target.hasSource then val rhs = target.defTree.asInstanceOf[DefDef].rhs - eval(rhs, addr, target.owner.asClass, heap) + eval(rhs, addr, target.owner.asClass) else val error = CallUnknown(target, source, Vector(source)) - Result(Hot, heap, error :: Nil) + Result(Hot, error :: Nil) else if obj.fields.contains(target) then - Result(obj.fields(target), heap, Nil) + Result(obj.fields(target), Nil) else val error = AccessNonInit(target, Vector(source)) - Result(Hot, heap, error :: Nil) + Result(Hot, error :: Nil) case Fun(body, thisV, klass) => - if meth.name == nme.apply then eval(body, thisV, klass, heap) - else if meth.name.toString == "tupled" then Result(value, heap, Nil) - else Result(Hot, heap, Nil) // TODO: refine + if meth.name == nme.apply then eval(body, thisV, klass) + else if meth.name.toString == "tupled" then Result(value, Nil) + else Result(Hot, Nil) // TODO: refine case RefSet(refs) => - val resList = refs.map(_.call(meth, superType, heap, source)) + val resList = refs.map(_.call(meth, superType, source)) val value2 = resList.map(_.value).join val errors = resList.flatMap(_.errors) - Result(value2, heap, errors) + Result(value2, errors) } end extension @@ -226,47 +232,45 @@ class Semantic { * * This method only handles cache logic and delegates the work to `cases`. */ - def eval(expr: Tree, thisV: Value, klass: ClassSymbol, heap: Heap)(using Context): Result = trace("evaluating " + expr.show, printer, res => res.asInstanceOf[Result].show) { + def eval(expr: Tree, thisV: Value, klass: ClassSymbol)(using Context): Result = trace("evaluating " + expr.show, printer, res => res.asInstanceOf[Result].show) { val cfg = Config(thisV, expr.sourcePos) - if (cache.contains(cfg)) Result(cache(cfg), heap, noErrors) + if (cache.contains(cfg)) Result(cache(cfg), noErrors) else { // no need to compute fix-point, because // 1. the result is decided by `cfg` for a legal program // (heap change is irrelevant thanks to monotonicity) // 2. errors will have been reported for an illegal program cache(cfg) = Hot - val res = cases(expr, thisV, klass, heap) + val res = cases(expr, thisV, klass) cache(cfg) = res.value res } } /** Evaluate a list of expressions */ - def eval(exprs: List[Tree], thisV: Value, klass: ClassSymbol, heap: Heap)(using Context): (List[Value], List[Error], Heap) = { + def eval(exprs: List[Tree], thisV: Value, klass: ClassSymbol)(using Context): (List[Value], List[Error]) = { val errors = new mutable.ArrayBuffer[Error] - var heap2 = heap val values = exprs.map { binding => - val res = eval(binding, thisV, klass, heap2) - heap2 = res.heap + val res = eval(binding, thisV, klass) errors ++= res.errors res.value } - (values, errors.toList, heap2) + (values, errors.toList) } /** Handles the evaluation of different expressions * * Note: Recursive call should go to `eval` instead of `cases`. */ - def cases(expr: Tree, thisV: Value, klass: ClassSymbol, heap: Heap)(using Context): Result = + def cases(expr: Tree, thisV: Value, klass: ClassSymbol)(using Context): Result = expr match { case Ident(nme.WILDCARD) => // TODO: disallow `var x: T = _` - Result(Hot, heap, noErrors) + Result(Hot, noErrors) case Ident(name) => assert(name.isTermName, "type trees should not reach here") - cases(expr.tpe, thisV, klass, heap, expr) + cases(expr.tpe, thisV, klass, expr) case NewExpr(newExpr, ctor, argss) => ??? @@ -274,7 +278,7 @@ class Semantic { case Call(ref, argss) => // check args val args = argss.flatten - val (values, errors, heap2) = eval(args, thisV, klass, heap) + val (values, errors) = eval(args, thisV, klass) val errors2 = new mutable.ArrayBuffer[Error] args.zip(values).foreach { (arg, value) => if value != Hot then @@ -285,11 +289,11 @@ class Semantic { ref match case Select(supert: Super, _) => val SuperType(thisTp, superTp) = supert.tpe - val thisValue2 = resolveThis(thisTp.classSymbol.asClass, thisV, klass, heap2) - Result(thisValue2, heap2, errors ++ errors2.toList).call(ref.symbol, superTp, expr) + val thisValue2 = resolveThis(thisTp.classSymbol.asClass, thisV, klass) + Result(thisValue2, errors ++ errors2.toList).call(ref.symbol, superTp, expr) case Select(qual, _) => - val res = eval(qual, thisV, klass, heap2) ++ errors ++ errors2.toList + val res = eval(qual, thisV, klass) ++ errors ++ errors2.toList res.call(ref.symbol, superType = NoType, source = expr) case id: Ident => @@ -297,149 +301,149 @@ class Semantic { case TermRef(NoPrefix, _) => // resolve this for the local method val enclosingClass = id.symbol.owner.enclosingClass.asClass - val thisValue2 = resolveThis(enclosingClass, thisV, klass, heap) + val thisValue2 = resolveThis(enclosingClass, thisV, klass) thisValue2 match - case Hot => Result(Hot, heap2, errors) + case Hot => Result(Hot, errors) case _ => val rhs = id.symbol.defTree.asInstanceOf[DefDef].rhs - eval(rhs, thisValue2, enclosingClass, heap2) + eval(rhs, thisValue2, enclosingClass) case TermRef(prefix, _) => - val res = cases(prefix, thisV, klass, heap2, id) ++ errors ++ errors2.toList + val res = cases(prefix, thisV, klass, id) ++ errors ++ errors2.toList res.call(id.symbol, superType = NoType, source = expr) case Select(qualifier, name) => - eval(qualifier, thisV, klass, heap).select(expr.symbol, expr) + eval(qualifier, thisV, klass).select(expr.symbol, expr) case _: This => - cases(expr.tpe, thisV, klass, heap, expr) + cases(expr.tpe, thisV, klass, expr) case Literal(_) => - Result(Hot, heap, noErrors) + Result(Hot, noErrors) case Typed(expr, tpt) => - if (tpt.tpe.hasAnnotation(defn.UncheckedAnnot)) Result(Hot, heap, noErrors) - else eval(expr, thisV, klass, heap) + if (tpt.tpe.hasAnnotation(defn.UncheckedAnnot)) Result(Hot, noErrors) + else eval(expr, thisV, klass) case NamedArg(name, arg) => - eval(arg, thisV, klass, heap) + eval(arg, thisV, klass) case Assign(lhs, rhs) => lhs match case Select(qual, _) => - val res = eval(qual, thisV, klass, heap) - eval(rhs, thisV, klass, res.heap).ensureHot("May only assign initialized value", rhs) ++ res.errors + val res = eval(qual, thisV, klass) + eval(rhs, thisV, klass).ensureHot("May only assign initialized value", rhs) ++ res.errors case id: Ident => - eval(rhs, thisV, klass, heap).ensureHot("May only assign initialized value", rhs) + eval(rhs, thisV, klass).ensureHot("May only assign initialized value", rhs) case closureDef(ddef) => thisV match case addr: Addr => val value = Fun(ddef.rhs, addr, klass) - Result(value, heap, Nil) + Result(value, Nil) case _ => ??? // impossible case Block(stats, expr) => - val (_, errors, heap2) = eval(stats, thisV, klass, heap) - eval(expr, thisV, klass, heap2) ++ errors + val (_, errors) = eval(stats, thisV, klass) + eval(expr, thisV, klass) ++ errors case If(cond, thenp, elsep) => - val (_ :: values, errors, heap2) = eval(cond :: thenp :: elsep :: Nil, thisV, klass, heap) - Result(values.join, heap2, errors) + val (_ :: values, errors) = eval(cond :: thenp :: elsep :: Nil, thisV, klass) + Result(values.join, errors) case Annotated(arg, annot) => - if (expr.tpe.hasAnnotation(defn.UncheckedAnnot)) Result(Hot, heap, noErrors) - else eval(arg, thisV, klass, heap) + if (expr.tpe.hasAnnotation(defn.UncheckedAnnot)) Result(Hot, noErrors) + else eval(arg, thisV, klass) case Match(selector, cases) => - val res1 = eval(selector, thisV, klass, heap) - val (values, errors, heap2) = eval(cases.map(_.body), thisV, klass, res1.heap) - Result(values.join, heap2, res1.errors ++ errors) + val res1 = eval(selector, thisV, klass) + val (values, errors) = eval(cases.map(_.body), thisV, klass) + Result(values.join, res1.errors ++ errors) case Return(expr, from) => - eval(expr, thisV, klass, heap).ensureHot("return expression may only be initialized value", expr) + eval(expr, thisV, klass).ensureHot("return expression may only be initialized value", expr) case WhileDo(cond, body) => - val (_, errors, heap2) = eval(cond :: body :: Nil, thisV, klass, heap) - Result(Hot, heap2, errors) + val (_, errors) = eval(cond :: body :: Nil, thisV, klass) + Result(Hot, errors) case Labeled(_, expr) => - eval(expr, thisV, klass, heap) + eval(expr, thisV, klass) case Try(block, cases, finalizer) => - val res1 = eval(block, thisV, klass, heap) - val (values, errors, heap2) = eval(cases.map(_.body), thisV, klass, res1.heap) + val res1 = eval(block, thisV, klass) + val (values, errors) = eval(cases.map(_.body), thisV, klass) val resValue = values.join if finalizer.isEmpty then - Result(resValue, heap2, res1.errors ++ errors) + Result(resValue, res1.errors ++ errors) else - val res2 = eval(finalizer, thisV, klass, heap2) - Result(resValue, res2.heap, res1.errors ++ errors ++ res2.errors) + val res2 = eval(finalizer, thisV, klass) + Result(resValue, res1.errors ++ errors ++ res2.errors) case SeqLiteral(elems, elemtpt) => - val (values, errors, heap2) = eval(elems, thisV, klass, heap) + val (values, errors) = eval(elems, thisV, klass) val errors2 = new mutable.ArrayBuffer[Error] elems.zip(values).foreach { (elem, value) => if value != Hot then // TODO: define a new error errors2 += PromoteCold(elem, Vector(elem)) } - Result(Hot, heap2, errors2.toList ++ errors) + Result(Hot, errors2.toList ++ errors) case Inlined(call, bindings, expansion) => - val (_, errors, heap2) = eval(bindings, thisV, klass, heap) - eval(expansion, thisV, klass, heap2) ++ errors + val (_, errors) = eval(bindings, thisV, klass) + eval(expansion, thisV, klass) ++ errors case Thicket(List()) => // possible in try/catch/finally, see tests/crash/i6914.scala - Result(Hot, heap, noErrors) + Result(Hot, noErrors) case vdef : ValDef => // local val definition // TODO: support explicit @cold annotation for local definitions - eval(vdef.rhs, thisV, klass, heap).ensureHot("Local definitions may only hold initialized values", vdef) + eval(vdef.rhs, thisV, klass).ensureHot("Local definitions may only hold initialized values", vdef) case ddef : DefDef => // local method - Result(Hot, heap, noErrors) + Result(Hot, noErrors) case tdef: TypeDef => // local type definition - Result(Hot, heap, noErrors) + Result(Hot, noErrors) case _: Import | _: Export => - Result(Hot, heap, noErrors) + Result(Hot, noErrors) case _ => throw new Exception("unexpected tree: " + expr.show) } /** Handle semantics of leaf nodes */ - def cases(tp: Type, thisV: Value, klass: ClassSymbol, heap: Heap, source: Tree)(using Context): Result = trace("evaluating " + tp.show, printer, res => res.asInstanceOf[Result].show) { + def cases(tp: Type, thisV: Value, klass: ClassSymbol, source: Tree)(using Context): Result = trace("evaluating " + tp.show, printer, res => res.asInstanceOf[Result].show) { tp match { case _: ConstantType => - Result(Hot, heap, noErrors) + Result(Hot, noErrors) case tmref: TermRef if tmref.prefix == NoPrefix => - Result(Hot, heap, noErrors) + Result(Hot, noErrors) case tmref: TermRef => - cases(tmref.prefix, thisV, klass, heap, source).select(tmref.symbol, source) + cases(tmref.prefix, thisV, klass, source).select(tmref.symbol, source) case tp @ ThisType(tref) => - if tref.symbol.is(Flags.Package) then Result(Hot, heap, noErrors) + if tref.symbol.is(Flags.Package) then Result(Hot, noErrors) else val value = thisV match case Hot => Hot case addr: Addr => - resolveThis(tp.classSymbol.asClass, addr, klass, heap) + resolveThis(tp.classSymbol.asClass, addr, klass) case _ => ??? - Result(value, heap, noErrors) + Result(value, noErrors) case _: TermParamRef | _: RecThis => // possible from checking effects of types - Result(Hot, heap, noErrors) + Result(Hot, noErrors) case _ => throw new Exception("unexpected type: " + tp) @@ -447,7 +451,7 @@ class Semantic { } /** Resolve C.this that appear in `klass` */ - def resolveThis(target: ClassSymbol, thisV: Value, klass: ClassSymbol, heap: Heap)(using Context): Value = trace("resolving " + target.show + ", this = " + thisV.show + " in " + klass.show, printer, res => res.asInstanceOf[Value].show) { + def resolveThis(target: ClassSymbol, thisV: Value, klass: ClassSymbol)(using Context): Value = trace("resolving " + target.show + ", this = " + thisV.show + " in " + klass.show, printer, res => res.asInstanceOf[Value].show) { if target == klass then thisV else thisV match @@ -455,12 +459,12 @@ class Semantic { case thisV: Addr => val outer = heap(thisV).outers.getOrElse(klass, Hot) val outerCls = klass.owner.enclosingClass.asClass - resolveThis(target, outer, outerCls, heap) + resolveThis(target, outer, outerCls) case _ => ??? } /** Initialize an abstract object */ - def init(klass: Symbol, thisV: Addr, heap: Heap): Result = ??? + def init(klass: Symbol, thisV: Addr): Result = ??? // ----- Utility methods and extractors -------------------------------- From 237ceecdd650a7ffc7504237b9b4eee85812930e Mon Sep 17 00:00:00 2001 From: Liu Fengyun Date: Thu, 13 May 2021 15:39:36 +0200 Subject: [PATCH 12/50] Refactor eval for list of expressions --- .../tools/dotc/transform/init/Semantic.scala | 67 ++++++++----------- 1 file changed, 29 insertions(+), 38 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala b/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala index 7505170a0161..27861ed54eb2 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala @@ -248,15 +248,8 @@ class Semantic { } /** Evaluate a list of expressions */ - def eval(exprs: List[Tree], thisV: Value, klass: ClassSymbol)(using Context): (List[Value], List[Error]) = { - val errors = new mutable.ArrayBuffer[Error] - val values = exprs.map { binding => - val res = eval(binding, thisV, klass) - errors ++= res.errors - res.value - } - (values, errors.toList) - } + def eval(exprs: List[Tree], thisV: Value, klass: ClassSymbol)(using Context): List[Result] = + exprs.map { expr => eval(expr, thisV, klass) } /** Handles the evaluation of different expressions * @@ -278,22 +271,19 @@ class Semantic { case Call(ref, argss) => // check args val args = argss.flatten - val (values, errors) = eval(args, thisV, klass) - val errors2 = new mutable.ArrayBuffer[Error] - args.zip(values).foreach { (arg, value) => - if value != Hot then - // TODO: define a new error - errors2 += PromoteCold(arg, Vector(arg)) + val ress = args.map { arg => + eval(arg, thisV, klass).ensureHot("May use initialized value as arguments", arg) } + val errors = ress.flatMap(_.errors) ref match case Select(supert: Super, _) => val SuperType(thisTp, superTp) = supert.tpe val thisValue2 = resolveThis(thisTp.classSymbol.asClass, thisV, klass) - Result(thisValue2, errors ++ errors2.toList).call(ref.symbol, superTp, expr) + Result(thisValue2, errors).call(ref.symbol, superTp, expr) case Select(qual, _) => - val res = eval(qual, thisV, klass) ++ errors ++ errors2.toList + val res = eval(qual, thisV, klass) ++ errors res.call(ref.symbol, superType = NoType, source = expr) case id: Ident => @@ -308,7 +298,7 @@ class Semantic { val rhs = id.symbol.defTree.asInstanceOf[DefDef].rhs eval(rhs, thisValue2, enclosingClass) case TermRef(prefix, _) => - val res = cases(prefix, thisV, klass, id) ++ errors ++ errors2.toList + val res = cases(prefix, thisV, klass, id) ++ errors res.call(id.symbol, superType = NoType, source = expr) case Select(qualifier, name) => @@ -344,36 +334,41 @@ class Semantic { ??? // impossible case Block(stats, expr) => - val (_, errors) = eval(stats, thisV, klass) - eval(expr, thisV, klass) ++ errors + val ress = eval(stats, thisV, klass) + eval(expr, thisV, klass) ++ ress.flatMap(_.errors) case If(cond, thenp, elsep) => - val (_ :: values, errors) = eval(cond :: thenp :: elsep :: Nil, thisV, klass) - Result(values.join, errors) + val ress = eval(cond :: thenp :: elsep :: Nil, thisV, klass) + val value = ress.map(_.value).join + val errors = ress.flatMap(_.errors) + Result(value, errors) case Annotated(arg, annot) => if (expr.tpe.hasAnnotation(defn.UncheckedAnnot)) Result(Hot, noErrors) else eval(arg, thisV, klass) case Match(selector, cases) => - val res1 = eval(selector, thisV, klass) - val (values, errors) = eval(cases.map(_.body), thisV, klass) - Result(values.join, res1.errors ++ errors) + val res1 = eval(selector, thisV, klass).ensureHot("The value to be matched needs to be initialized", selector) + val ress = eval(cases.map(_.body), thisV, klass) + val value = ress.map(_.value).join + val errors = res1.errors ++ ress.flatMap(_.errors) + Result(value, errors) case Return(expr, from) => eval(expr, thisV, klass).ensureHot("return expression may only be initialized value", expr) case WhileDo(cond, body) => - val (_, errors) = eval(cond :: body :: Nil, thisV, klass) - Result(Hot, errors) + val ress = eval(cond :: body :: Nil, thisV, klass) + Result(Hot, ress.flatMap(_.errors)) case Labeled(_, expr) => eval(expr, thisV, klass) case Try(block, cases, finalizer) => val res1 = eval(block, thisV, klass) - val (values, errors) = eval(cases.map(_.body), thisV, klass) - val resValue = values.join + val ress = eval(cases.map(_.body), thisV, klass) + val errors = ress.flatMap(_.errors) + val resValue = ress.map(_.value).join if finalizer.isEmpty then Result(resValue, res1.errors ++ errors) else @@ -381,18 +376,14 @@ class Semantic { Result(resValue, res1.errors ++ errors ++ res2.errors) case SeqLiteral(elems, elemtpt) => - val (values, errors) = eval(elems, thisV, klass) - val errors2 = new mutable.ArrayBuffer[Error] - elems.zip(values).foreach { (elem, value) => - if value != Hot then - // TODO: define a new error - errors2 += PromoteCold(elem, Vector(elem)) + val ress = elems.map { elem => + eval(elem, thisV, klass).ensureHot("May only use initialized value as arguments", elem) } - Result(Hot, errors2.toList ++ errors) + Result(Hot, ress.flatMap(_.errors)) case Inlined(call, bindings, expansion) => - val (_, errors) = eval(bindings, thisV, klass) - eval(expansion, thisV, klass) ++ errors + val ress = eval(bindings, thisV, klass) + eval(expansion, thisV, klass) ++ ress.flatMap(_.errors) case Thicket(List()) => // possible in try/catch/finally, see tests/crash/i6914.scala From 3058635cb1bd29a3f88b22d1b4871b573862dfe2 Mon Sep 17 00:00:00 2001 From: Liu Fengyun Date: Thu, 13 May 2021 16:06:48 +0200 Subject: [PATCH 13/50] Implement case for new expressions --- .../tools/dotc/transform/init/Semantic.scala | 33 ++++++++++++++++--- 1 file changed, 29 insertions(+), 4 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala b/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala index 27861ed54eb2..b8c5dc83bb81 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala @@ -50,7 +50,7 @@ class Semantic { * needed to finitize addresses. E.g. OOPSLA 2020 paper restricts * args to be either `Hot` or `Cold` */ - case class Addr(klass: ClassSymbol, args: List[Value], outer: Value) extends Value + case class Addr(klass: ClassSymbol, outer: Value) extends Value /** A function value */ case class Fun(expr: Tree, thisV: Addr, klass: ClassSymbol) extends Value @@ -129,6 +129,9 @@ class Semantic { def call(meth: Symbol, superType: Type, source: Tree)(using Context): Result = value.call(meth, superType, source) ++ errors + + def instantiate(klass: ClassSymbol, source: Tree)(using Context): Result = + value.instantiate(klass, source) ++ errors } val noErrors = Nil @@ -223,6 +226,8 @@ class Semantic { val errors = resList.flatMap(_.errors) Result(value2, errors) } + + def instantiate(klass: ClassSymbol, source: Tree)(using Context): Result = ??? end extension @@ -265,8 +270,28 @@ class Semantic { assert(name.isTermName, "type trees should not reach here") cases(expr.tpe, thisV, klass, expr) - case NewExpr(newExpr, ctor, argss) => - ??? + case NewExpr(New(tpt), ctor, argss) => + def typeRefOf(tp: Type): TypeRef = tp.dealias.typeConstructor match { + case tref: TypeRef => tref + case hklambda: HKTypeLambda => typeRefOf(hklambda.resType) + } + + // check args + val args = argss.flatten + val ress = args.map { arg => + eval(arg, thisV, klass).ensureHot("May use initialized value as arguments", arg) + } + val errors = ress.flatMap(_.errors) + + val tref = typeRefOf(tpt.tpe) + val cls = tref.classSymbol.asClass + if (tref.prefix == NoPrefix) then + val enclosing = cls.owner.lexicallyEnclosingClass.asClass + val outer = resolveThis(enclosing, thisV, klass) + Result(outer, errors).instantiate(cls, expr) + else + val res = cases(tref.prefix, thisV, klass, tpt) + (res ++ errors).instantiate(cls, expr) case Call(ref, argss) => // check args @@ -475,7 +500,7 @@ class Semantic { } object NewExpr { - def unapply(tree: Tree)(using Context): Option[(Tree, Symbol, List[List[Tree]])] = + def unapply(tree: Tree)(using Context): Option[(New, Symbol, List[List[Tree]])] = tree match case Call(fn @ Select(newTree: New, init), argss) if init == nme.CONSTRUCTOR => Some((newTree, fn.symbol, argss)) From a8c70d58dae6c28b8fe7f5c009d3675334bb9429 Mon Sep 17 00:00:00 2001 From: Liu Fengyun Date: Fri, 14 May 2021 10:28:43 +0200 Subject: [PATCH 14/50] Handle instantiation in abstract semantics --- .../tools/dotc/transform/init/Semantic.scala | 37 ++++++++++++++++--- 1 file changed, 31 insertions(+), 6 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala b/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala index b8c5dc83bb81..0aa334fee0d6 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala @@ -130,8 +130,8 @@ class Semantic { def call(meth: Symbol, superType: Type, source: Tree)(using Context): Result = value.call(meth, superType, source) ++ errors - def instantiate(klass: ClassSymbol, source: Tree)(using Context): Result = - value.instantiate(klass, source) ++ errors + def instantiate(klass: ClassSymbol, ctor: Symbol, source: Tree)(using Context): Result = + value.instantiate(klass, ctor, source) ++ errors } val noErrors = Nil @@ -201,7 +201,9 @@ class Semantic { resolveSuper(obj.klass, superType.classSymbol.asClass, meth) else resolve(obj.klass, meth) - if (target.isOneOf(Flags.Method | Flags.Lazy)) + if target.isPrimaryConstructor then + init(addr.klass, addr) + else if target.isOneOf(Flags.Method | Flags.Lazy) then if target.hasSource then val rhs = target.defTree.asInstanceOf[DefDef].rhs eval(rhs, addr, target.owner.asClass) @@ -227,7 +229,30 @@ class Semantic { Result(value2, errors) } - def instantiate(klass: ClassSymbol, source: Tree)(using Context): Result = ??? + def instantiate(klass: ClassSymbol, ctor: Symbol, source: Tree)(using Context): Result = + value match { + case Hot => + Result(Hot, noErrors) + + case Cold => + val error = CallCold(ctor, source, Vector(source)) + Result(Hot, error :: Nil) + + case addr: Addr => + // widen the outer to finitize addresses + val outer = if addr.outer.isInstanceOf[Addr] then addr.copy(outer = Cold) else addr + val addr2 = Addr(klass, outer) + addr2.call(ctor, superType = NoType, source) + + case Fun(body, thisV, klass) => + ??? // impossible + + case RefSet(refs) => + val resList = refs.map(_.instantiate(klass, ctor, source)) + val value2 = resList.map(_.value).join + val errors = resList.flatMap(_.errors) + Result(value2, errors) + } end extension @@ -288,10 +313,10 @@ class Semantic { if (tref.prefix == NoPrefix) then val enclosing = cls.owner.lexicallyEnclosingClass.asClass val outer = resolveThis(enclosing, thisV, klass) - Result(outer, errors).instantiate(cls, expr) + Result(outer, errors).instantiate(cls, ctor, expr) else val res = cases(tref.prefix, thisV, klass, tpt) - (res ++ errors).instantiate(cls, expr) + (res ++ errors).instantiate(cls, ctor, expr) case Call(ref, argss) => // check args From 6c7f170af120114a9da6abb7eb939bb6358e4f28 Mon Sep 17 00:00:00 2001 From: Liu Fengyun Date: Fri, 14 May 2021 12:18:02 +0200 Subject: [PATCH 15/50] Initial implementation of object initialization --- .../tools/dotc/transform/init/Semantic.scala | 110 +++++++++++++++--- 1 file changed, 92 insertions(+), 18 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala b/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala index 0aa334fee0d6..8c9765151ff9 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala @@ -242,6 +242,8 @@ class Semantic { // widen the outer to finitize addresses val outer = if addr.outer.isInstanceOf[Addr] then addr.copy(outer = Cold) else addr val addr2 = Addr(klass, outer) + if !heap.contains(addr2) then + heap(addr2) = Objekt(klass, Map.empty, Map(klass -> outer)) addr2.call(ctor, superType = NoType, source) case Fun(body, thisV, klass) => @@ -295,12 +297,7 @@ class Semantic { assert(name.isTermName, "type trees should not reach here") cases(expr.tpe, thisV, klass, expr) - case NewExpr(New(tpt), ctor, argss) => - def typeRefOf(tp: Type): TypeRef = tp.dealias.typeConstructor match { - case tref: TypeRef => tref - case hklambda: HKTypeLambda => typeRefOf(hklambda.resType) - } - + case NewExpr(tref, New(tpt), ctor, argss) => // check args val args = argss.flatten val ress = args.map { arg => @@ -308,15 +305,9 @@ class Semantic { } val errors = ress.flatMap(_.errors) - val tref = typeRefOf(tpt.tpe) val cls = tref.classSymbol.asClass - if (tref.prefix == NoPrefix) then - val enclosing = cls.owner.lexicallyEnclosingClass.asClass - val outer = resolveThis(enclosing, thisV, klass) - Result(outer, errors).instantiate(cls, ctor, expr) - else - val res = cases(tref.prefix, thisV, klass, tpt) - (res ++ errors).instantiate(cls, ctor, expr) + val res = outerValue(tref, thisV, klass, tpt) + (res ++ errors).instantiate(cls, ctor, expr) case Call(ref, argss) => // check args @@ -504,8 +495,85 @@ class Semantic { case _ => ??? } - /** Initialize an abstract object */ - def init(klass: Symbol, thisV: Addr): Result = ??? + /** Compute the outer value that correspond to `tref.prefix` */ + def outerValue(tref: TypeRef, thisV: Value, klass: ClassSymbol, source: Tree)(using Context): Result = + val cls = tref.classSymbol.asClass + if (tref.prefix == NoPrefix) then + val enclosing = cls.owner.lexicallyEnclosingClass.asClass + val outerV = resolveThis(enclosing, thisV, klass) + Result(outerV, noErrors) + else + cases(tref.prefix, thisV, klass, source) + + /** Initialize part of an abstract object in `klass` of the inheritance chain */ + def init(klass: ClassSymbol, thisV: Addr)(using Context): Result = + val errorBuffer = new mutable.ArrayBuffer[Error] + + val tpl = klass.defTree.asInstanceOf[TypeDef].rhs.asInstanceOf[Template] + + // init param fields + var obj = heap(thisV) + klass.paramAccessors.foreach { acc => + if (!acc.is(Flags.Method)) { + traceIndented(acc.show + " initialized", printer) + obj = obj.copy(fields = obj.fields.updated(acc, Hot)) + } + } + heap(thisV) = obj + + def superCall(tref: TypeRef, ctor: Symbol, source: Tree): Unit = + // update outer for super class + val cls = tref.classSymbol.asClass + val res = outerValue(tref, thisV, klass, source) + errorBuffer ++= res.errors + val obj = heap(thisV) + val obj2 = obj.copy(outers = obj.outers.updated(cls, res.value)) + heap(thisV) = obj2 + + // follow constructor + val res2 = thisV.call(ctor, superType = NoType, source) + errorBuffer ++= res2.errors + + // parents + tpl.parents.foreach { + case tree @ Block(stats, NewExpr(tref, New(tpt), ctor, argss)) => + eval(stats, thisV, klass).foreach { res => errorBuffer ++= res.errors } + argss.flatten.foreach { arg => + val res = eval(arg, thisV, klass) + res.ensureHot("Argument must be an initialized value", arg) + errorBuffer ++ res.errors + } + superCall(tref, ctor, tree) + + case tree @ NewExpr(tref, New(tpt), ctor, argss) => + argss.flatten.foreach { arg => + val res = eval(arg, thisV, klass) + res.ensureHot("Argument must be an initialized value", arg) + errorBuffer ++ res.errors + } + superCall(tref, ctor, tree) + + case ref: RefTree => + val tref = ref.tpe.asInstanceOf[TypeRef] + superCall(tref, tref.classSymbol.primaryConstructor, ref) + } + + // class body + tpl.body.foreach { + case vdef : ValDef => + val res = eval(vdef.rhs, thisV, klass) + errorBuffer ++ res.errors + val obj = heap(thisV) + val obj2 = obj.copy(fields = obj.fields.updated(vdef.symbol, res.value)) + heap(thisV) = obj2 + + case _: MemberDef => + + case tree => + eval(tree, thisV, klass) + } + + Result(thisV, errorBuffer.toList) // ----- Utility methods and extractors -------------------------------- @@ -525,10 +593,16 @@ class Semantic { } object NewExpr { - def unapply(tree: Tree)(using Context): Option[(New, Symbol, List[List[Tree]])] = + private def typeRefOf(tp: Type)(using Context): TypeRef = tp.dealias.typeConstructor match { + case tref: TypeRef => tref + case hklambda: HKTypeLambda => typeRefOf(hklambda.resType) + } + + def unapply(tree: Tree)(using Context): Option[(TypeRef, New, Symbol, List[List[Tree]])] = tree match case Call(fn @ Select(newTree: New, init), argss) if init == nme.CONSTRUCTOR => - Some((newTree, fn.symbol, argss)) + val tref = typeRefOf(newTree.tpe) + Some((tref, newTree, fn.symbol, argss)) case _ => None } } From f8abf7edd9f46a487721118ae95626b8c8278dfc Mon Sep 17 00:00:00 2001 From: Liu Fengyun Date: Fri, 14 May 2021 12:29:59 +0200 Subject: [PATCH 16/50] Refactor: use extension methods for heap operations --- .../tools/dotc/transform/init/Semantic.scala | 20 +++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala b/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala index 8c9765151ff9..70dab75f1c12 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala @@ -257,6 +257,18 @@ class Semantic { } end extension + extension (addr: Addr) + def updateOuter(klass: ClassSymbol, value: Value): Unit = + val obj = heap(addr) + val obj2 = obj.copy(outers = obj.outers.updated(klass, value)) + heap(addr) = obj2 + + def updateField(field: Symbol, value: Value): Unit = + val obj = heap(addr) + val obj2 = obj.copy(fields = obj.fields.updated(field, value)) + heap(addr) = obj2 + end extension + // ----- Semantic definition -------------------------------- @@ -526,9 +538,7 @@ class Semantic { val cls = tref.classSymbol.asClass val res = outerValue(tref, thisV, klass, source) errorBuffer ++= res.errors - val obj = heap(thisV) - val obj2 = obj.copy(outers = obj.outers.updated(cls, res.value)) - heap(thisV) = obj2 + thisV.updateOuter(cls, res.value) // follow constructor val res2 = thisV.call(ctor, superType = NoType, source) @@ -563,9 +573,7 @@ class Semantic { case vdef : ValDef => val res = eval(vdef.rhs, thisV, klass) errorBuffer ++ res.errors - val obj = heap(thisV) - val obj2 = obj.copy(fields = obj.fields.updated(vdef.symbol, res.value)) - heap(thisV) = obj2 + thisV.updateField(vdef.symbol, res.value) case _: MemberDef => From 0a39a8f4f2f93132a5e6d15bb673919e2396246b Mon Sep 17 00:00:00 2001 From: Liu Fengyun Date: Fri, 14 May 2021 13:16:28 +0200 Subject: [PATCH 17/50] Be faithful to initialization semantics --- .../tools/dotc/transform/init/Semantic.scala | 32 +++++++++++++++---- 1 file changed, 26 insertions(+), 6 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala b/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala index 70dab75f1c12..535bd9157367 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala @@ -545,7 +545,7 @@ class Semantic { errorBuffer ++= res2.errors // parents - tpl.parents.foreach { + def initParent(parent: Tree) = parent match { case tree @ Block(stats, NewExpr(tref, New(tpt), ctor, argss)) => eval(stats, thisV, klass).foreach { res => errorBuffer ++= res.errors } argss.flatten.foreach { arg => @@ -568,6 +568,26 @@ class Semantic { superCall(tref, tref.classSymbol.primaryConstructor, ref) } + // 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 => + val tref = typeRefOf(klass.typeRef.baseType(mixin).typeConstructor) + superCall(tref, tref.classSymbol.primaryConstructor, superParent) + } + + // class body tpl.body.foreach { case vdef : ValDef => @@ -585,6 +605,11 @@ class Semantic { // ----- Utility methods and extractors -------------------------------- + def typeRefOf(tp: Type)(using Context): TypeRef = tp.dealias.typeConstructor match { + case tref: TypeRef => tref + case hklambda: HKTypeLambda => typeRefOf(hklambda.resType) + } + object Call { def unapply(tree: Tree)(using Context): Option[(Tree, List[List[Tree]])] = tree match @@ -601,11 +626,6 @@ class Semantic { } object NewExpr { - private def typeRefOf(tp: Type)(using Context): TypeRef = tp.dealias.typeConstructor match { - case tref: TypeRef => tref - case hklambda: HKTypeLambda => typeRefOf(hklambda.resType) - } - def unapply(tree: Tree)(using Context): Option[(TypeRef, New, Symbol, List[List[Tree]])] = tree match case Call(fn @ Select(newTree: New, init), argss) if init == nme.CONSTRUCTOR => From 4f298c03390146804f356e472c6fdc0a8fca4ea0 Mon Sep 17 00:00:00 2001 From: Liu Fengyun Date: Fri, 14 May 2021 14:23:11 +0200 Subject: [PATCH 18/50] First example works --- .../tools/dotc/transform/init/Checker.scala | 10 +++++++- .../tools/dotc/transform/init/Semantic.scala | 23 +++++++++++-------- 2 files changed, 23 insertions(+), 10 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/transform/init/Checker.scala b/compiler/src/dotty/tools/dotc/transform/init/Checker.scala index ec15d35096f6..cb6dead4c49a 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Checker.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Checker.scala @@ -26,6 +26,8 @@ class Checker extends MiniPhase { // cache of class summary private val cache = new Cache + private val semantic = new Semantic + override val runsAfter = Set(Pickler.name) override def isEnabled(using Context): Boolean = @@ -57,7 +59,13 @@ class Checker extends MiniPhase { env = Env(ctx.withOwner(cls), cache) ) - Checking.checkClassBody(tree) + // Checking.checkClassBody(tree) + + import semantic._ + val addr = Addr(cls, outer = Hot) + heap(addr) = Objekt(cls, Map.empty, Map(cls -> Hot)) + val res = addr.call(cls.primaryConstructor, superType = NoType, tree) + res.errors.foreach(_.issue) } tree diff --git a/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala b/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala index 535bd9157367..28299753a297 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala @@ -112,7 +112,7 @@ class Semantic { /** Result of abstract interpretation */ case class Result(value: Value, errors: Seq[Error]) { - def show(using Context) = ??? + def show(using Context) = value.show + ", errors = " + errors.map(_.toString) def ++(errors: Seq[Error]): Result = this.copy(errors = this.errors ++ errors) @@ -202,7 +202,7 @@ class Semantic { else resolve(obj.klass, meth) if target.isPrimaryConstructor then - init(addr.klass, addr) + init(target.owner.asClass, addr) else if target.isOneOf(Flags.Method | Flags.Lazy) then if target.hasSource then val rhs = target.defTree.asInstanceOf[DefDef].rhs @@ -518,7 +518,7 @@ class Semantic { cases(tref.prefix, thisV, klass, source) /** Initialize part of an abstract object in `klass` of the inheritance chain */ - def init(klass: ClassSymbol, thisV: Addr)(using Context): Result = + def init(klass: ClassSymbol, thisV: Addr)(using Context): Result = trace("init " + klass.show, printer, res => res.asInstanceOf[Result].show) { val errorBuffer = new mutable.ArrayBuffer[Error] val tpl = klass.defTree.asInstanceOf[TypeDef].rhs.asInstanceOf[Template] @@ -541,8 +541,9 @@ class Semantic { thisV.updateOuter(cls, res.value) // follow constructor - val res2 = thisV.call(ctor, superType = NoType, source) - errorBuffer ++= res2.errors + if !cls.defTree.isEmpty then + val res2 = thisV.call(ctor, superType = NoType, source) + errorBuffer ++= res2.errors // parents def initParent(parent: Tree) = parent match { @@ -551,7 +552,7 @@ class Semantic { argss.flatten.foreach { arg => val res = eval(arg, thisV, klass) res.ensureHot("Argument must be an initialized value", arg) - errorBuffer ++ res.errors + errorBuffer ++= res.errors } superCall(tref, ctor, tree) @@ -559,7 +560,7 @@ class Semantic { argss.flatten.foreach { arg => val res = eval(arg, thisV, klass) res.ensureHot("Argument must be an initialized value", arg) - errorBuffer ++ res.errors + errorBuffer ++= res.errors } superCall(tref, ctor, tree) @@ -584,7 +585,8 @@ class Semantic { case Some(parent) => initParent(parent) case None => val tref = typeRefOf(klass.typeRef.baseType(mixin).typeConstructor) - superCall(tref, tref.classSymbol.primaryConstructor, superParent) + val ctor = tref.classSymbol.primaryConstructor + if ctor.exists then superCall(tref, ctor, superParent) } @@ -592,7 +594,7 @@ class Semantic { tpl.body.foreach { case vdef : ValDef => val res = eval(vdef.rhs, thisV, klass) - errorBuffer ++ res.errors + errorBuffer ++= res.errors thisV.updateField(vdef.symbol, res.value) case _: MemberDef => @@ -602,6 +604,7 @@ class Semantic { } Result(thisV, errorBuffer.toList) + } // ----- Utility methods and extractors -------------------------------- @@ -623,6 +626,8 @@ class Semantic { case ref: RefTree if ref.symbol.is(Flags.Method) => Some((ref, Nil)) + + case _ => None } object NewExpr { From dd65e1ad25d867cf9294149598f090b81792d151 Mon Sep 17 00:00:00 2001 From: Liu Fengyun Date: Fri, 14 May 2021 14:43:59 +0200 Subject: [PATCH 19/50] Show stacktrace for errors --- .../tools/dotc/transform/init/Checker.scala | 2 +- .../tools/dotc/transform/init/Semantic.scala | 62 +++++++++++-------- 2 files changed, 36 insertions(+), 28 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/transform/init/Checker.scala b/compiler/src/dotty/tools/dotc/transform/init/Checker.scala index cb6dead4c49a..58ef132607d9 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Checker.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Checker.scala @@ -64,7 +64,7 @@ class Checker extends MiniPhase { import semantic._ val addr = Addr(cls, outer = Hot) heap(addr) = Objekt(cls, Map.empty, Map(cls -> Hot)) - val res = addr.call(cls.primaryConstructor, superType = NoType, tree) + val res = addr.call(cls.primaryConstructor, superType = NoType, tree)(using ctx, Vector.empty) res.errors.foreach(_.issue) } diff --git a/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala b/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala index 28299753a297..1ca27a5484a7 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala @@ -10,8 +10,8 @@ import StdNames._ import ast.tpd._ import util.SourcePosition -import config.Printers.{ init => printer } -import reporting.trace +import config.Printers.init as printer +import reporting.trace as log import Errors._ import Util._ @@ -118,24 +118,32 @@ class Semantic { def +(error: Error): Result = this.copy(errors = this.errors :+ error) - def ensureHot(msg: String, source: Tree): Result = + def ensureHot(msg: String, source: Tree)(using Trace): Result = if value == Hot then this else // TODO: define a new error - this + PromoteCold(source, Vector(source)) + this + PromoteCold(source, trace) - def select(f: Symbol, source: Tree)(using Context): Result = + def select(f: Symbol, source: Tree)(using Context, Trace): Result = value.select(f, source) ++ errors - def call(meth: Symbol, superType: Type, source: Tree)(using Context): Result = + def call(meth: Symbol, superType: Type, source: Tree)(using Context, Trace): Result = value.call(meth, superType, source) ++ errors - def instantiate(klass: ClassSymbol, ctor: Symbol, source: Tree)(using Context): Result = + def instantiate(klass: ClassSymbol, ctor: Symbol, source: Tree)(using Context, Trace): Result = value.instantiate(klass, ctor, source) ++ errors } +// ----- Error Handling ----------------------------------- + type Trace = Vector[Tree] + val noErrors = Nil + extension (trace: Trace) + def add(node: Tree): Trace = trace :+ node + + def trace(using t: Trace): Trace = t + // ----- Operations on domains ----------------------------- extension (a: Value) def join(b: Value): Value = @@ -157,13 +165,13 @@ class Semantic { def join: Value = values.reduce { (v1, v2) => v1.join(v2) } extension (value: Value) - def select(f: Symbol, source: Tree)(using Context): Result = + def select(f: Symbol, source: Tree)(using Context, Trace): Result = value match { case Hot => Result(Hot, noErrors) case Cold => - val error = AccessCold(f, source, Vector(source)) + val error = AccessCold(f, source, trace) Result(Hot, error :: Nil) case addr: Addr => @@ -171,7 +179,7 @@ class Semantic { if obj.fields.contains(f) then Result(obj.fields(f), Nil) else - val error = AccessNonInit(f, Vector(source)) + val error = AccessNonInit(f, trace.add(source)) Result(Hot, error :: Nil) case _: Fun => @@ -184,13 +192,13 @@ class Semantic { Result(value2, errors) } - def call(meth: Symbol, superType: Type, source: Tree)(using Context): Result = + def call(meth: Symbol, superType: Type, source: Tree)(using Context, Trace): Result = value match { case Hot => Result(Hot, noErrors) case Cold => - val error = CallCold(meth, source, Vector(source)) + val error = CallCold(meth, source, trace) Result(Hot, error :: Nil) case addr: Addr => @@ -208,13 +216,13 @@ class Semantic { val rhs = target.defTree.asInstanceOf[DefDef].rhs eval(rhs, addr, target.owner.asClass) else - val error = CallUnknown(target, source, Vector(source)) + val error = CallUnknown(target, source, trace) Result(Hot, error :: Nil) else if obj.fields.contains(target) then Result(obj.fields(target), Nil) else - val error = AccessNonInit(target, Vector(source)) + val error = AccessNonInit(target, trace.add(source)) Result(Hot, error :: Nil) case Fun(body, thisV, klass) => @@ -229,13 +237,13 @@ class Semantic { Result(value2, errors) } - def instantiate(klass: ClassSymbol, ctor: Symbol, source: Tree)(using Context): Result = + def instantiate(klass: ClassSymbol, ctor: Symbol, source: Tree)(using Context, Trace): Result = value match { case Hot => Result(Hot, noErrors) case Cold => - val error = CallCold(ctor, source, Vector(source)) + val error = CallCold(ctor, source, trace) Result(Hot, error :: Nil) case addr: Addr => @@ -276,7 +284,7 @@ class Semantic { * * This method only handles cache logic and delegates the work to `cases`. */ - def eval(expr: Tree, thisV: Value, klass: ClassSymbol)(using Context): Result = trace("evaluating " + expr.show, printer, res => res.asInstanceOf[Result].show) { + def eval(expr: Tree, thisV: Value, klass: ClassSymbol)(using Context, Trace): Result = log("evaluating " + expr.show, printer, res => res.asInstanceOf[Result].show) { val cfg = Config(thisV, expr.sourcePos) if (cache.contains(cfg)) Result(cache(cfg), noErrors) else { @@ -292,14 +300,14 @@ class Semantic { } /** Evaluate a list of expressions */ - def eval(exprs: List[Tree], thisV: Value, klass: ClassSymbol)(using Context): List[Result] = + def eval(exprs: List[Tree], thisV: Value, klass: ClassSymbol)(using Context, Trace): List[Result] = exprs.map { expr => eval(expr, thisV, klass) } /** Handles the evaluation of different expressions * * Note: Recursive call should go to `eval` instead of `cases`. */ - def cases(expr: Tree, thisV: Value, klass: ClassSymbol)(using Context): Result = + def cases(expr: Tree, thisV: Value, klass: ClassSymbol)(using Context, Trace): Result = expr match { case Ident(nme.WILDCARD) => // TODO: disallow `var x: T = _` @@ -333,11 +341,11 @@ class Semantic { case Select(supert: Super, _) => val SuperType(thisTp, superTp) = supert.tpe val thisValue2 = resolveThis(thisTp.classSymbol.asClass, thisV, klass) - Result(thisValue2, errors).call(ref.symbol, superTp, expr) + Result(thisValue2, errors).call(ref.symbol, superTp, expr)(using ctx, trace.add(expr)) case Select(qual, _) => val res = eval(qual, thisV, klass) ++ errors - res.call(ref.symbol, superType = NoType, source = expr) + res.call(ref.symbol, superType = NoType, source = expr)(using ctx, trace.add(expr)) case id: Ident => id.tpe match @@ -349,10 +357,10 @@ class Semantic { case Hot => Result(Hot, errors) case _ => val rhs = id.symbol.defTree.asInstanceOf[DefDef].rhs - eval(rhs, thisValue2, enclosingClass) + eval(rhs, thisValue2, enclosingClass)(using ctx, trace.add(expr)) case TermRef(prefix, _) => val res = cases(prefix, thisV, klass, id) ++ errors - res.call(id.symbol, superType = NoType, source = expr) + res.call(id.symbol, superType = NoType, source = expr)(using ctx, trace.add(expr)) case Select(qualifier, name) => eval(qualifier, thisV, klass).select(expr.symbol, expr) @@ -463,7 +471,7 @@ class Semantic { } /** Handle semantics of leaf nodes */ - def cases(tp: Type, thisV: Value, klass: ClassSymbol, source: Tree)(using Context): Result = trace("evaluating " + tp.show, printer, res => res.asInstanceOf[Result].show) { + def cases(tp: Type, thisV: Value, klass: ClassSymbol, source: Tree)(using Context, Trace): Result = log("evaluating " + tp.show, printer, res => res.asInstanceOf[Result].show) { tp match { case _: ConstantType => Result(Hot, noErrors) @@ -495,7 +503,7 @@ class Semantic { } /** Resolve C.this that appear in `klass` */ - def resolveThis(target: ClassSymbol, thisV: Value, klass: ClassSymbol)(using Context): Value = trace("resolving " + target.show + ", this = " + thisV.show + " in " + klass.show, printer, res => res.asInstanceOf[Value].show) { + def resolveThis(target: ClassSymbol, thisV: Value, klass: ClassSymbol)(using Context, Trace): Value = log("resolving " + target.show + ", this = " + thisV.show + " in " + klass.show, printer, res => res.asInstanceOf[Value].show) { if target == klass then thisV else thisV match @@ -508,7 +516,7 @@ class Semantic { } /** Compute the outer value that correspond to `tref.prefix` */ - def outerValue(tref: TypeRef, thisV: Value, klass: ClassSymbol, source: Tree)(using Context): Result = + def outerValue(tref: TypeRef, thisV: Value, klass: ClassSymbol, source: Tree)(using Context, Trace): Result = val cls = tref.classSymbol.asClass if (tref.prefix == NoPrefix) then val enclosing = cls.owner.lexicallyEnclosingClass.asClass @@ -518,7 +526,7 @@ class Semantic { cases(tref.prefix, thisV, klass, source) /** Initialize part of an abstract object in `klass` of the inheritance chain */ - def init(klass: ClassSymbol, thisV: Addr)(using Context): Result = trace("init " + klass.show, printer, res => res.asInstanceOf[Result].show) { + def init(klass: ClassSymbol, thisV: Addr)(using Context, Trace): Result = log("init " + klass.show, printer, res => res.asInstanceOf[Result].show) { val errorBuffer = new mutable.ArrayBuffer[Error] val tpl = klass.defTree.asInstanceOf[TypeDef].rhs.asInstanceOf[Template] From 849a5df01f029cac2311396555476bf5c54ef377 Mon Sep 17 00:00:00 2001 From: Liu Fengyun Date: Sat, 15 May 2021 01:32:33 +0200 Subject: [PATCH 20/50] Refactor domain: Introduce Warm and ThisRef This brings us closer to OOPSLA paper. The key insight to avoid cycles like the following ``` class A(b: B) { val b2 = new B(this) } class B(a: A) { val a2 = new A(this) } ``` is to use `Warm(C, outer)` to evaluate fields and methods of `C`. The evaluation results are cached uniformly. Therefore, there is no need to treat `Warm(C, outer)` as addresses. --- .../tools/dotc/transform/init/Checker.scala | 5 +- .../tools/dotc/transform/init/Semantic.scala | 189 +++++++++--------- 2 files changed, 95 insertions(+), 99 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/transform/init/Checker.scala b/compiler/src/dotty/tools/dotc/transform/init/Checker.scala index 58ef132607d9..280bd2bf65c4 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Checker.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Checker.scala @@ -62,9 +62,8 @@ class Checker extends MiniPhase { // Checking.checkClassBody(tree) import semantic._ - val addr = Addr(cls, outer = Hot) - heap(addr) = Objekt(cls, Map.empty, Map(cls -> Hot)) - val res = addr.call(cls.primaryConstructor, superType = NoType, tree)(using ctx, Vector.empty) + val thisRef = ThisRef(cls)(fields = mutable.Map.empty) + val res = semantic.init(cls, thisRef)(using ctx, Vector.empty) res.errors.foreach(_.issue) } diff --git a/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala b/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala index 1ca27a5484a7..cc939140a8e1 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala @@ -27,9 +27,7 @@ class Semantic { /** Abstract values * - * Value = Hot | Cold | Addr | Fun | RefSet - * - * `Warm` and `This` will be addresses refer to the abstract heap + * Value = Hot | Cold | Warm | ThisRef | Fun | RefSet */ trait Value { def show: String = this.toString() @@ -41,47 +39,30 @@ class Semantic { /** An object with unknown initialization status */ case object Cold extends Value - /** Addresses to the abstract heap - * - * Addresses determine abstractions of objects. Objects created - * with same address are represented with the same abstraction. - * - * Nested addresses may lead to infinite domain, thus widen is - * needed to finitize addresses. E.g. OOPSLA 2020 paper restricts - * args to be either `Hot` or `Cold` - */ - case class Addr(klass: ClassSymbol, outer: Value) extends Value - - /** A function value */ - case class Fun(expr: Tree, thisV: Addr, klass: ClassSymbol) extends Value - - /** A value which represents a set of addresses + /** Object referred by `this` which stores abstract values for all fields * - * It comes from `if` expressions. + * Note: the mutable `fields` plays the role of heap. Thanks to monotonicity + * of the heap, we may handle it in a simple way. */ - case class RefSet(refs: List[Addr | Fun]) extends Value + case class ThisRef(klass: ClassSymbol)(val fields: mutable.Map[Symbol, Value]) extends Value { + def updateField(field: Symbol, value: Value): Unit = + fields(field) = value + } - /** Object stores abstract values for all fields and the outer. + /** An object with all fields initialized but reaches objects under initialization * - * Theoretically we only need to store the outer for the concrete class, - * as all other outers are determined. - * - * From performance reasons, we cache the immediate outer for all classes - * in the inheritance hierarchy. + * We need to restrict nesting levels of `outer` to finitize the domain. */ - case class Objekt(klass: ClassSymbol, fields: Map[Symbol, Value], outers: Map[ClassSymbol, Value]) + case class Warm(klass: ClassSymbol, outer: Value) extends Value - /** Abstract heap stores abstract objects - * - * As in the OOPSLA paper, the abstract heap is monotonistic - */ - type Heap = mutable.Map[Addr, Objekt] + /** A function value */ + case class Fun(expr: Tree, thisV: ThisRef | Warm, klass: ClassSymbol) extends Value - /** The heap for abstract objects + /** A value which represents a set of addresses * - * As the heap is monotonistic, we can avoid passing it around. + * It comes from `if` expressions. */ - val heap: Heap = mutable.Map.empty[Addr, Objekt] + case class RefSet(refs: List[Warm | Fun | ThisRef]) extends Value /** Interpreter configuration * @@ -154,10 +135,10 @@ class Semantic { case (Cold, _) => Cold case (_, Cold) => Cold - case (a: (Fun | Addr), b: (Fun | Addr)) => RefSet(a :: b :: Nil) + case (a: (Fun | Warm | ThisRef), b: (Fun | Warm | ThisRef)) => RefSet(a :: b :: Nil) - case (a: (Fun | Addr), RefSet(refs)) => RefSet(a :: refs) - case (RefSet(refs), b: (Fun | Addr)) => RefSet(b :: refs) + case (a: (Fun | Warm | ThisRef), RefSet(refs)) => RefSet(a :: refs) + case (RefSet(refs), b: (Fun | Warm | ThisRef)) => RefSet(b :: refs) case (RefSet(refs1), RefSet(refs2)) => RefSet(refs1 ++ refs2) @@ -165,28 +146,39 @@ class Semantic { def join: Value = values.reduce { (v1, v2) => v1.join(v2) } extension (value: Value) - def select(f: Symbol, source: Tree)(using Context, Trace): Result = + def select(field: Symbol, source: Tree)(using Context, Trace): Result = value match { case Hot => Result(Hot, noErrors) case Cold => - val error = AccessCold(f, source, trace) + val error = AccessCold(field, source, trace) Result(Hot, error :: Nil) - case addr: Addr => - val obj = heap(addr) - if obj.fields.contains(f) then - Result(obj.fields(f), Nil) + case thisRef: ThisRef => + val target = resolve(thisRef.klass, field) + if target.is(Flags.Lazy) then value.call(target, superType = NoType, source) + else if thisRef.fields.contains(target) then + Result(thisRef.fields(target), Nil) else - val error = AccessNonInit(f, trace.add(source)) + val error = AccessNonInit(target, trace.add(source)) + Result(Hot, error :: Nil) + + case warm: Warm => + val target = resolve(warm.klass, field) + if target.is(Flags.Lazy) then value.call(target, superType = NoType, source) + else if target.hasSource then + val rhs = target.defTree.asInstanceOf[ValDef].rhs + eval(rhs, warm, target.owner.asClass) + else + val error = CallUnknown(field, source, trace) Result(Hot, error :: Nil) case _: Fun => ??? case RefSet(refs) => - val resList = refs.map(_.select(f, source)) + val resList = refs.map(_.select(field, source)) val value2 = resList.map(_.value).join val errors = resList.flatMap(_.errors) Result(value2, errors) @@ -201,29 +193,48 @@ class Semantic { val error = CallCold(meth, source, trace) Result(Hot, error :: Nil) - case addr: Addr => - val obj = heap(addr) + case thisRef: ThisRef => val target = if superType.exists then // TODO: superType could be A & B when there is self-annotation - resolveSuper(obj.klass, superType.classSymbol.asClass, meth) + resolveSuper(thisRef.klass, superType.classSymbol.asClass, meth) else - resolve(obj.klass, meth) + resolve(thisRef.klass, meth) if target.isPrimaryConstructor then - init(target.owner.asClass, addr) + init(target.owner.asClass, thisRef) else if target.isOneOf(Flags.Method | Flags.Lazy) then if target.hasSource then val rhs = target.defTree.asInstanceOf[DefDef].rhs - eval(rhs, addr, target.owner.asClass) + eval(rhs, thisRef, target.owner.asClass) else val error = CallUnknown(target, source, trace) Result(Hot, error :: Nil) + else if thisRef.fields.contains(target) then + Result(thisRef.fields(target), Nil) else - if obj.fields.contains(target) then - Result(obj.fields(target), Nil) + val error = AccessNonInit(target, trace.add(source)) + Result(Hot, error :: Nil) + + case warm: Warm => + val target = + if superType.exists then + // TODO: superType could be A & B when there is self-annotation + resolveSuper(warm.klass, superType.classSymbol.asClass, meth) + else + resolve(warm.klass, meth) + if target.isOneOf(Flags.Method | Flags.Lazy) then + if target.hasSource then + val rhs = target.defTree.asInstanceOf[DefDef].rhs + eval(rhs, warm, target.owner.asClass) else - val error = AccessNonInit(target, trace.add(source)) + val error = CallUnknown(target, source, trace) Result(Hot, error :: Nil) + else if target.hasSource then + val rhs = target.defTree.asInstanceOf[ValDef].rhs + eval(rhs, warm, target.owner.asClass) + else + val error = CallUnknown(target, source, trace) + Result(Hot, error :: Nil) case Fun(body, thisV, klass) => if meth.name == nme.apply then eval(body, thisV, klass) @@ -246,13 +257,13 @@ class Semantic { val error = CallCold(ctor, source, trace) Result(Hot, error :: Nil) - case addr: Addr => + case thisRef: ThisRef => + Result(Warm(klass, outer = thisRef), noErrors) + + case warm: Warm => // widen the outer to finitize addresses - val outer = if addr.outer.isInstanceOf[Addr] then addr.copy(outer = Cold) else addr - val addr2 = Addr(klass, outer) - if !heap.contains(addr2) then - heap(addr2) = Objekt(klass, Map.empty, Map(klass -> outer)) - addr2.call(ctor, superType = NoType, source) + val outer = if warm.outer.isInstanceOf[Warm] then warm.copy(outer = Cold) else warm + Result(Warm(klass, outer), noErrors) case Fun(body, thisV, klass) => ??? // impossible @@ -265,19 +276,6 @@ class Semantic { } end extension - extension (addr: Addr) - def updateOuter(klass: ClassSymbol, value: Value): Unit = - val obj = heap(addr) - val obj2 = obj.copy(outers = obj.outers.updated(klass, value)) - heap(addr) = obj2 - - def updateField(field: Symbol, value: Value): Unit = - val obj = heap(addr) - val obj2 = obj.copy(fields = obj.fields.updated(field, value)) - heap(addr) = obj2 - end extension - - // ----- Semantic definition -------------------------------- /** Evaluate an expression with the given value for `this` in a given class `klass` @@ -388,8 +386,8 @@ class Semantic { case closureDef(ddef) => thisV match - case addr: Addr => - val value = Fun(ddef.rhs, addr, klass) + case obj: (ThisRef | Warm) => + val value = Fun(ddef.rhs, obj, klass) Result(value, Nil) case _ => ??? // impossible @@ -485,12 +483,7 @@ class Semantic { case tp @ ThisType(tref) => if tref.symbol.is(Flags.Package) then Result(Hot, noErrors) else - val value = - thisV match - case Hot => Hot - case addr: Addr => - resolveThis(tp.classSymbol.asClass, addr, klass) - case _ => ??? + val value = resolveThis(tp.classSymbol.asClass, thisV, klass) Result(value, noErrors) case _: TermParamRef | _: RecThis => @@ -507,18 +500,26 @@ class Semantic { if target == klass then thisV else thisV match - case Hot => Hot - case thisV: Addr => - val outer = heap(thisV).outers.getOrElse(klass, Hot) - val outerCls = klass.owner.enclosingClass.asClass - resolveThis(target, outer, outerCls) + case Hot | _: ThisRef => Hot + case warm: Warm => + // use existing type information as a shortcut + val tref = typeRefOf(warm.klass.typeRef.baseType(klass)) + if tref.prefix == NoPrefix then + // Current class is local, in the enclosing scope of `warm.klass` + val outerCls = warm.klass.owner.enclosingClass.asClass + resolveThis(target, warm.outer, outerCls) + else + val outerCls = klass.owner.enclosingClass.asClass + val res = cases(tref.prefix, warm.outer, warm.klass.owner.asClass, EmptyTree) + assert(res.errors.isEmpty, "unexpected error " + res) + resolveThis(target, res.value, outerCls) case _ => ??? } /** Compute the outer value that correspond to `tref.prefix` */ def outerValue(tref: TypeRef, thisV: Value, klass: ClassSymbol, source: Tree)(using Context, Trace): Result = val cls = tref.classSymbol.asClass - if (tref.prefix == NoPrefix) then + if tref.prefix == NoPrefix then val enclosing = cls.owner.lexicallyEnclosingClass.asClass val outerV = resolveThis(enclosing, thisV, klass) Result(outerV, noErrors) @@ -526,27 +527,23 @@ class Semantic { cases(tref.prefix, thisV, klass, source) /** Initialize part of an abstract object in `klass` of the inheritance chain */ - def init(klass: ClassSymbol, thisV: Addr)(using Context, Trace): Result = log("init " + klass.show, printer, res => res.asInstanceOf[Result].show) { + def init(klass: ClassSymbol, thisV: ThisRef)(using Context, Trace): Result = log("init " + klass.show, printer, res => res.asInstanceOf[Result].show) { val errorBuffer = new mutable.ArrayBuffer[Error] val tpl = klass.defTree.asInstanceOf[TypeDef].rhs.asInstanceOf[Template] // init param fields - var obj = heap(thisV) klass.paramAccessors.foreach { acc => if (!acc.is(Flags.Method)) { traceIndented(acc.show + " initialized", printer) - obj = obj.copy(fields = obj.fields.updated(acc, Hot)) + thisV.updateField(acc, Hot) } } - heap(thisV) = obj def superCall(tref: TypeRef, ctor: Symbol, source: Tree): Unit = - // update outer for super class val cls = tref.classSymbol.asClass - val res = outerValue(tref, thisV, klass, source) - errorBuffer ++= res.errors - thisV.updateOuter(cls, res.value) + // update outer for super class + // ignored as they are all hot // follow constructor if !cls.defTree.isEmpty then From 252f3e40184968feb9ca1246c8da9790543b0021 Mon Sep 17 00:00:00 2001 From: Liu Fengyun Date: Sat, 15 May 2021 01:37:43 +0200 Subject: [PATCH 21/50] Remove useless file --- .../tools/dotc/transform/init/Domain.scala | 42 ------------------- 1 file changed, 42 deletions(-) delete mode 100644 compiler/src/dotty/tools/dotc/transform/init/Domain.scala diff --git a/compiler/src/dotty/tools/dotc/transform/init/Domain.scala b/compiler/src/dotty/tools/dotc/transform/init/Domain.scala deleted file mode 100644 index 25859b0e5cc6..000000000000 --- a/compiler/src/dotty/tools/dotc/transform/init/Domain.scala +++ /dev/null @@ -1,42 +0,0 @@ -package dotty.tools.dotc -package transform -package init - -import core._ -import Symbols._ - -import ast.tpd._ -import util.SourcePosition - -trait Domain { - /** Abstract values */ - type Value - - /** The bottom value */ - val bottom: Value - - /** Addresses to the abstract heap */ - type Addr - - /** Abstract object in the heap */ - type Objekt - - /** Abstract heap stores abstract objects */ - type Heap = Map[Addr, Objekt] - - /** Interpreter configuration - * - * The (abstract) interpreter can be seen as a push-down automaton that - * transits between the configurations where the stack is the implicit call - * stack of the meta-language. - * - * It's important that the configuration is finite for the analysis to - * terminate. - * - * For soundness, we need to compute fixed point of the cache, which maps - * configuration to evaluation result. - * - */ - type Config - def makeConfig(expr: Tree, thisV: Value, heap: Heap): Config -} From fff6f481343277c81cbee5188f696ece07da47e2 Mon Sep 17 00:00:00 2001 From: Liu Fengyun Date: Sat, 15 May 2021 09:35:13 +0200 Subject: [PATCH 22/50] Only cache top-level expressions --- .../tools/dotc/transform/init/Semantic.scala | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala b/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala index cc939140a8e1..8f3a797d7e26 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala @@ -169,7 +169,7 @@ class Semantic { if target.is(Flags.Lazy) then value.call(target, superType = NoType, source) else if target.hasSource then val rhs = target.defTree.asInstanceOf[ValDef].rhs - eval(rhs, warm, target.owner.asClass) + eval(rhs, warm, target.owner.asClass, cacheResult = true) else val error = CallUnknown(field, source, trace) Result(Hot, error :: Nil) @@ -205,7 +205,7 @@ class Semantic { else if target.isOneOf(Flags.Method | Flags.Lazy) then if target.hasSource then val rhs = target.defTree.asInstanceOf[DefDef].rhs - eval(rhs, thisRef, target.owner.asClass) + eval(rhs, thisRef, target.owner.asClass, cacheResult = true) else val error = CallUnknown(target, source, trace) Result(Hot, error :: Nil) @@ -225,19 +225,19 @@ class Semantic { if target.isOneOf(Flags.Method | Flags.Lazy) then if target.hasSource then val rhs = target.defTree.asInstanceOf[DefDef].rhs - eval(rhs, warm, target.owner.asClass) + eval(rhs, warm, target.owner.asClass, cacheResult = true) else val error = CallUnknown(target, source, trace) Result(Hot, error :: Nil) else if target.hasSource then val rhs = target.defTree.asInstanceOf[ValDef].rhs - eval(rhs, warm, target.owner.asClass) + eval(rhs, warm, target.owner.asClass, cacheResult = true) else val error = CallUnknown(target, source, trace) Result(Hot, error :: Nil) case Fun(body, thisV, klass) => - if meth.name == nme.apply then eval(body, thisV, klass) + if meth.name == nme.apply then eval(body, thisV, klass, cacheResult = true) else if meth.name.toString == "tupled" then Result(value, Nil) else Result(Hot, Nil) // TODO: refine @@ -282,7 +282,7 @@ class Semantic { * * This method only handles cache logic and delegates the work to `cases`. */ - def eval(expr: Tree, thisV: Value, klass: ClassSymbol)(using Context, Trace): Result = log("evaluating " + expr.show, printer, res => res.asInstanceOf[Result].show) { + def eval(expr: Tree, thisV: Value, klass: ClassSymbol, cacheResult: Boolean = false)(using Context, Trace): Result = log("evaluating " + expr.show, printer, res => res.asInstanceOf[Result].show) { val cfg = Config(thisV, expr.sourcePos) if (cache.contains(cfg)) Result(cache(cfg), noErrors) else { @@ -290,9 +290,9 @@ class Semantic { // 1. the result is decided by `cfg` for a legal program // (heap change is irrelevant thanks to monotonicity) // 2. errors will have been reported for an illegal program - cache(cfg) = Hot + if cacheResult then cache(cfg) = Hot val res = cases(expr, thisV, klass) - cache(cfg) = res.value + if cacheResult then cache(cfg) = res.value res } } @@ -355,7 +355,7 @@ class Semantic { case Hot => Result(Hot, errors) case _ => val rhs = id.symbol.defTree.asInstanceOf[DefDef].rhs - eval(rhs, thisValue2, enclosingClass)(using ctx, trace.add(expr)) + eval(rhs, thisValue2, enclosingClass, cacheResult = true)(using ctx, trace.add(expr)) case TermRef(prefix, _) => val res = cases(prefix, thisV, klass, id) ++ errors res.call(id.symbol, superType = NoType, source = expr)(using ctx, trace.add(expr)) From 44031b844000cb5a0ec254aa09e735287080223b Mon Sep 17 00:00:00 2001 From: Liu Fengyun Date: Sat, 15 May 2021 21:31:45 +0200 Subject: [PATCH 23/50] Handle more tree forms in parents --- compiler/src/dotty/tools/dotc/transform/init/Semantic.scala | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala b/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala index 8f3a797d7e26..030168b94e61 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala @@ -569,9 +569,9 @@ class Semantic { } superCall(tref, ctor, tree) - case ref: RefTree => - val tref = ref.tpe.asInstanceOf[TypeRef] - superCall(tref, tref.classSymbol.primaryConstructor, ref) + case _ => + val tref = typeRefOf(parent.tpe) + superCall(tref, tref.classSymbol.primaryConstructor, parent) } // see spec 5.1 about "Template Evaluation". From 962fb94bbe736f90a402c008a874a3e17c09af4f Mon Sep 17 00:00:00 2001 From: Liu Fengyun Date: Sat, 15 May 2021 21:37:32 +0200 Subject: [PATCH 24/50] Simplify Warm.call and Warm.select --- .../tools/dotc/transform/init/Semantic.scala | 18 +++++------------- 1 file changed, 5 insertions(+), 13 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala b/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala index 030168b94e61..7e35501c3026 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala @@ -166,9 +166,8 @@ class Semantic { case warm: Warm => val target = resolve(warm.klass, field) - if target.is(Flags.Lazy) then value.call(target, superType = NoType, source) - else if target.hasSource then - val rhs = target.defTree.asInstanceOf[ValDef].rhs + if target.hasSource then + val rhs = target.defTree.asInstanceOf[ValOrDefDef].rhs eval(rhs, warm, target.owner.asClass, cacheResult = true) else val error = CallUnknown(field, source, trace) @@ -204,7 +203,7 @@ class Semantic { init(target.owner.asClass, thisRef) else if target.isOneOf(Flags.Method | Flags.Lazy) then if target.hasSource then - val rhs = target.defTree.asInstanceOf[DefDef].rhs + val rhs = target.defTree.asInstanceOf[ValOrDefDef].rhs eval(rhs, thisRef, target.owner.asClass, cacheResult = true) else val error = CallUnknown(target, source, trace) @@ -222,15 +221,8 @@ class Semantic { resolveSuper(warm.klass, superType.classSymbol.asClass, meth) else resolve(warm.klass, meth) - if target.isOneOf(Flags.Method | Flags.Lazy) then - if target.hasSource then - val rhs = target.defTree.asInstanceOf[DefDef].rhs - eval(rhs, warm, target.owner.asClass, cacheResult = true) - else - val error = CallUnknown(target, source, trace) - Result(Hot, error :: Nil) - else if target.hasSource then - val rhs = target.defTree.asInstanceOf[ValDef].rhs + if target.hasSource then + val rhs = target.defTree.asInstanceOf[ValOrDefDef].rhs eval(rhs, warm, target.owner.asClass, cacheResult = true) else val error = CallUnknown(target, source, trace) From a6f27dfc8bdf0b55c601132bcd868a5c25fbd1d3 Mon Sep 17 00:00:00 2001 From: Liu Fengyun Date: Sat, 15 May 2021 22:14:01 +0200 Subject: [PATCH 25/50] Fix crash --- compiler/src/dotty/tools/dotc/transform/init/Semantic.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala b/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala index 7e35501c3026..bbca8c41d0f8 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala @@ -475,7 +475,7 @@ class Semantic { case tp @ ThisType(tref) => if tref.symbol.is(Flags.Package) then Result(Hot, noErrors) else - val value = resolveThis(tp.classSymbol.asClass, thisV, klass) + val value = resolveThis(tref.classSymbol.asClass, thisV, klass) Result(value, noErrors) case _: TermParamRef | _: RecThis => From 3d6c79a035a25ac5d3c29c5afa7fc6a5cda80d94 Mon Sep 17 00:00:00 2001 From: Liu Fengyun Date: Sat, 15 May 2021 22:16:19 +0200 Subject: [PATCH 26/50] Respect lazy fields --- compiler/src/dotty/tools/dotc/transform/init/Semantic.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala b/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala index bbca8c41d0f8..5d0f80bc4ade 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 @@ class Semantic { // class body tpl.body.foreach { - case vdef : ValDef => + case vdef : ValDef if !vdef.symbol.is(Flags.Lazy) => val res = eval(vdef.rhs, thisV, klass) errorBuffer ++= res.errors thisV.updateField(vdef.symbol, res.value) From 683a4a14658183cc44a6307ea2134b8d2e416e33 Mon Sep 17 00:00:00 2001 From: Liu Fengyun Date: Sat, 15 May 2021 22:22:01 +0200 Subject: [PATCH 27/50] Handle access param of warm objects --- compiler/src/dotty/tools/dotc/transform/init/Semantic.scala | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala b/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala index 5d0f80bc4ade..bfd593009757 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala @@ -221,7 +221,9 @@ class Semantic { resolveSuper(warm.klass, superType.classSymbol.asClass, meth) else resolve(warm.klass, meth) - if target.hasSource then + if target.is(Flags.Param) then + Result(Hot, Nil) + else if target.hasSource then val rhs = target.defTree.asInstanceOf[ValOrDefDef].rhs eval(rhs, warm, target.owner.asClass, cacheResult = true) else From 5a3e3d13f118cc55f4216c8121de9d35f8146953 Mon Sep 17 00:00:00 2001 From: Liu Fengyun Date: Sat, 15 May 2021 22:40:57 +0200 Subject: [PATCH 28/50] Handle parameterless method calls --- compiler/src/dotty/tools/dotc/transform/init/Semantic.scala | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala b/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala index bfd593009757..548470c9875d 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala @@ -305,7 +305,7 @@ class Semantic { // TODO: disallow `var x: T = _` Result(Hot, noErrors) - case Ident(name) => + case id @ Ident(name) if !id.symbol.is(Flags.Method) => assert(name.isTermName, "type trees should not reach here") cases(expr.tpe, thisV, klass, expr) @@ -599,7 +599,7 @@ class Semantic { case _: MemberDef => case tree => - eval(tree, thisV, klass) + errorBuffer ++= eval(tree, thisV, klass).errors } Result(thisV, errorBuffer.toList) From 3fccd639d66f003e8902ae2ca9d5055ce483cc14 Mon Sep 17 00:00:00 2001 From: Liu Fengyun Date: Sat, 15 May 2021 23:46:08 +0200 Subject: [PATCH 29/50] Implement promotion --- .../tools/dotc/transform/init/Checking.scala | 6 +- .../tools/dotc/transform/init/Errors.scala | 8 +-- .../tools/dotc/transform/init/Semantic.scala | 71 +++++++++++++++++-- tests/init/neg/function1.scala | 2 +- 4 files changed, 72 insertions(+), 15 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/transform/init/Checking.scala b/compiler/src/dotty/tools/dotc/transform/init/Checking.scala index 1e4090f841ed..1b3eaacde8ee 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Checking.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Checking.scala @@ -342,7 +342,7 @@ object Checking { for (eff <- buffer.toList) { val errs = check(eff) if !errs.isEmpty then - return UnsafePromotion(warm, eff.source, state.path, errs.toList).toErrors + return UnsafePromotion(eff.source, state.path, errs.toList).toErrors } Errors.empty @@ -355,7 +355,7 @@ object Checking { Errors.empty else pot match { case pot: ThisRef => - PromoteThis(pot, eff.source, state.path).toErrors + PromoteThis(eff.source, state.path).toErrors case _: Cold => PromoteCold(eff.source, state.path).toErrors @@ -374,7 +374,7 @@ object Checking { } if (errs1.nonEmpty || errs2.nonEmpty) - UnsafePromotion(pot, eff.source, state.path, errs1 ++ errs2).toErrors + UnsafePromotion(eff.source, state.path, errs1 ++ errs2).toErrors else Errors.empty diff --git a/compiler/src/dotty/tools/dotc/transform/init/Errors.scala b/compiler/src/dotty/tools/dotc/transform/init/Errors.scala index 73b8cd123033..2be9fac3c2d4 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Errors.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Errors.scala @@ -12,7 +12,7 @@ import Types._, Symbols._, Contexts._ import Effects._, Potentials._ object Errors { - type Errors = List[Error] + type Errors = Seq[Error] val empty: Errors = Nil def show(errs: Errors)(using Context): String = @@ -70,12 +70,12 @@ object Errors { } /** Promote `this` under initialization to fully-initialized */ - case class PromoteThis(pot: ThisRef, source: Tree, trace: Vector[Tree]) extends Error { + case class PromoteThis(source: Tree, trace: Vector[Tree]) extends Error { def show(using Context): String = "Promote the value under initialization to fully-initialized." } /** Promote `this` under initialization to fully-initialized */ - case class PromoteWarm(pot: Warm, source: Tree, trace: Vector[Tree]) extends Error { + case class PromoteWarm(source: Tree, trace: Vector[Tree]) extends Error { def show(using Context): String = "Promoting the value under initialization to fully-initialized." } @@ -102,7 +102,7 @@ object Errors { } /** Promote a value under initialization to fully-initialized */ - case class UnsafePromotion(pot: Potential, source: Tree, trace: Vector[Tree], errors: Errors) extends Error { + case class UnsafePromotion(source: Tree, trace: Vector[Tree], errors: Errors) extends Error { assert(errors.nonEmpty) override def issue(using Context): Unit = diff --git a/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala b/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala index 548470c9875d..125c2539bf54 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala @@ -45,8 +45,7 @@ class Semantic { * of the heap, we may handle it in a simple way. */ case class ThisRef(klass: ClassSymbol)(val fields: mutable.Map[Symbol, Value]) extends Value { - def updateField(field: Symbol, value: Value): Unit = - fields(field) = value + var allFieldsInitialized: Boolean = false } /** An object with all fields initialized but reaches objects under initialization @@ -99,11 +98,8 @@ class Semantic { def +(error: Error): Result = this.copy(errors = this.errors :+ error) - def ensureHot(msg: String, source: Tree)(using Trace): Result = - if value == Hot then this - else - // TODO: define a new error - this + PromoteCold(source, trace) + def ensureHot(msg: String, source: Tree)(using Context, Trace): Result = + this ++ value.promote(msg, source) def select(f: Symbol, source: Tree)(using Context, Trace): Result = value.select(f, source) ++ errors @@ -270,6 +266,67 @@ class Semantic { } end extension + extension (value: Value) + def canDirectlyPromote(using Context): Boolean = + value match + case Hot => true + case Cold => false + + case warm: Warm => + warm.outer.canDirectlyPromote + + case thisRef: ThisRef => + thisRef.allFieldsInitialized || { + // If we have all fields initialized, then we can promote This to hot. + thisRef.allFieldsInitialized = thisRef.klass.appliedRef.fields.forall { denot => + val sym = denot.symbol + sym.isOneOf(Flags.Lazy | Flags.Deferred) || thisRef.fields.contains(sym) + } + thisRef.allFieldsInitialized + } + + case fun: Fun => false + + case RefSet(refs) => + refs.forall(_.canDirectlyPromote) + + end canDirectlyPromote + + /** Promotion of values to hot */ + def promote(msg: String, source: Tree)(using Context, Trace): List[Error] = + value match + case Hot => Nil + + case Cold => PromoteCold(source, trace) :: Nil + + case thisRef: ThisRef => + if thisRef.canDirectlyPromote then Nil + else PromoteThis(source, trace) :: Nil + + case warm: Warm => + if warm.outer.canDirectlyPromote then Nil + else PromoteWarm(source, trace) :: Nil + + case Fun(body, thisV, klass) => + val res = eval(body, thisV, klass) + val errors2 = res.value.promote(msg, source) + if (res.errors.nonEmpty || errors2.nonEmpty) + UnsafePromotion(source, trace, res.errors ++ errors2) :: Nil + else + Nil + + case RefSet(refs) => + refs.flatMap(_.promote(msg, source)) + end extension + + extension (ref: ThisRef | Warm) + def updateField(field: Symbol, value: Value): Unit = + ref match + case thisRef: ThisRef => thisRef.fields(field) = value + case warm: Warm => // ignore + end extension + + // ----- Semantic definition -------------------------------- /** Evaluate an expression with the given value for `this` in a given class `klass` diff --git a/tests/init/neg/function1.scala b/tests/init/neg/function1.scala index e01864ae1f47..15427f3de750 100644 --- a/tests/init/neg/function1.scala +++ b/tests/init/neg/function1.scala @@ -4,7 +4,7 @@ class Foo { val fun2: Int => Int = n => 1 + n + list.size fun2(5) - List(5, 9).map(n => 2 + n + list.size) + List(5, 9).map(n => 2 + n + list.size) // error final val list = List(1, 2, 3) // error From 7717c5b021695f760b918256c96084bc010c0b8e Mon Sep 17 00:00:00 2001 From: Liu Fengyun Date: Sun, 16 May 2021 01:12:28 +0200 Subject: [PATCH 30/50] Handle constructor call effects on warm objects --- .../tools/dotc/transform/init/Checker.scala | 3 +- .../tools/dotc/transform/init/Semantic.scala | 41 +++++++++++++------ tests/init/neg/hybrid2.scala | 4 +- tests/init/neg/inner-loop.scala | 2 +- 4 files changed, 33 insertions(+), 17 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/transform/init/Checker.scala b/compiler/src/dotty/tools/dotc/transform/init/Checker.scala index 280bd2bf65c4..593597e39b44 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Checker.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Checker.scala @@ -62,8 +62,9 @@ class Checker extends MiniPhase { // Checking.checkClassBody(tree) import semantic._ + val tpl = tree.rhs.asInstanceOf[Template] val thisRef = ThisRef(cls)(fields = mutable.Map.empty) - val res = semantic.init(cls, thisRef)(using ctx, Vector.empty) + val res = eval(tpl, thisRef, cls)(using ctx, Vector.empty) res.errors.foreach(_.issue) } diff --git a/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala b/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala index 125c2539bf54..6b8d19761a1a 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala @@ -195,12 +195,15 @@ class Semantic { resolveSuper(thisRef.klass, superType.classSymbol.asClass, meth) else resolve(thisRef.klass, meth) - if target.isPrimaryConstructor then - init(target.owner.asClass, thisRef) - else if target.isOneOf(Flags.Method | Flags.Lazy) then + if target.isOneOf(Flags.Method | Flags.Lazy) then if target.hasSource then - val rhs = target.defTree.asInstanceOf[ValOrDefDef].rhs - eval(rhs, thisRef, target.owner.asClass, cacheResult = true) + if target.isPrimaryConstructor then + val cls = target.owner.asClass + val tpl = cls.defTree.asInstanceOf[TypeDef].rhs.asInstanceOf[Template] + eval(tpl, thisRef, cls, cacheResult = true) + else + val rhs = target.defTree.asInstanceOf[ValOrDefDef].rhs + eval(rhs, thisRef, target.owner.asClass, cacheResult = true) else val error = CallUnknown(target, source, trace) Result(Hot, error :: Nil) @@ -220,8 +223,13 @@ class Semantic { if target.is(Flags.Param) then Result(Hot, Nil) else if target.hasSource then - val rhs = target.defTree.asInstanceOf[ValOrDefDef].rhs - eval(rhs, warm, target.owner.asClass, cacheResult = true) + if target.isPrimaryConstructor then + val cls = target.owner.asClass + val tpl = cls.defTree.asInstanceOf[TypeDef].rhs.asInstanceOf[Template] + eval(tpl, warm, cls, cacheResult = true) + else + val rhs = target.defTree.asInstanceOf[ValOrDefDef].rhs + eval(rhs, warm, target.owner.asClass, cacheResult = true) else val error = CallUnknown(target, source, trace) Result(Hot, error :: Nil) @@ -248,12 +256,16 @@ class Semantic { Result(Hot, error :: Nil) case thisRef: ThisRef => - Result(Warm(klass, outer = thisRef), noErrors) + val value = Warm(klass, outer = thisRef) + val res = value.call(ctor, superType = NoType, source) + Result(value, res.errors) case warm: Warm => // widen the outer to finitize addresses val outer = if warm.outer.isInstanceOf[Warm] then warm.copy(outer = Cold) else warm - Result(Warm(klass, outer), noErrors) + val value = Warm(klass, outer) + val res = value.call(ctor, superType = NoType, source) + Result(value, res.errors) case Fun(body, thisV, klass) => ??? // impossible @@ -333,7 +345,7 @@ class Semantic { * * This method only handles cache logic and delegates the work to `cases`. */ - def eval(expr: Tree, thisV: Value, klass: ClassSymbol, cacheResult: Boolean = false)(using Context, Trace): Result = log("evaluating " + expr.show, printer, res => res.asInstanceOf[Result].show) { + def eval(expr: Tree, thisV: Value, klass: ClassSymbol, cacheResult: Boolean = false)(using Context, Trace): Result = log("evaluating " + expr.show + ", this = " + thisV.show, printer, res => res.asInstanceOf[Result].show) { val cfg = Config(thisV, expr.sourcePos) if (cache.contains(cfg)) Result(cache(cfg), noErrors) else { @@ -512,6 +524,11 @@ class Semantic { // local type definition Result(Hot, noErrors) + case tpl: Template => + thisV match + case value: (ThisRef | Warm) => init(tpl, value, klass) + case _ => ??? // impossible + case _: Import | _: Export => Result(Hot, noErrors) @@ -578,11 +595,9 @@ class Semantic { cases(tref.prefix, thisV, klass, source) /** Initialize part of an abstract object in `klass` of the inheritance chain */ - def init(klass: ClassSymbol, thisV: ThisRef)(using Context, Trace): Result = log("init " + klass.show, printer, res => res.asInstanceOf[Result].show) { + def init(tpl: Template, thisV: ThisRef | Warm, klass: ClassSymbol)(using Context, Trace): Result = log("init " + klass.show, printer, res => res.asInstanceOf[Result].show) { val errorBuffer = new mutable.ArrayBuffer[Error] - val tpl = klass.defTree.asInstanceOf[TypeDef].rhs.asInstanceOf[Template] - // init param fields klass.paramAccessors.foreach { acc => if (!acc.is(Flags.Method)) { diff --git a/tests/init/neg/hybrid2.scala b/tests/init/neg/hybrid2.scala index d5c8b037a324..a9f8246fd58d 100644 --- a/tests/init/neg/hybrid2.scala +++ b/tests/init/neg/hybrid2.scala @@ -13,7 +13,7 @@ class Y { } val x = new X - x.b.g // error + x.b.g - val n = 10 + val n = 10 // error } diff --git a/tests/init/neg/inner-loop.scala b/tests/init/neg/inner-loop.scala index c56f31a96757..a7ff5c153d32 100644 --- a/tests/init/neg/inner-loop.scala +++ b/tests/init/neg/inner-loop.scala @@ -1,6 +1,6 @@ class Outer { outer => class Inner extends Outer { - val x = 5 + outer.n // error + val x = 5 + outer.n } val inner = new Inner val n = 6 // error From d3383f63b733710b741cb31357949c941ca3c6aa Mon Sep 17 00:00:00 2001 From: Liu Fengyun Date: Sun, 16 May 2021 01:28:55 +0200 Subject: [PATCH 31/50] Refactor cache: don't use location as cache key --- .../tools/dotc/transform/init/Semantic.scala | 26 +++++++++++-------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala b/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala index 6b8d19761a1a..77390fb73085 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala @@ -9,7 +9,7 @@ import Types._ import StdNames._ import ast.tpd._ -import util.SourcePosition +import util.EqHashMap import config.Printers.init as printer import reporting.trace as log @@ -22,9 +22,6 @@ class Semantic { // ----- Domain definitions -------------------------------- - /** Locations are finite for any givn program */ - type Loc = SourcePosition - /** Abstract values * * Value = Hot | Cold | Warm | ThisRef | Fun | RefSet @@ -79,16 +76,23 @@ class Semantic { * Which also avoid computing fix-point on the cache, as the cache is * immutable. */ - case class Config(thisV: Value, loc: Loc) + case class Config(thisV: Value, expr: Tree) /** Cache used to terminate the analysis * * A finitary configuration is not enough for the analysis to * terminate. We need to use cache to let the interpreter "know" * that it can terminate. + * + * For performance reasons we use curried key. + * + * Note: It's tempting to use location of trees as key. That should + * be avoided as a template may have the same location as its single + * statement body. Macros may also create incorrect locations. + * */ - type Cache = mutable.Map[Config, Value] - val cache: Cache = mutable.Map.empty[Config, Value] + type Cache = mutable.Map[Value, EqHashMap[Tree, Value]] + val cache: Cache = mutable.Map.empty[Value, EqHashMap[Tree, Value]] /** Result of abstract interpretation */ case class Result(value: Value, errors: Seq[Error]) { @@ -346,16 +350,16 @@ class Semantic { * This method only handles cache logic and delegates the work to `cases`. */ def eval(expr: Tree, thisV: Value, klass: ClassSymbol, cacheResult: Boolean = false)(using Context, Trace): Result = log("evaluating " + expr.show + ", this = " + thisV.show, printer, res => res.asInstanceOf[Result].show) { - val cfg = Config(thisV, expr.sourcePos) - if (cache.contains(cfg)) Result(cache(cfg), noErrors) + val innerMap = cache.getOrElseUpdate(thisV, new EqHashMap[Tree, Value]) + if (innerMap.contains(expr)) Result(innerMap(expr), noErrors) else { // no need to compute fix-point, because // 1. the result is decided by `cfg` for a legal program // (heap change is irrelevant thanks to monotonicity) // 2. errors will have been reported for an illegal program - if cacheResult then cache(cfg) = Hot + innerMap(expr) = Hot val res = cases(expr, thisV, klass) - if cacheResult then cache(cfg) = res.value + if cacheResult then innerMap(expr) = res.value else innerMap.remove(expr) res } } From 9831b9793e1e1377a5c02c453dc65b14de574c45 Mon Sep 17 00:00:00 2001 From: Liu Fengyun Date: Sun, 16 May 2021 15:30:55 +0200 Subject: [PATCH 32/50] Add test --- tests/init/neg/function-loop.scala | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 tests/init/neg/function-loop.scala diff --git a/tests/init/neg/function-loop.scala b/tests/init/neg/function-loop.scala new file mode 100644 index 000000000000..12048860c3a6 --- /dev/null +++ b/tests/init/neg/function-loop.scala @@ -0,0 +1,6 @@ +class Foo { + val f: Int => Foo = (x: Int) => if x > 0 then f(x) else this + f(10).n + + val n = 10 // error +} \ No newline at end of file From 366c97264aff53e1173a1273d1a8092a49ed266b Mon Sep 17 00:00:00 2001 From: Liu Fengyun Date: Sun, 16 May 2021 15:42:44 +0200 Subject: [PATCH 33/50] Check term usage in types for type soundness --- .../tools/dotc/transform/init/Semantic.scala | 22 +++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala b/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala index 77390fb73085..54abee9d2111 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala @@ -438,7 +438,7 @@ class Semantic { case Typed(expr, tpt) => if (tpt.tpe.hasAnnotation(defn.UncheckedAnnot)) Result(Hot, noErrors) - else eval(expr, thisV, klass) + else eval(expr, thisV, klass) ++ checkTermUsage(tpt, thisV, klass) case NamedArg(name, arg) => eval(arg, thisV, klass) @@ -526,7 +526,8 @@ class Semantic { case tdef: TypeDef => // local type definition - Result(Hot, noErrors) + if tdef.isClassDef then Result(Hot, noErrors) + else Result(Hot, checkTermUsage(tdef.rhs, thisV, klass)) case tpl: Template => thisV match @@ -681,6 +682,23 @@ class Semantic { Result(thisV, errorBuffer.toList) } + /** Check usage of terms inside types + * + * This is intended to avoid type soundness issues in Dotty. + */ + def checkTermUsage(tpt: Tree, thisV: Value, klass: ClassSymbol)(using Context, Trace): List[Error] = + val buf = new mutable.ArrayBuffer[Error] + val traverser = new TypeTraverser { + def traverse(tp: Type): Unit = tp match { + case TermRef(_: SingletonType, _) => + buf ++= cases(tp, thisV, klass, tpt).errors + case _ => + traverseChildren(tp) + } + } + traverser.traverse(tpt.tpe) + buf.toList + // ----- Utility methods and extractors -------------------------------- def typeRefOf(tp: Type)(using Context): TypeRef = tp.dealias.typeConstructor match { From 9960e5a2e8319d2e4747fb5ee6594b251b1bbf49 Mon Sep 17 00:00:00 2001 From: Liu Fengyun Date: Sun, 16 May 2021 16:48:09 +0200 Subject: [PATCH 34/50] Support early promotion of warm objects --- .../tools/dotc/transform/init/Checking.scala | 1 - .../tools/dotc/transform/init/Semantic.scala | 46 ++++++++++++++++++- 2 files changed, 44 insertions(+), 3 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/transform/init/Checking.scala b/compiler/src/dotty/tools/dotc/transform/init/Checking.scala index 1b3eaacde8ee..1695b0c069ca 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Checking.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Checking.scala @@ -321,7 +321,6 @@ object Checking { val f = denot.symbol if !f.isOneOf(excludedFlags) && f.hasSource then buffer += Promote(FieldReturn(warm, f)(source))(source) - buffer += FieldAccess(warm, f)(source) } classRef.membersBasedOnFlags(Flags.Method, Flags.Deferred).foreach { denot => diff --git a/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala b/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala index 54abee9d2111..40826c327e33 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala @@ -321,7 +321,7 @@ class Semantic { case warm: Warm => if warm.outer.canDirectlyPromote then Nil - else PromoteWarm(source, trace) :: Nil + else warm.tryPromote(msg, source) case Fun(body, thisV, klass) => val res = eval(body, thisV, klass) @@ -335,6 +335,48 @@ class Semantic { refs.flatMap(_.promote(msg, source)) end extension + extension (warm: Warm) + /** Try early promotion of warm objects + * + * Promotion is expensive and should only be performed for small classes. + * + * 1. for each concrete method `m` of the warm object: + * call the method and promote the result + * + * 2. for each concrete field `f` of the warm object: + * promote the field value + * + */ + def tryPromote(msg: String, source: Tree)(using Context, Trace): List[Error] = + val classRef = warm.klass.appliedRef + if classRef.memberClasses.nonEmpty then + return PromoteWarm(source, trace) :: Nil + + val fields = classRef.fields + val methods = classRef.membersBasedOnFlags(Flags.Method, Flags.Deferred | Flags.Accessor) + val buffer = new mutable.ArrayBuffer[Error] + + fields.exists { denot => + val f = denot.symbol + if !f.isOneOf(Flags.Deferred | Flags.Private | Flags.Protected) && f.hasSource then + val res = warm.select(f, source) + buffer ++= res.ensureHot(msg, source).errors + buffer.nonEmpty + } + + buffer.nonEmpty || methods.exists { denot => + val m = denot.symbol + if !m.isConstructor && m.hasSource then + val res = warm.call(m, superType = m.owner.typeRef, source = source) + buffer ++= res.ensureHot(msg, source).errors + buffer.nonEmpty + } + + if buffer.isEmpty then Nil + else UnsafePromotion(source, trace, buffer.toList) :: Nil + + end extension + extension (ref: ThisRef | Warm) def updateField(field: Symbol, value: Value): Unit = ref match @@ -669,7 +711,7 @@ class Semantic { // class body tpl.body.foreach { case vdef : ValDef if !vdef.symbol.is(Flags.Lazy) => - val res = eval(vdef.rhs, thisV, klass) + val res = eval(vdef.rhs, thisV, klass, cacheResult = true) errorBuffer ++= res.errors thisV.updateField(vdef.symbol, res.value) From 89a0845b6f9b480906e838c9df71c4eeecc35d8b Mon Sep 17 00:00:00 2001 From: Liu Fengyun Date: Sun, 16 May 2021 17:28:57 +0200 Subject: [PATCH 35/50] Handle by-name arguments --- .../tools/dotc/transform/init/Semantic.scala | 70 ++++++++++++------- tests/init/neg/t3273.check | 4 -- 2 files changed, 46 insertions(+), 28 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala b/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala index 40826c327e33..0737fbd8e6bf 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala @@ -19,6 +19,7 @@ import Util._ import scala.collection.mutable class Semantic { + import Semantic._ // ----- Domain definitions -------------------------------- @@ -410,6 +411,23 @@ class Semantic { def eval(exprs: List[Tree], thisV: Value, klass: ClassSymbol)(using Context, Trace): List[Result] = exprs.map { expr => eval(expr, thisV, klass) } + /** Evaluate arguments of methods */ + def evalArgs(args: List[Arg], thisV: Value, klass: ClassSymbol)(using Context, Trace): List[Error] = + val ress = args.map { arg => + val res = + if arg.isByName then + thisV match + case obj: (ThisRef | Warm) => + val value = Fun(arg.tree, obj, klass) + Result(value, Nil) + case _ => ??? // impossible + else + eval(arg.tree, thisV, klass) + + res.ensureHot("May only use initialized value as arguments", arg.tree) + } + ress.flatMap(_.errors) + /** Handles the evaluation of different expressions * * Note: Recursive call should go to `eval` instead of `cases`. @@ -426,11 +444,7 @@ class Semantic { case NewExpr(tref, New(tpt), ctor, argss) => // check args - val args = argss.flatten - val ress = args.map { arg => - eval(arg, thisV, klass).ensureHot("May use initialized value as arguments", arg) - } - val errors = ress.flatMap(_.errors) + val errors = evalArgs(argss.flatten, thisV, klass) val cls = tref.classSymbol.asClass val res = outerValue(tref, thisV, klass, tpt) @@ -438,11 +452,7 @@ class Semantic { case Call(ref, argss) => // check args - val args = argss.flatten - val ress = args.map { arg => - eval(arg, thisV, klass).ensureHot("May use initialized value as arguments", arg) - } - val errors = ress.flatMap(_.errors) + val errors = evalArgs(argss.flatten, thisV, klass) ref match case Select(supert: Super, _) => @@ -667,19 +677,11 @@ class Semantic { def initParent(parent: Tree) = parent match { case tree @ Block(stats, NewExpr(tref, New(tpt), ctor, argss)) => eval(stats, thisV, klass).foreach { res => errorBuffer ++= res.errors } - argss.flatten.foreach { arg => - val res = eval(arg, thisV, klass) - res.ensureHot("Argument must be an initialized value", arg) - errorBuffer ++= res.errors - } + errorBuffer ++= evalArgs(argss.flatten, thisV, klass) superCall(tref, ctor, tree) case tree @ NewExpr(tref, New(tpt), ctor, argss) => - argss.flatten.foreach { arg => - val res = eval(arg, thisV, klass) - res.ensureHot("Argument must be an initialized value", arg) - errorBuffer ++= res.errors - } + errorBuffer ++= evalArgs(argss.flatten, thisV, klass) superCall(tref, ctor, tree) case _ => @@ -728,7 +730,7 @@ class Semantic { * * This is intended to avoid type soundness issues in Dotty. */ - def checkTermUsage(tpt: Tree, thisV: Value, klass: ClassSymbol)(using Context, Trace): List[Error] = + def checkTermUsage(tpt: Tree, thisV: Value, klass: ClassSymbol)(using Context, Trace): List[Error] = val buf = new mutable.ArrayBuffer[Error] val traverser = new TypeTraverser { def traverse(tp: Type): Unit = tp match { @@ -741,6 +743,10 @@ class Semantic { traverser.traverse(tpt.tpe) buf.toList +} + +object Semantic { + // ----- Utility methods and extractors -------------------------------- def typeRefOf(tp: Type)(using Context): TypeRef = tp.dealias.typeConstructor match { @@ -748,12 +754,28 @@ class Semantic { case hklambda: HKTypeLambda => typeRefOf(hklambda.resType) } + opaque type Arg = Tree | ByNameArg + case class ByNameArg(tree: Tree) + + extension (arg: Arg) + def isByName = arg.isInstanceOf[ByNameArg] + def tree: Tree = arg match + case t: Tree => t + case ByNameArg(t) => t + object Call { - def unapply(tree: Tree)(using Context): Option[(Tree, List[List[Tree]])] = + + def unapply(tree: Tree)(using Context): Option[(Tree, List[List[Arg]])] = tree match case Apply(fn, args) => + val argTps = fn.tpe.widen match + case mt: MethodType => mt.paramInfos + val normArgs: List[Arg] = args.zip(argTps).map { + case (arg, _: ExprType) => ByNameArg(arg) + case (arg, _) => arg + } unapply(fn) match - case Some((ref, args0)) => Some((ref, args0 :+ args)) + case Some((ref, args0)) => Some((ref, args0 :+ normArgs)) case None => None case TypeApply(fn, targs) => @@ -766,7 +788,7 @@ class Semantic { } object NewExpr { - def unapply(tree: Tree)(using Context): Option[(TypeRef, New, Symbol, List[List[Tree]])] = + def unapply(tree: Tree)(using Context): Option[(TypeRef, New, Symbol, List[List[Arg]])] = tree match case Call(fn @ Select(newTree: New, init), argss) if init == nme.CONSTRUCTOR => val tref = typeRefOf(newTree.tpe) diff --git a/tests/init/neg/t3273.check b/tests/init/neg/t3273.check index 74c016ef521f..9598c274c9b2 100644 --- a/tests/init/neg/t3273.check +++ b/tests/init/neg/t3273.check @@ -2,8 +2,6 @@ 4 | val num1: LazyList[Int] = 1 #:: num1.map(_ + 1) // error | ^^^^^^^^^^^^^^^ | Promoting the value to fully-initialized is unsafe. - | Calling trace: - | -> val num1: LazyList[Int] = 1 #:: num1.map(_ + 1) // error [ t3273.scala:4 ] | | The unsafe promotion may cause the following problem(s): | @@ -13,8 +11,6 @@ 5 | val num2: LazyList[Int] = 1 #:: num2.iterator.map(_ + 1).to(LazyList) // error | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | Promoting the value to fully-initialized is unsafe. - | Calling trace: - | -> val num2: LazyList[Int] = 1 #:: num2.iterator.map(_ + 1).to(LazyList) // error [ t3273.scala:5 ] | | The unsafe promotion may cause the following problem(s): | From 01fc3e6e6b3ef9aa1e7e57dd7185652b47a6ffb6 Mon Sep 17 00:00:00 2001 From: Liu Fengyun Date: Sun, 16 May 2021 17:50:15 +0200 Subject: [PATCH 36/50] Ignore known safe method calls --- .../tools/dotc/transform/init/Semantic.scala | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala b/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala index 0737fbd8e6bf..e2d70cadf271 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala @@ -209,6 +209,8 @@ class Semantic { else val rhs = target.defTree.asInstanceOf[ValOrDefDef].rhs eval(rhs, thisRef, target.owner.asClass, cacheResult = true) + else if thisRef.canIgnoreMethodCall(target) then + Result(Hot, Nil) else val error = CallUnknown(target, source, trace) Result(Hot, error :: Nil) @@ -235,6 +237,8 @@ class Semantic { else val rhs = target.defTree.asInstanceOf[ValOrDefDef].rhs eval(rhs, warm, target.owner.asClass, cacheResult = true) + else if warm.canIgnoreMethodCall(target) then + Result(Hot, Nil) else val error = CallUnknown(target, source, trace) Result(Hot, error :: Nil) @@ -385,6 +389,17 @@ class Semantic { case warm: Warm => // ignore end extension +// ----- Policies ------------------------------------------------------ + extension (value: Warm | ThisRef) + /** Can the method call on `value` be ignored? + * + * Note: assume overriding resolution has been performed. + */ + def canIgnoreMethodCall(meth: Symbol)(using Context): Boolean = + val cls = meth.owner + cls == defn.AnyClass || + cls == defn.AnyValClass || + cls == defn.ObjectClass // ----- Semantic definition -------------------------------- From 390691e7e29740cddb0baf460d88176a20ad1b3d Mon Sep 17 00:00:00 2001 From: Liu Fengyun Date: Sun, 16 May 2021 19:14:17 +0200 Subject: [PATCH 37/50] Fix crash: impossible to construct super calls for leaked objects --- compiler/src/dotty/tools/dotc/transform/init/Semantic.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala b/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala index e2d70cadf271..b17b35bb6edb 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala @@ -372,7 +372,7 @@ class Semantic { buffer.nonEmpty || methods.exists { denot => val m = denot.symbol if !m.isConstructor && m.hasSource then - val res = warm.call(m, superType = m.owner.typeRef, source = source) + val res = warm.call(m, superType = NoType, source = source) buffer ++= res.ensureHot(msg, source).errors buffer.nonEmpty } From c6ea1a29ef4f8224c9f95a78a47ed431547400ed Mon Sep 17 00:00:00 2001 From: Liu Fengyun Date: Sun, 16 May 2021 19:18:33 +0200 Subject: [PATCH 38/50] Fix joining empty sequence of values --- compiler/src/dotty/tools/dotc/transform/init/Semantic.scala | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala b/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala index b17b35bb6edb..c4e903e82950 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala @@ -144,7 +144,9 @@ class Semantic { case (RefSet(refs1), RefSet(refs2)) => RefSet(refs1 ++ refs2) extension (values: Seq[Value]) - def join: Value = values.reduce { (v1, v2) => v1.join(v2) } + def join: Value = + if values.isEmpty then Hot + else values.reduce { (v1, v2) => v1.join(v2) } extension (value: Value) def select(field: Symbol, source: Tree)(using Context, Trace): Result = From fa3d6ed4da447490b2280cd0512fffb5d2fa64b4 Mon Sep 17 00:00:00 2001 From: Liu Fengyun Date: Sun, 16 May 2021 19:22:39 +0200 Subject: [PATCH 39/50] Fix crash: use the correct outer class tests/init/crash/explicitOuter.scala --- compiler/src/dotty/tools/dotc/transform/init/Semantic.scala | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala b/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala index c4e903e82950..ed5b82da1f55 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala @@ -652,7 +652,8 @@ class Semantic { resolveThis(target, warm.outer, outerCls) else val outerCls = klass.owner.enclosingClass.asClass - val res = cases(tref.prefix, warm.outer, warm.klass.owner.asClass, EmptyTree) + val warmOuterCls = warm.klass.owner.enclosingClass.asClass + val res = cases(tref.prefix, warm.outer, warmOuterCls, EmptyTree) assert(res.errors.isEmpty, "unexpected error " + res) resolveThis(target, res.value, outerCls) case _ => ??? From f4aa49b6416893f3d35094f467783f247a9e7d87 Mon Sep 17 00:00:00 2001 From: Liu Fengyun Date: Sun, 16 May 2021 19:47:52 +0200 Subject: [PATCH 40/50] Reorganize tests --- tests/init/{pos => neg}/early-promote.scala | 2 +- tests/init/{pos => neg}/features-doublelist.scala | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) rename tests/init/{pos => neg}/early-promote.scala (87%) rename tests/init/{pos => neg}/features-doublelist.scala (58%) diff --git a/tests/init/pos/early-promote.scala b/tests/init/neg/early-promote.scala similarity index 87% rename from tests/init/pos/early-promote.scala rename to tests/init/neg/early-promote.scala index 02b85c3f4e8d..44768579c8ab 100644 --- a/tests/init/pos/early-promote.scala +++ b/tests/init/neg/early-promote.scala @@ -24,7 +24,7 @@ class A { // checking A def c = new C } val b = new B() - List(b) // Direct promotion works here + List(b) // error: no promotion for objects that contain inner classes val af = 42 } diff --git a/tests/init/pos/features-doublelist.scala b/tests/init/neg/features-doublelist.scala similarity index 58% rename from tests/init/pos/features-doublelist.scala rename to tests/init/neg/features-doublelist.scala index 77b4999bf4a6..a18b9ada9f10 100644 --- a/tests/init/pos/features-doublelist.scala +++ b/tests/init/neg/features-doublelist.scala @@ -1,6 +1,6 @@ class DoubleList { class Node(var prev: Node, var next: Node, data: Int) - object sentinel extends Node(sentinel, sentinel, 0) + object sentinel extends Node(sentinel, sentinel, 0) // error // error def insert(x: Int) = ??? } From 48f7b979ff8921de099a335ab0c1f20579472048 Mon Sep 17 00:00:00 2001 From: Liu Fengyun Date: Sun, 16 May 2021 20:55:32 +0200 Subject: [PATCH 41/50] Refactor: move heap from ThisRef for sharing more information --- .../tools/dotc/transform/init/Checker.scala | 5 +- .../tools/dotc/transform/init/Semantic.scala | 111 ++++++++++-------- 2 files changed, 67 insertions(+), 49 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/transform/init/Checker.scala b/compiler/src/dotty/tools/dotc/transform/init/Checker.scala index 593597e39b44..8c803852d11d 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Checker.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Checker.scala @@ -63,8 +63,9 @@ class Checker extends MiniPhase { import semantic._ val tpl = tree.rhs.asInstanceOf[Template] - val thisRef = ThisRef(cls)(fields = mutable.Map.empty) - val res = eval(tpl, thisRef, cls)(using ctx, Vector.empty) + val thisRef = ThisRef(cls) + val heap = Objekt(fields = mutable.Map.empty) + val res = eval(tpl, thisRef, cls)(using heap, ctx, Vector.empty) res.errors.foreach(_.issue) } diff --git a/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala b/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala index ed5b82da1f55..8fb0298bd0a4 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala @@ -38,13 +38,8 @@ class Semantic { case object Cold extends Value /** Object referred by `this` which stores abstract values for all fields - * - * Note: the mutable `fields` plays the role of heap. Thanks to monotonicity - * of the heap, we may handle it in a simple way. */ - case class ThisRef(klass: ClassSymbol)(val fields: mutable.Map[Symbol, Value]) extends Value { - var allFieldsInitialized: Boolean = false - } + case class ThisRef(klass: ClassSymbol) extends Value /** An object with all fields initialized but reaches objects under initialization * @@ -61,6 +56,21 @@ class Semantic { */ case class RefSet(refs: List[Warm | Fun | ThisRef]) extends Value + /** The current object under initialization + */ + case class Objekt(val fields: mutable.Map[Symbol, Value]) { + var allFieldsInitialized: Boolean = false + } + + /** Abstract heap stores abstract objects + * + * As in the OOPSLA paper, the abstract heap is monotonistic. + * + * This is only one object we need to care about, hence it's just `Objekt`. + */ + type Heap = Objekt + def heap(using h: Heap): Heap = h + /** Interpreter configuration * * The (abstract) interpreter can be seen as a push-down automaton @@ -103,28 +113,30 @@ class Semantic { def +(error: Error): Result = this.copy(errors = this.errors :+ error) - def ensureHot(msg: String, source: Tree)(using Context, Trace): Result = + def ensureHot(msg: String, source: Tree): Contextual[Result] = this ++ value.promote(msg, source) - def select(f: Symbol, source: Tree)(using Context, Trace): Result = + def select(f: Symbol, source: Tree): Contextual[Result] = value.select(f, source) ++ errors - def call(meth: Symbol, superType: Type, source: Tree)(using Context, Trace): Result = + def call(meth: Symbol, superType: Type, source: Tree): Contextual[Result] = value.call(meth, superType, source) ++ errors - def instantiate(klass: ClassSymbol, ctor: Symbol, source: Tree)(using Context, Trace): Result = + def instantiate(klass: ClassSymbol, ctor: Symbol, source: Tree): Contextual[Result] = value.instantiate(klass, ctor, source) ++ errors } + /** The state that threads through the interpreter */ + type Contextual[T] = (Heap, Context, Trace) ?=> T + // ----- Error Handling ----------------------------------- type Trace = Vector[Tree] - - val noErrors = Nil + def trace(using t: Trace): Trace = t extension (trace: Trace) def add(node: Tree): Trace = trace :+ node - def trace(using t: Trace): Trace = t + val noErrors = Nil // ----- Operations on domains ----------------------------- extension (a: Value) @@ -149,7 +161,7 @@ class Semantic { else values.reduce { (v1, v2) => v1.join(v2) } extension (value: Value) - def select(field: Symbol, source: Tree)(using Context, Trace): Result = + def select(field: Symbol, source: Tree): Contextual[Result] = value match { case Hot => Result(Hot, noErrors) @@ -161,8 +173,8 @@ class Semantic { case thisRef: ThisRef => val target = resolve(thisRef.klass, field) if target.is(Flags.Lazy) then value.call(target, superType = NoType, source) - else if thisRef.fields.contains(target) then - Result(thisRef.fields(target), Nil) + else if heap.fields.contains(target) then + Result(heap.fields(target), Nil) else val error = AccessNonInit(target, trace.add(source)) Result(Hot, error :: Nil) @@ -186,7 +198,7 @@ class Semantic { Result(value2, errors) } - def call(meth: Symbol, superType: Type, source: Tree)(using Context, Trace): Result = + def call(meth: Symbol, superType: Type, source: Tree): Contextual[Result] = value match { case Hot => Result(Hot, noErrors) @@ -216,8 +228,8 @@ class Semantic { else val error = CallUnknown(target, source, trace) Result(Hot, error :: Nil) - else if thisRef.fields.contains(target) then - Result(thisRef.fields(target), Nil) + else if heap.fields.contains(target) then + Result(heap.fields(target), Nil) else val error = AccessNonInit(target, trace.add(source)) Result(Hot, error :: Nil) @@ -257,7 +269,7 @@ class Semantic { Result(value2, errors) } - def instantiate(klass: ClassSymbol, ctor: Symbol, source: Tree)(using Context, Trace): Result = + def instantiate(klass: ClassSymbol, ctor: Symbol, source: Tree): Contextual[Result] = value match { case Hot => Result(Hot, noErrors) @@ -289,8 +301,17 @@ class Semantic { } end extension + extension (ref: ThisRef | Warm) + def updateField(field: Symbol, value: Value): Contextual[Unit] = + ref match + case thisRef: ThisRef => heap.fields(field) = value + case warm: Warm => // ignore + end extension + +// ----- Promotion ---------------------------------------------------- + extension (value: Value) - def canDirectlyPromote(using Context): Boolean = + def canDirectlyPromote(using Heap, Context): Boolean = value match case Hot => true case Cold => false @@ -299,13 +320,13 @@ class Semantic { warm.outer.canDirectlyPromote case thisRef: ThisRef => - thisRef.allFieldsInitialized || { + heap.allFieldsInitialized || { // If we have all fields initialized, then we can promote This to hot. - thisRef.allFieldsInitialized = thisRef.klass.appliedRef.fields.forall { denot => + heap.allFieldsInitialized = thisRef.klass.appliedRef.fields.forall { denot => val sym = denot.symbol - sym.isOneOf(Flags.Lazy | Flags.Deferred) || thisRef.fields.contains(sym) + sym.isOneOf(Flags.Lazy | Flags.Deferred) || heap.fields.contains(sym) } - thisRef.allFieldsInitialized + heap.allFieldsInitialized } case fun: Fun => false @@ -316,7 +337,7 @@ class Semantic { end canDirectlyPromote /** Promotion of values to hot */ - def promote(msg: String, source: Tree)(using Context, Trace): List[Error] = + def promote(msg: String, source: Tree): Contextual[List[Error]] = value match case Hot => Nil @@ -354,7 +375,7 @@ class Semantic { * promote the field value * */ - def tryPromote(msg: String, source: Tree)(using Context, Trace): List[Error] = + def tryPromote(msg: String, source: Tree): Contextual[List[Error]] = log("promote " + warm.show, printer) { val classRef = warm.klass.appliedRef if classRef.memberClasses.nonEmpty then return PromoteWarm(source, trace) :: Nil @@ -381,16 +402,10 @@ class Semantic { if buffer.isEmpty then Nil else UnsafePromotion(source, trace, buffer.toList) :: Nil + } end extension - extension (ref: ThisRef | Warm) - def updateField(field: Symbol, value: Value): Unit = - ref match - case thisRef: ThisRef => thisRef.fields(field) = value - case warm: Warm => // ignore - end extension - // ----- Policies ------------------------------------------------------ extension (value: Warm | ThisRef) /** Can the method call on `value` be ignored? @@ -409,7 +424,7 @@ class Semantic { * * This method only handles cache logic and delegates the work to `cases`. */ - def eval(expr: Tree, thisV: Value, klass: ClassSymbol, cacheResult: Boolean = false)(using Context, Trace): Result = log("evaluating " + expr.show + ", this = " + thisV.show, printer, res => res.asInstanceOf[Result].show) { + def eval(expr: Tree, thisV: Value, klass: ClassSymbol, cacheResult: Boolean = false): Contextual[Result] = log("evaluating " + expr.show + ", this = " + thisV.show, printer, res => res.asInstanceOf[Result].show) { val innerMap = cache.getOrElseUpdate(thisV, new EqHashMap[Tree, Value]) if (innerMap.contains(expr)) Result(innerMap(expr), noErrors) else { @@ -425,11 +440,11 @@ class Semantic { } /** Evaluate a list of expressions */ - def eval(exprs: List[Tree], thisV: Value, klass: ClassSymbol)(using Context, Trace): List[Result] = + def eval(exprs: List[Tree], thisV: Value, klass: ClassSymbol): Contextual[List[Result]] = exprs.map { expr => eval(expr, thisV, klass) } /** Evaluate arguments of methods */ - def evalArgs(args: List[Arg], thisV: Value, klass: ClassSymbol)(using Context, Trace): List[Error] = + def evalArgs(args: List[Arg], thisV: Value, klass: ClassSymbol): Contextual[List[Error]] = val ress = args.map { arg => val res = if arg.isByName then @@ -449,7 +464,7 @@ class Semantic { * * Note: Recursive call should go to `eval` instead of `cases`. */ - def cases(expr: Tree, thisV: Value, klass: ClassSymbol)(using Context, Trace): Result = + def cases(expr: Tree, thisV: Value, klass: ClassSymbol): Contextual[Result] = expr match { case Ident(nme.WILDCARD) => // TODO: disallow `var x: T = _` @@ -471,15 +486,17 @@ class Semantic { // check args val errors = evalArgs(argss.flatten, thisV, klass) + val trace2: Trace = trace.add(expr) + ref match case Select(supert: Super, _) => val SuperType(thisTp, superTp) = supert.tpe val thisValue2 = resolveThis(thisTp.classSymbol.asClass, thisV, klass) - Result(thisValue2, errors).call(ref.symbol, superTp, expr)(using ctx, trace.add(expr)) + Result(thisValue2, errors).call(ref.symbol, superTp, expr)(using heap, ctx, trace2) case Select(qual, _) => val res = eval(qual, thisV, klass) ++ errors - res.call(ref.symbol, superType = NoType, source = expr)(using ctx, trace.add(expr)) + res.call(ref.symbol, superType = NoType, source = expr)(using heap, ctx, trace2) case id: Ident => id.tpe match @@ -491,10 +508,10 @@ class Semantic { case Hot => Result(Hot, errors) case _ => val rhs = id.symbol.defTree.asInstanceOf[DefDef].rhs - eval(rhs, thisValue2, enclosingClass, cacheResult = true)(using ctx, trace.add(expr)) + eval(rhs, thisValue2, enclosingClass, cacheResult = true)(using heap, ctx, trace2) case TermRef(prefix, _) => val res = cases(prefix, thisV, klass, id) ++ errors - res.call(id.symbol, superType = NoType, source = expr)(using ctx, trace.add(expr)) + res.call(id.symbol, superType = NoType, source = expr)(using heap, ctx, trace2) case Select(qualifier, name) => eval(qualifier, thisV, klass).select(expr.symbol, expr) @@ -611,7 +628,7 @@ class Semantic { } /** Handle semantics of leaf nodes */ - def cases(tp: Type, thisV: Value, klass: ClassSymbol, source: Tree)(using Context, Trace): Result = log("evaluating " + tp.show, printer, res => res.asInstanceOf[Result].show) { + def cases(tp: Type, thisV: Value, klass: ClassSymbol, source: Tree): Contextual[Result] = log("evaluating " + tp.show, printer, res => res.asInstanceOf[Result].show) { tp match { case _: ConstantType => Result(Hot, noErrors) @@ -638,7 +655,7 @@ class Semantic { } /** Resolve C.this that appear in `klass` */ - def resolveThis(target: ClassSymbol, thisV: Value, klass: ClassSymbol)(using Context, Trace): Value = log("resolving " + target.show + ", this = " + thisV.show + " in " + klass.show, printer, res => res.asInstanceOf[Value].show) { + def resolveThis(target: ClassSymbol, thisV: Value, klass: ClassSymbol): Contextual[Value] = log("resolving " + target.show + ", this = " + thisV.show + " in " + klass.show, printer, res => res.asInstanceOf[Value].show) { if target == klass then thisV else thisV match @@ -660,7 +677,7 @@ class Semantic { } /** Compute the outer value that correspond to `tref.prefix` */ - def outerValue(tref: TypeRef, thisV: Value, klass: ClassSymbol, source: Tree)(using Context, Trace): Result = + def outerValue(tref: TypeRef, thisV: Value, klass: ClassSymbol, source: Tree): Contextual[Result] = val cls = tref.classSymbol.asClass if tref.prefix == NoPrefix then val enclosing = cls.owner.lexicallyEnclosingClass.asClass @@ -670,7 +687,7 @@ class Semantic { cases(tref.prefix, thisV, klass, source) /** Initialize part of an abstract object in `klass` of the inheritance chain */ - def init(tpl: Template, thisV: ThisRef | Warm, klass: ClassSymbol)(using Context, Trace): Result = log("init " + klass.show, printer, res => res.asInstanceOf[Result].show) { + def init(tpl: Template, thisV: ThisRef | Warm, klass: ClassSymbol): Contextual[Result] = log("init " + klass.show, printer, res => res.asInstanceOf[Result].show) { val errorBuffer = new mutable.ArrayBuffer[Error] // init param fields @@ -748,7 +765,7 @@ class Semantic { * * This is intended to avoid type soundness issues in Dotty. */ - def checkTermUsage(tpt: Tree, thisV: Value, klass: ClassSymbol)(using Context, Trace): List[Error] = + def checkTermUsage(tpt: Tree, thisV: Value, klass: ClassSymbol): Contextual[List[Error]] = val buf = new mutable.ArrayBuffer[Error] val traverser = new TypeTraverser { def traverse(tp: Type): Unit = tp match { From ee84531efeb7108b5058f9bcae986fec300c72ad Mon Sep 17 00:00:00 2001 From: Liu Fengyun Date: Sun, 16 May 2021 21:38:50 +0200 Subject: [PATCH 42/50] Handle cycles in early promotion --- .../tools/dotc/transform/init/Semantic.scala | 39 ++++++++++++------- tests/init/neg/inner-loop.scala | 19 +++++++++ tests/init/neg/promotion-loop.check | 9 +++++ tests/init/neg/promotion-loop.scala | 19 +++++++++ 4 files changed, 73 insertions(+), 13 deletions(-) create mode 100644 tests/init/neg/promotion-loop.check create mode 100644 tests/init/neg/promotion-loop.scala diff --git a/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala b/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala index 8fb0298bd0a4..d227bd3793fc 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala @@ -60,6 +60,8 @@ class Semantic { */ case class Objekt(val fields: mutable.Map[Symbol, Value]) { var allFieldsInitialized: Boolean = false + + val promotedValues = mutable.Set.empty[Value] } /** Abstract heap stores abstract objects @@ -219,7 +221,7 @@ class Semantic { if target.isPrimaryConstructor then val cls = target.owner.asClass val tpl = cls.defTree.asInstanceOf[TypeDef].rhs.asInstanceOf[Template] - eval(tpl, thisRef, cls, cacheResult = true) + eval(tpl, thisRef, cls, cacheResult = true)(using heap, ctx, trace.add(tpl)) else val rhs = target.defTree.asInstanceOf[ValOrDefDef].rhs eval(rhs, thisRef, target.owner.asClass, cacheResult = true) @@ -317,19 +319,22 @@ class Semantic { case Cold => false case warm: Warm => - warm.outer.canDirectlyPromote + heap.promotedValues.contains(warm) + || warm.outer.canDirectlyPromote case thisRef: ThisRef => - heap.allFieldsInitialized || { + heap.promotedValues.contains(thisRef) || { // If we have all fields initialized, then we can promote This to hot. - heap.allFieldsInitialized = thisRef.klass.appliedRef.fields.forall { denot => + val allFieldsInitialized = thisRef.klass.appliedRef.fields.forall { denot => val sym = denot.symbol sym.isOneOf(Flags.Lazy | Flags.Deferred) || heap.fields.contains(sym) } - heap.allFieldsInitialized + if allFieldsInitialized then heap.promotedValues += thisRef + allFieldsInitialized } - case fun: Fun => false + case fun: Fun => + heap.promotedValues.contains(fun) case RefSet(refs) => refs.forall(_.canDirectlyPromote) @@ -348,8 +353,13 @@ class Semantic { else PromoteThis(source, trace) :: Nil case warm: Warm => - if warm.outer.canDirectlyPromote then Nil - else warm.tryPromote(msg, source) + if warm.canDirectlyPromote then Nil + else { + heap.promotedValues += warm + val errors = warm.tryPromote(msg, source) + if errors.nonEmpty then heap.promotedValues -= warm + errors + } case Fun(body, thisV, klass) => val res = eval(body, thisV, klass) @@ -374,6 +384,9 @@ class Semantic { * 2. for each concrete field `f` of the warm object: * promote the field value * + * TODO: we need to revisit whether this is needed once make the + * system more flexible in other dimentions: e.g. leak to + * methods or constructors, or use ownership for creating cold data structures. */ def tryPromote(msg: String, source: Tree): Contextual[List[Error]] = log("promote " + warm.show, printer) { val classRef = warm.klass.appliedRef @@ -388,15 +401,15 @@ class Semantic { val f = denot.symbol if !f.isOneOf(Flags.Deferred | Flags.Private | Flags.Protected) && f.hasSource then val res = warm.select(f, source) - buffer ++= res.ensureHot(msg, source).errors + buffer ++= res.ensureHot(msg, source)(using heap, ctx, trace.add(f.defTree)).errors buffer.nonEmpty } buffer.nonEmpty || methods.exists { denot => val m = denot.symbol if !m.isConstructor && m.hasSource then - val res = warm.call(m, superType = NoType, source = source) - buffer ++= res.ensureHot(msg, source).errors + val res = warm.call(m, superType = NoType, source = source)(using heap, ctx, trace.add(m.defTree)) + buffer ++= res.ensureHot(msg, source)(using heap, ctx, trace.add(m.defTree)).errors buffer.nonEmpty } @@ -480,7 +493,7 @@ class Semantic { val cls = tref.classSymbol.asClass val res = outerValue(tref, thisV, klass, tpt) - (res ++ errors).instantiate(cls, ctor, expr) + (res ++ errors).instantiate(cls, ctor, expr)(using heap, ctx, trace.add(expr)) case Call(ref, argss) => // check args @@ -705,7 +718,7 @@ class Semantic { // follow constructor if !cls.defTree.isEmpty then - val res2 = thisV.call(ctor, superType = NoType, source) + val res2 = thisV.call(ctor, superType = NoType, source)(using heap, ctx, trace.add(source)) errorBuffer ++= res2.errors // parents diff --git a/tests/init/neg/inner-loop.scala b/tests/init/neg/inner-loop.scala index a7ff5c153d32..c6d5c615580c 100644 --- a/tests/init/neg/inner-loop.scala +++ b/tests/init/neg/inner-loop.scala @@ -13,3 +13,22 @@ class Outer2 { outer => val inner = new Inner val n = 6 } + +class Test { + class Outer3 { outer => + class Inner extends Outer3 { + val x = 5 + n + } + val inner = new Inner + val n = 6 + } + + val outer = new Outer3 + // Warm objects with inner classes not checked. + // If we change policy to check more eagerly, + // the check has to avoid loop here. + + println(outer) // error + + val m = 10 +} \ No newline at end of file diff --git a/tests/init/neg/promotion-loop.check b/tests/init/neg/promotion-loop.check new file mode 100644 index 000000000000..66e74da4de81 --- /dev/null +++ b/tests/init/neg/promotion-loop.check @@ -0,0 +1,9 @@ +-- Error: tests/init/neg/promotion-loop.scala:16:10 -------------------------------------------------------------------- +16 | println(b) // error + | ^ + | Promoting the value to fully-initialized is unsafe. + | + | The unsafe promotion may cause the following problem(s): + | + | 1. Promote the value under initialization to fully-initialized. Calling trace: + | -> val outer = test [ promotion-loop.scala:12 ] diff --git a/tests/init/neg/promotion-loop.scala b/tests/init/neg/promotion-loop.scala new file mode 100644 index 000000000000..7f6856c34cae --- /dev/null +++ b/tests/init/neg/promotion-loop.scala @@ -0,0 +1,19 @@ +class Test { test => + class A { + val self = this + } + + val a = new A + println(a) + + + class B { + val self = this + val outer = test + } + + val b = new B + println(b) // error + + val n = 10 +} \ No newline at end of file From d94d794425e7614655ac77d49e8039c0b5a02e37 Mon Sep 17 00:00:00 2001 From: Liu Fengyun Date: Sun, 16 May 2021 22:06:37 +0200 Subject: [PATCH 43/50] Handle exception with super calls --- .../tools/dotc/transform/init/Semantic.scala | 30 +++++++++++++++---- tests/init/neg/super.scala | 30 +++++++++++++++++++ 2 files changed, 54 insertions(+), 6 deletions(-) create mode 100644 tests/init/neg/super.scala diff --git a/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala b/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala index d227bd3793fc..d9f7c30c6fdf 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala @@ -14,7 +14,6 @@ import config.Printers.init as printer import reporting.trace as log import Errors._ -import Util._ import scala.collection.mutable @@ -212,8 +211,7 @@ class Semantic { case thisRef: ThisRef => val target = if superType.exists then - // TODO: superType could be A & B when there is self-annotation - resolveSuper(thisRef.klass, superType.classSymbol.asClass, meth) + resolveSuper(thisRef.klass, superType, meth) else resolve(thisRef.klass, meth) if target.isOneOf(Flags.Method | Flags.Lazy) then @@ -239,8 +237,7 @@ class Semantic { case warm: Warm => val target = if superType.exists then - // TODO: superType could be A & B when there is self-annotation - resolveSuper(warm.klass, superType.classSymbol.asClass, meth) + resolveSuper(warm.klass, superType, meth) else resolve(warm.klass, meth) if target.is(Flags.Param) then @@ -706,7 +703,7 @@ class Semantic { // init param fields klass.paramAccessors.foreach { acc => if (!acc.is(Flags.Method)) { - traceIndented(acc.show + " initialized", printer) + printer.println(acc.show + " initialized") thisV.updateField(acc, Hot) } } @@ -843,4 +840,25 @@ object Semantic { Some((tref, newTree, fn.symbol, argss)) case _ => None } + + extension (symbol: Symbol) def hasSource(using Context): Boolean = + !symbol.defTree.isEmpty + + def resolve(cls: ClassSymbol, sym: Symbol)(using Context): Symbol = + if (sym.isEffectivelyFinal || sym.isConstructor) sym + else sym.matchingMember(cls.appliedRef) + + def resolveSuper(cls: ClassSymbol, superType: Type, sym: Symbol)(using Context): Symbol = { + import annotation.tailrec + @tailrec def loop(bcs: List[ClassSymbol]): Symbol = bcs match { + case bc :: bcs1 => + val cand = sym.matchingDecl(bcs.head, cls.thisType) + .suchThat(alt => !alt.is(Flags.Deferred)).symbol + if (cand.exists) cand else loop(bcs.tail) + case _ => + NoSymbol + } + loop(cls.info.baseClasses.dropWhile(sym.owner != _)) + } + } diff --git a/tests/init/neg/super.scala b/tests/init/neg/super.scala new file mode 100644 index 000000000000..92ad707018f9 --- /dev/null +++ b/tests/init/neg/super.scala @@ -0,0 +1,30 @@ +trait A: + def foo() = 1 + +trait B: + def foo() = 1 + +trait C: + def foo() = n + def n: Int + +class Foo extends A, B, C: + super[A].foo() + + override def foo() = n + + val n = 10 + +class Bar extends A, B, C: + super[C].foo() + + override def foo() = n * n + + val n = 10 + +class Qux extends A, B, C: + super.foo() + + override def foo() = n * n + + val n = 10 From 2f2253d12070e04c456f52cea1b4abc9b5a26f3d Mon Sep 17 00:00:00 2001 From: Liu Fengyun Date: Sun, 16 May 2021 22:47:52 +0200 Subject: [PATCH 44/50] Handle package and static objects in this resolution --- .../tools/dotc/transform/init/Checker.scala | 2 +- .../tools/dotc/transform/init/Semantic.scala | 23 +++++++++++-------- tests/init/neg/inner30.scala | 21 +++++++++++++++++ 3 files changed, 35 insertions(+), 11 deletions(-) create mode 100644 tests/init/neg/inner30.scala diff --git a/compiler/src/dotty/tools/dotc/transform/init/Checker.scala b/compiler/src/dotty/tools/dotc/transform/init/Checker.scala index 8c803852d11d..23b8d826a77c 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Checker.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Checker.scala @@ -64,7 +64,7 @@ class Checker extends MiniPhase { import semantic._ val tpl = tree.rhs.asInstanceOf[Template] val thisRef = ThisRef(cls) - val heap = Objekt(fields = mutable.Map.empty) + val heap = Objekt(cls, fields = mutable.Map.empty) val res = eval(tpl, thisRef, cls)(using heap, ctx, Vector.empty) res.errors.foreach(_.issue) } diff --git a/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala b/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala index d9f7c30c6fdf..f2a19468e2f6 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala @@ -57,7 +57,7 @@ class Semantic { /** The current object under initialization */ - case class Objekt(val fields: mutable.Map[Symbol, Value]) { + case class Objekt(klass: ClassSymbol, val fields: mutable.Map[Symbol, Value]) { var allFieldsInitialized: Boolean = false val promotedValues = mutable.Set.empty[Value] @@ -501,7 +501,7 @@ class Semantic { ref match case Select(supert: Super, _) => val SuperType(thisTp, superTp) = supert.tpe - val thisValue2 = resolveThis(thisTp.classSymbol.asClass, thisV, klass) + val thisValue2 = resolveThis(thisTp.classSymbol.asClass, thisV, klass, ref) Result(thisValue2, errors).call(ref.symbol, superTp, expr)(using heap, ctx, trace2) case Select(qual, _) => @@ -513,7 +513,7 @@ class Semantic { case TermRef(NoPrefix, _) => // resolve this for the local method val enclosingClass = id.symbol.owner.enclosingClass.asClass - val thisValue2 = resolveThis(enclosingClass, thisV, klass) + val thisValue2 = resolveThis(enclosingClass, thisV, klass, id) thisValue2 match case Hot => Result(Hot, errors) case _ => @@ -652,7 +652,7 @@ class Semantic { case tp @ ThisType(tref) => if tref.symbol.is(Flags.Package) then Result(Hot, noErrors) else - val value = resolveThis(tref.classSymbol.asClass, thisV, klass) + val value = resolveThis(tref.classSymbol.asClass, thisV, klass, source) Result(value, noErrors) case _: TermParamRef | _: RecThis => @@ -665,8 +665,9 @@ class Semantic { } /** Resolve C.this that appear in `klass` */ - def resolveThis(target: ClassSymbol, thisV: Value, klass: ClassSymbol): Contextual[Value] = log("resolving " + target.show + ", this = " + thisV.show + " in " + klass.show, printer, res => res.asInstanceOf[Value].show) { + def resolveThis(target: ClassSymbol, thisV: Value, klass: ClassSymbol, source: Tree): Contextual[Value] = log("resolving " + target.show + ", this = " + thisV.show + " in " + klass.show, printer, res => res.asInstanceOf[Value].show) { if target == klass then thisV + else if target.is(Flags.Package) || target.isStaticOwner && target != heap.klass then Hot else thisV match case Hot | _: ThisRef => Hot @@ -676,14 +677,16 @@ class Semantic { if tref.prefix == NoPrefix then // Current class is local, in the enclosing scope of `warm.klass` val outerCls = warm.klass.owner.enclosingClass.asClass - resolveThis(target, warm.outer, outerCls) + resolveThis(target, warm.outer, outerCls, source) else val outerCls = klass.owner.enclosingClass.asClass val warmOuterCls = warm.klass.owner.enclosingClass.asClass - val res = cases(tref.prefix, warm.outer, warmOuterCls, EmptyTree) + val res = cases(tref.prefix, warm.outer, warmOuterCls, source) assert(res.errors.isEmpty, "unexpected error " + res) - resolveThis(target, res.value, outerCls) - case _ => ??? + resolveThis(target, res.value, outerCls, source) + case _ => + // report.error("unexpected thisV = " + thisV + ", target = " + target.show + ", klass = " + klass.show, source.srcPos) + Cold } /** Compute the outer value that correspond to `tref.prefix` */ @@ -691,7 +694,7 @@ class Semantic { val cls = tref.classSymbol.asClass if tref.prefix == NoPrefix then val enclosing = cls.owner.lexicallyEnclosingClass.asClass - val outerV = resolveThis(enclosing, thisV, klass) + val outerV = resolveThis(enclosing, thisV, klass, source) Result(outerV, noErrors) else cases(tref.prefix, thisV, klass, source) diff --git a/tests/init/neg/inner30.scala b/tests/init/neg/inner30.scala new file mode 100644 index 000000000000..01bb5754d485 --- /dev/null +++ b/tests/init/neg/inner30.scala @@ -0,0 +1,21 @@ +object Scanners { + enum IndentWidth { + case Run(ch: Char, n: Int) + case Conc(l: IndentWidth, r: Run) + } + + import IndentWidth.* + + class Scanner { + def foo() = + Conc(Run('a', 3), Run('b', 4)) + new LookAheadScanner + + class LookAheadScanner() extends Scanner + + foo() + } + + val m: Int = n * 2 + val n = 10 // error +} \ No newline at end of file From 062bb98536b8491027e73304597858d04dced989 Mon Sep 17 00:00:00 2001 From: Liu Fengyun Date: Mon, 17 May 2021 09:51:35 +0200 Subject: [PATCH 45/50] Handle poly functions --- .../tools/dotc/transform/init/Semantic.scala | 23 ++++++++++++++++++- tests/init/neg/polyfun.scala | 8 +++++++ tests/init/neg/super.scala | 4 ++-- 3 files changed, 32 insertions(+), 3 deletions(-) create mode 100644 tests/init/neg/polyfun.scala diff --git a/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala b/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala index f2a19468e2f6..495e2baa7cd2 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala @@ -555,6 +555,14 @@ class Semantic { case _ => ??? // impossible + case PolyFun(body) => + thisV match + case obj: (ThisRef | Warm) => + val value = Fun(body, obj, klass) + Result(value, Nil) + case _ => + ??? // impossible + case Block(stats, expr) => val ress = eval(stats, thisV, klass) eval(expr, thisV, klass) ++ ress.flatMap(_.errors) @@ -829,7 +837,7 @@ object Semantic { case TypeApply(fn, targs) => unapply(fn) - case ref: RefTree if ref.symbol.is(Flags.Method) => + case ref: RefTree if ref.tpe.widenSingleton.isInstanceOf[MethodicType] => Some((ref, Nil)) case _ => None @@ -844,6 +852,19 @@ object Semantic { case _ => None } + object PolyFun { + def unapply(tree: Tree)(using Context): Option[Tree] = + 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) + case _ => + None + } + extension (symbol: Symbol) def hasSource(using Context): Boolean = !symbol.defTree.isEmpty diff --git a/tests/init/neg/polyfun.scala b/tests/init/neg/polyfun.scala new file mode 100644 index 000000000000..25196ceaa76e --- /dev/null +++ b/tests/init/neg/polyfun.scala @@ -0,0 +1,8 @@ +class Test { + val m: [T] => (arg: T) => T = + [T] => (arg: T) => { + println(n) + arg + } + val n = m.apply(arg = 23) +} diff --git a/tests/init/neg/super.scala b/tests/init/neg/super.scala index 92ad707018f9..5a8e72cce65f 100644 --- a/tests/init/neg/super.scala +++ b/tests/init/neg/super.scala @@ -20,11 +20,11 @@ class Bar extends A, B, C: override def foo() = n * n - val n = 10 + val n = 10 // error class Qux extends A, B, C: super.foo() override def foo() = n * n - val n = 10 + val n = 10 // error From a7af9d80bc3825fa68858bbed5cef12de348bd40 Mon Sep 17 00:00:00 2001 From: Liu Fengyun Date: Mon, 17 May 2021 09:54:30 +0200 Subject: [PATCH 46/50] Add test for structural types --- tests/init/neg/structural.scala | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 tests/init/neg/structural.scala diff --git a/tests/init/neg/structural.scala b/tests/init/neg/structural.scala new file mode 100644 index 000000000000..27b37a04bef7 --- /dev/null +++ b/tests/init/neg/structural.scala @@ -0,0 +1,12 @@ +import reflect.Selectable.reflectiveSelectable + +class Test { + trait A + val m: A { def apply(x: Int): Int } = + new A { + def apply(x: Int): Int = + n + x + } + + val n = m(23) // error +} From b978ad9e4be1f269a4a5f7658e600387e0596499 Mon Sep 17 00:00:00 2001 From: Liu Fengyun Date: Mon, 17 May 2021 10:21:29 +0200 Subject: [PATCH 47/50] Fix test for poly functions --- compiler/src/dotty/tools/dotc/transform/init/Semantic.scala | 6 +++--- tests/init/neg/polyfun.scala | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala b/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala index 495e2baa7cd2..1e88e1063168 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala @@ -257,9 +257,9 @@ class Semantic { Result(Hot, error :: Nil) case Fun(body, thisV, klass) => - if meth.name == nme.apply then eval(body, thisV, klass, cacheResult = true) - else if meth.name.toString == "tupled" then Result(value, Nil) - else Result(Hot, Nil) // TODO: refine + // meth == NoSymbol for poly functions + if meth.name.toString == "tupled" then Result(value, Nil) + else eval(body, thisV, klass, cacheResult = true) case RefSet(refs) => val resList = refs.map(_.call(meth, superType, source)) diff --git a/tests/init/neg/polyfun.scala b/tests/init/neg/polyfun.scala index 25196ceaa76e..a3a3ecc76814 100644 --- a/tests/init/neg/polyfun.scala +++ b/tests/init/neg/polyfun.scala @@ -4,5 +4,5 @@ class Test { println(n) arg } - val n = m.apply(arg = 23) + val n = m.apply(arg = 23) // error } From 3c255330be0fcd918aacf4b4297fbc8292d658b4 Mon Sep 17 00:00:00 2001 From: Liu Fengyun Date: Tue, 18 May 2021 11:50:57 +0200 Subject: [PATCH 48/50] Remove unused field --- compiler/src/dotty/tools/dotc/transform/init/Semantic.scala | 2 -- 1 file changed, 2 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala b/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala index 1e88e1063168..e5d60d7ace24 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala @@ -58,8 +58,6 @@ class Semantic { /** The current object under initialization */ case class Objekt(klass: ClassSymbol, val fields: mutable.Map[Symbol, Value]) { - var allFieldsInitialized: Boolean = false - val promotedValues = mutable.Set.empty[Value] } From f67b3cd24e98a2db507df87106f5b7f056c01c8a Mon Sep 17 00:00:00 2001 From: Liu Fengyun Date: Wed, 19 May 2021 11:30:44 +0200 Subject: [PATCH 49/50] Address review --- .../tools/dotc/transform/init/Errors.scala | 3 +- .../tools/dotc/transform/init/Semantic.scala | 119 ++++++++++++++---- tests/init/neg/early-promote.scala | 2 +- 3 files changed, 95 insertions(+), 29 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/transform/init/Errors.scala b/compiler/src/dotty/tools/dotc/transform/init/Errors.scala index 2be9fac3c2d4..143d53c9d2a7 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Errors.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Errors.scala @@ -98,7 +98,8 @@ object Errors { case class CallUnknown(meth: Symbol, source: Tree, trace: Vector[Tree]) extends Error { def show(using Context): String = - "Calling the external method " + meth.show + " may cause initialization errors" + "." + val prefix = if meth.is(Flags.Method) then "Calling the external method " else "Accessing the external field" + prefix + meth.show + " may cause initialization errors" + "." } /** Promote a value under initialization to fully-initialized */ diff --git a/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala b/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala index e5d60d7ace24..02ebab8a9980 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala @@ -24,9 +24,24 @@ class Semantic { /** Abstract values * - * Value = Hot | Cold | Warm | ThisRef | Fun | RefSet + * Value = Hot | Cold | Warm | ThisRef | Fun | RefSet + * + * Cold + * ┌──────► ▲ ◄──┐ ◄────┐ + * │ │ │ │ + * │ │ │ │ + * ThisRef(C) │ │ │ + * ▲ │ │ │ + * │ Warm(D) Fun RefSet + * │ ▲ ▲ ▲ + * │ │ │ │ + * Warm(C) │ │ │ + * ▲ │ │ │ + * │ │ │ │ + * └─────────┴──────┴───────┘ + * Hot */ - trait Value { + sealed abstract class Value { def show: String = this.toString() } @@ -36,7 +51,7 @@ class Semantic { /** An object with unknown initialization status */ case object Cold extends Value - /** Object referred by `this` which stores abstract values for all fields + /** A reference to the object under initialization pointed by `this` */ case class ThisRef(klass: ClassSymbol) extends Value @@ -55,7 +70,11 @@ class Semantic { */ case class RefSet(refs: List[Warm | Fun | ThisRef]) extends Value + // end of value definition + /** The current object under initialization + * + * Note: Object is NOT a value. */ case class Objekt(klass: ClassSymbol, val fields: mutable.Map[Symbol, Value]) { val promotedValues = mutable.Set.empty[Value] @@ -147,6 +166,9 @@ class Semantic { case (Cold, _) => Cold case (_, Cold) => Cold + case (a: Warm, b: ThisRef) if a.klass == b.klass => b + case (a: ThisRef, b: Warm) if a.klass == b.klass => a + case (a: (Fun | Warm | ThisRef), b: (Fun | Warm | ThisRef)) => RefSet(a :: b :: Nil) case (a: (Fun | Warm | ThisRef), RefSet(refs)) => RefSet(a :: refs) @@ -256,7 +278,7 @@ class Semantic { case Fun(body, thisV, klass) => // meth == NoSymbol for poly functions - if meth.name.toString == "tupled" then Result(value, Nil) + if meth.name.toString == "tupled" then Result(value, Nil) // a call like `fun.tupled` else eval(body, thisV, klass, cacheResult = true) case RefSet(refs) => @@ -308,14 +330,23 @@ class Semantic { // ----- Promotion ---------------------------------------------------- extension (value: Value) - def canDirectlyPromote(using Heap, Context): Boolean = + /** Can we promote the value by checking the extrinsic values? + * + * The extrinsic values are environment values, e.g. outers for `Warm` + * and `thisV` captured in functions. + * + * This is a fast track for early promotion of values. + */ + def canPromoteExtrinsic(using Heap, Context): Boolean = value match case Hot => true case Cold => false case warm: Warm => - heap.promotedValues.contains(warm) - || warm.outer.canDirectlyPromote + warm.outer.canPromoteExtrinsic && { + heap.promotedValues += warm + true + } case thisRef: ThisRef => heap.promotedValues.contains(thisRef) || { @@ -329,12 +360,15 @@ class Semantic { } case fun: Fun => - heap.promotedValues.contains(fun) + fun.thisV.canPromoteExtrinsic && { + heap.promotedValues += fun + true + } case RefSet(refs) => - refs.forall(_.canDirectlyPromote) + refs.forall(_.canPromoteExtrinsic) - end canDirectlyPromote + end canPromoteExtrinsic /** Promotion of values to hot */ def promote(msg: String, source: Tree): Contextual[List[Error]] = @@ -344,11 +378,13 @@ class Semantic { case Cold => PromoteCold(source, trace) :: Nil case thisRef: ThisRef => - if thisRef.canDirectlyPromote then Nil + if heap.promotedValues.contains(thisRef) then Nil + else if thisRef.canPromoteExtrinsic then Nil else PromoteThis(source, trace) :: Nil case warm: Warm => - if warm.canDirectlyPromote then Nil + if heap.promotedValues.contains(warm) then Nil + else if warm.canPromoteExtrinsic then Nil else { heap.promotedValues += warm val errors = warm.tryPromote(msg, source) @@ -356,13 +392,16 @@ class Semantic { errors } - case Fun(body, thisV, klass) => - val res = eval(body, thisV, klass) - val errors2 = res.value.promote(msg, source) - if (res.errors.nonEmpty || errors2.nonEmpty) - UnsafePromotion(source, trace, res.errors ++ errors2) :: Nil + case fun @ Fun(body, thisV, klass) => + if heap.promotedValues.contains(fun) then Nil else - Nil + val res = eval(body, thisV, klass) + val errors2 = res.value.promote(msg, source) + if (res.errors.nonEmpty || errors2.nonEmpty) + UnsafePromotion(source, trace, res.errors ++ errors2) :: Nil + else + heap.promotedValues += fun + Nil case RefSet(refs) => refs.flatMap(_.promote(msg, source)) @@ -379,7 +418,10 @@ class Semantic { * 2. for each concrete field `f` of the warm object: * promote the field value * - * TODO: we need to revisit whether this is needed once make the + * If the object contains nested classes as members, the checker simply + * reports a warning to avoid expensive checks. + * + * TODO: we need to revisit whether this is needed once we make the * system more flexible in other dimentions: e.g. leak to * methods or constructors, or use ownership for creating cold data structures. */ @@ -430,7 +472,18 @@ class Semantic { /** Evaluate an expression with the given value for `this` in a given class `klass` * - * This method only handles cache logic and delegates the work to `cases`. + * 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`. */ def eval(expr: Tree, thisV: Value, klass: ClassSymbol, cacheResult: Boolean = false): Contextual[Result] = log("evaluating " + expr.show + ", this = " + thisV.show, printer, res => res.asInstanceOf[Result].show) { val innerMap = cache.getOrElseUpdate(thisV, new EqHashMap[Tree, Value]) @@ -551,7 +604,10 @@ class Semantic { val value = Fun(ddef.rhs, obj, klass) Result(value, Nil) case _ => - ??? // impossible + // The reason is that we never evaluate an expression if `thisV` is + // Cold. And `thisV` can never be `Fun`. + report.warning("Unexpected branch reached. this = " + thisV.show, expr.srcPos) + Result(Hot, Nil) case PolyFun(body) => thisV match @@ -559,7 +615,9 @@ class Semantic { val value = Fun(body, obj, klass) Result(value, Nil) case _ => - ??? // impossible + // See the comment for the case above + report.warning("Unexpected branch reached. this = " + thisV.show, expr.srcPos) + Result(Hot, Nil) case Block(stats, expr) => val ress = eval(stats, thisV, klass) @@ -723,22 +781,22 @@ class Semantic { // ignored as they are all hot // follow constructor - if !cls.defTree.isEmpty then + if cls.hasSource then val res2 = thisV.call(ctor, superType = NoType, source)(using heap, ctx, trace.add(source)) errorBuffer ++= res2.errors // parents def initParent(parent: Tree) = parent match { - case tree @ Block(stats, NewExpr(tref, New(tpt), ctor, argss)) => + case tree @ Block(stats, NewExpr(tref, New(tpt), ctor, argss)) => // can happen eval(stats, thisV, klass).foreach { res => errorBuffer ++= res.errors } errorBuffer ++= evalArgs(argss.flatten, thisV, klass) superCall(tref, ctor, tree) - case tree @ NewExpr(tref, New(tpt), ctor, argss) => + case tree @ NewExpr(tref, New(tpt), ctor, argss) => // extends A(args) errorBuffer ++= evalArgs(argss.flatten, thisV, klass) superCall(tref, ctor, tree) - case _ => + case _ => // extends A or extends A[T] val tref = typeRefOf(parent.tpe) superCall(tref, tref.classSymbol.primaryConstructor, parent) } @@ -758,6 +816,13 @@ class Semantic { 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, superParent) @@ -780,7 +845,7 @@ class Semantic { Result(thisV, errorBuffer.toList) } - /** Check usage of terms inside types + /** Check that path in path-dependent types are initialized * * This is intended to avoid type soundness issues in Dotty. */ diff --git a/tests/init/neg/early-promote.scala b/tests/init/neg/early-promote.scala index 44768579c8ab..ac1a7c8fe82e 100644 --- a/tests/init/neg/early-promote.scala +++ b/tests/init/neg/early-promote.scala @@ -24,7 +24,7 @@ class A { // checking A def c = new C } val b = new B() - List(b) // error: no promotion for objects that contain inner classes + List(b) // error: the checker simply issue warnings for objects that contain inner classes val af = 42 } From 12bfbd27637d381f8860fcaf804dd8c2859bfa49 Mon Sep 17 00:00:00 2001 From: Liu Fengyun Date: Fri, 21 May 2021 09:26:49 +0200 Subject: [PATCH 50/50] Address review: Add more documentation --- .../tools/dotc/transform/init/Semantic.scala | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala b/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala index 02ebab8a9980..bd24cfbe5775 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala @@ -40,6 +40,20 @@ class Semantic { * │ │ │ │ * └─────────┴──────┴───────┘ * Hot + * + * The most important ordering is the following: + * + * Hot ⊑ Warm(C) ⊑ ThisRef(C) ⊑ Cold + * + * The diagram above does not reflect relationship between `RefSet` + * and other values. `RefSet` represents a set of values which could + * be `ThisRef`, `Warm` or `Fun`. The following ordering applies for + * RefSet: + * + * R_a ⊑ R_b if R_a ⊆ R_b + * + * V ⊑ R if V ∈ R + * */ sealed abstract class Value { def show: String = this.toString() @@ -288,6 +302,7 @@ class Semantic { Result(value2, errors) } + /** Handle a new expression `new p.C` where `p` is abstracted by `value` */ def instantiate(klass: ClassSymbol, ctor: Symbol, source: Tree): Contextual[Result] = value match { case Hot =>