From 4a7e4204add9ab032c2d85a99be89ddb6509869d Mon Sep 17 00:00:00 2001 From: Fengyun Liu Date: Mon, 23 May 2022 02:16:48 +0200 Subject: [PATCH 01/11] Don't chain promotion --- .../tools/dotc/transform/init/Errors.scala | 8 ++--- .../tools/dotc/transform/init/Semantic.scala | 31 +++++++++++-------- tests/init/neg/closureLeak.check | 6 ++-- tests/init/neg/default-this.check | 6 ++-- tests/init/neg/inherit-non-hot.check | 4 +-- tests/init/neg/inlined-method.check | 6 ++-- tests/init/neg/local-warm4.check | 30 +++++++++--------- tests/init/neg/promotion-loop.check | 6 ++-- tests/init/neg/t3273.check | 8 ++--- 9 files changed, 55 insertions(+), 50 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/transform/init/Errors.scala b/compiler/src/dotty/tools/dotc/transform/init/Errors.scala index 80788a1ef715..219953c43f4c 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Errors.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Errors.scala @@ -85,9 +85,9 @@ object Errors { report.warning(show + stacktrace, field.srcPos) } - /** Promote `this` under initialization to fully-initialized */ + /** Promote a value under initialization to fully-initialized */ case class PromoteError(msg: String, source: Tree, trace: Seq[Tree]) extends Error { - def show(using Context): String = "Cannot prove that the value is fully initialized. " + msg + "." + def show(using Context): String = msg } case class AccessCold(field: Symbol, source: Tree, trace: Seq[Tree]) extends Error { @@ -114,8 +114,8 @@ object Errors { def show(using Context): String = { var index = 0 - "Cannot prove that the value is fully initialized. " + msg + ".\n" + stacktrace + - "\nThe unsafe promotion may cause the following problem:\n" + + msg + "\n" + stacktrace + "\n" + + "Promoting the value to fully initialized failed due to the following problem:\n" + errors.head.show + errors.head.stacktrace } } diff --git a/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala b/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala index f88de0100dac..1abb46a175a3 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala @@ -918,20 +918,21 @@ object Semantic { case warm: Warm => if promoted.contains(warm) then Nil - else { + else promoted.add(warm) val errors = warm.tryPromote(msg, source) if errors.nonEmpty then promoted.remove(warm) errors - } case fun @ Fun(body, thisV, klass, env) => if promoted.contains(fun) then Nil else val res = withEnv(env) { eval(body, thisV, klass) } - val errors2 = res.value.promote(msg, source) - if (res.errors.nonEmpty || errors2.nonEmpty) - UnsafePromotion(msg, source, trace.toVector, res.errors ++ errors2) :: Nil + val errors = + if res.value == Hot then res.errors + else res.errors :+ PromoteError("The function return value is not fully initialized.", source, trace.toVector) + if (res.errors.nonEmpty || res.value != Hot) + UnsafePromotion(msg, source, trace.toVector, errors) :: Nil else promoted.add(fun) Nil @@ -966,20 +967,24 @@ object Semantic { val buffer = new mutable.ArrayBuffer[Error] + def checkHot(res: Result, msg: String, source: Tree): Errors = + if res.value == Hot || promoted.contains(res.value) then res.errors + else res.errors :+ PromoteError(msg, source, trace.toVector) + warm.klass.baseClasses.exists { klass => klass.hasSource && klass.info.decls.exists { member => if !member.isType && !member.isConstructor && member.hasSource && !member.is(Flags.Deferred) then - if member.is(Flags.Method) then + if member.is(Flags.Method, butNot = Flags.Accessor) then val trace2 = trace.add(source) locally { given Trace = trace2 val args = member.info.paramInfoss.flatten.map(_ => ArgInfo(Hot, EmptyTree)) val res = warm.call(member, args, receiver = NoType, superType = NoType, source = member.defTree) - buffer ++= res.ensureHot(msg, source).errors + buffer ++= checkHot(res, "Cannot prove that the return value of " + member + " is fully initialized.", source) } else val res = warm.select(member, source) - buffer ++= res.ensureHot(msg, source).errors + buffer ++= checkHot(res, "Cannot prove that the field " + member + " is fully initialized.", source) buffer.nonEmpty } } @@ -1076,7 +1081,7 @@ object Semantic { /** Utility definition used for better error-reporting of argument errors */ case class ArgInfo(value: Value, source: Tree) { - def promote: Contextual[List[Error]] = value.promote("Only initialized values may be used as arguments", source) + def promote: Contextual[List[Error]] = value.promote("Cannot prove the argument is fully initialized.", source) } /** Evaluate an expression with the given value for `this` in a given class `klass` @@ -1206,9 +1211,9 @@ object Semantic { lhs match case Select(qual, _) => val res = eval(qual, thisV, klass) - eval(rhs, thisV, klass).ensureHot("May only assign fully initialized value", rhs) ++ res.errors + eval(rhs, thisV, klass).ensureHot("May only assign fully initialized value.", rhs) ++ res.errors case id: Ident => - eval(rhs, thisV, klass).ensureHot("May only assign fully initialized value", rhs) + eval(rhs, thisV, klass).ensureHot("May only assign fully initialized value.", rhs) case closureDef(ddef) => val value = Fun(ddef.rhs, thisV, klass, env) @@ -1233,14 +1238,14 @@ object Semantic { else eval(arg, thisV, klass) case Match(selector, cases) => - val res1 = eval(selector, thisV, klass).ensureHot("The value to be matched needs to be fully initialized", selector) + val res1 = eval(selector, thisV, klass).ensureHot("The value to be matched needs to be fully 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) + eval(expr, thisV, klass).ensureHot("return expression may only be initialized value.", expr) case WhileDo(cond, body) => val ress = eval(cond :: body :: Nil, thisV, klass) diff --git a/tests/init/neg/closureLeak.check b/tests/init/neg/closureLeak.check index 3bd3cdd09b9b..12d0316bb3b0 100644 --- a/tests/init/neg/closureLeak.check +++ b/tests/init/neg/closureLeak.check @@ -1,7 +1,7 @@ -- Error: tests/init/neg/closureLeak.scala:11:14 ----------------------------------------------------------------------- 11 | l.foreach(a => a.addX(this)) // error | ^^^^^^^^^^^^^^^^^ - | Cannot prove that the value is fully initialized. Only initialized values may be used as arguments. + | Cannot prove the argument is fully initialized. | - | The unsafe promotion may cause the following problem: - | Cannot prove that the value is fully initialized. Only initialized values may be used as arguments. + | Promoting the value to fully initialized failed due to the following problem: + | Cannot prove the argument is fully initialized. diff --git a/tests/init/neg/default-this.check b/tests/init/neg/default-this.check index 22ff677c4113..fd111c559819 100644 --- a/tests/init/neg/default-this.check +++ b/tests/init/neg/default-this.check @@ -1,6 +1,6 @@ -- Error: tests/init/neg/default-this.scala:9:8 ------------------------------------------------------------------------ 9 | compare() // error | ^^^^^^^ - |Cannot prove that the value is fully initialized. Only initialized values may be used as arguments. Calling trace: - |-> val result = updateThenCompare(5) [ default-this.scala:11 ] - | ^^^^^^^^^^^^^^^^^^^^ + | Cannot prove the argument is fully initialized. Calling trace: + | -> val result = updateThenCompare(5) [ default-this.scala:11 ] + | ^^^^^^^^^^^^^^^^^^^^ diff --git a/tests/init/neg/inherit-non-hot.check b/tests/init/neg/inherit-non-hot.check index 432ddfa276a5..1b3d907e61de 100644 --- a/tests/init/neg/inherit-non-hot.check +++ b/tests/init/neg/inherit-non-hot.check @@ -1,7 +1,7 @@ -- Error: tests/init/neg/inherit-non-hot.scala:6:34 -------------------------------------------------------------------- 6 | if b == null then b = new B(this) // error | ^^^^^^^^^^^ - | Cannot prove that the value is fully initialized. May only assign fully initialized value. + | May only assign fully initialized value. | Calling trace: | -> val c = new C [ inherit-non-hot.scala:19 ] | ^^^^^ @@ -10,7 +10,7 @@ | -> val bAgain = toB.getBAgain [ inherit-non-hot.scala:16 ] | ^^^ | - | The unsafe promotion may cause the following problem: + | Promoting the value to fully initialized failed due to the following problem: | Call method Foo.B.this.aCopy.toB on a value with an unknown initialization. Calling trace: | -> val c = new C [ inherit-non-hot.scala:19 ] | ^^^^^ diff --git a/tests/init/neg/inlined-method.check b/tests/init/neg/inlined-method.check index 851e8dea40fe..d89fc7b2841b 100644 --- a/tests/init/neg/inlined-method.check +++ b/tests/init/neg/inlined-method.check @@ -1,6 +1,6 @@ -- Error: tests/init/neg/inlined-method.scala:8:45 --------------------------------------------------------------------- 8 | scala.runtime.Scala3RunTime.assertFailed(message) // error | ^^^^^^^ - |Cannot prove that the value is fully initialized. Only initialized values may be used as arguments. Calling trace: - |-> Assertion.failAssert(this) [ inlined-method.scala:2 ] - | ^^^^^^^^^^^^^^^^^^^^^^^^^^ + | Cannot prove the argument is fully initialized. Calling trace: + | -> Assertion.failAssert(this) [ inlined-method.scala:2 ] + | ^^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/tests/init/neg/local-warm4.check b/tests/init/neg/local-warm4.check index 22265d1f4d61..b2b78b0076bc 100644 --- a/tests/init/neg/local-warm4.check +++ b/tests/init/neg/local-warm4.check @@ -1,18 +1,18 @@ -- Error: tests/init/neg/local-warm4.scala:18:20 ----------------------------------------------------------------------- 18 | a = newA // error | ^^^^ - | Cannot prove that the value is fully initialized. May only assign fully initialized value. Calling trace: - | -> val a = new A(5) [ local-warm4.scala:26 ] - | ^^^^^^^^ - | -> class A(x: Int) extends Foo(x) { [ local-warm4.scala:6 ] - | ^ - | -> val b = new B(y) [ local-warm4.scala:10 ] - | ^^^^^^^^ - | -> class B(x: Int) extends A(x) { [ local-warm4.scala:13 ] - | ^ - | -> class A(x: Int) extends Foo(x) { [ local-warm4.scala:6 ] - | ^ - | -> increment() [ local-warm4.scala:9 ] - | ^^^^^^^^^^^ - | -> updateA() [ local-warm4.scala:21 ] - | ^^^^^^^^^ + | May only assign fully initialized value. Calling trace: + | -> val a = new A(5) [ local-warm4.scala:26 ] + | ^^^^^^^^ + | -> class A(x: Int) extends Foo(x) { [ local-warm4.scala:6 ] + | ^ + | -> val b = new B(y) [ local-warm4.scala:10 ] + | ^^^^^^^^ + | -> class B(x: Int) extends A(x) { [ local-warm4.scala:13 ] + | ^ + | -> class A(x: Int) extends Foo(x) { [ local-warm4.scala:6 ] + | ^ + | -> increment() [ local-warm4.scala:9 ] + | ^^^^^^^^^^^ + | -> updateA() [ local-warm4.scala:21 ] + | ^^^^^^^^^ diff --git a/tests/init/neg/promotion-loop.check b/tests/init/neg/promotion-loop.check index b53dd676081f..d92eff2d4173 100644 --- a/tests/init/neg/promotion-loop.check +++ b/tests/init/neg/promotion-loop.check @@ -1,7 +1,7 @@ -- Error: tests/init/neg/promotion-loop.scala:16:10 -------------------------------------------------------------------- 16 | println(b) // error | ^ - | Cannot prove that the value is fully initialized. Only initialized values may be used as arguments. + | Cannot prove the argument is fully initialized. | - | The unsafe promotion may cause the following problem: - | Cannot prove that the value is fully initialized. Only initialized values may be used as arguments. + | Promoting the value to fully initialized failed due to the following problem: + | Cannot prove that the field val outer is fully initialized. diff --git a/tests/init/neg/t3273.check b/tests/init/neg/t3273.check index 8c7f0f32e2d8..7f0ba4768618 100644 --- a/tests/init/neg/t3273.check +++ b/tests/init/neg/t3273.check @@ -1,18 +1,18 @@ -- Error: tests/init/neg/t3273.scala:4:42 ------------------------------------------------------------------------------ 4 | val num1: LazyList[Int] = 1 #:: num1.map(_ + 1) // error | ^^^^^^^^^^^^^^^ - | Cannot prove that the value is fully initialized. Only initialized values may be used as arguments. + | Cannot prove the argument is fully initialized. | - | The unsafe promotion may cause the following problem: + | Promoting the value to fully initialized failed due to the following problem: | Access non-initialized value num1. Calling trace: | -> val num1: LazyList[Int] = 1 #:: num1.map(_ + 1) // error [ t3273.scala:4 ] | ^^^^ -- Error: tests/init/neg/t3273.scala:5:61 ------------------------------------------------------------------------------ 5 | val num2: LazyList[Int] = 1 #:: num2.iterator.map(_ + 1).to(LazyList) // error | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | Cannot prove that the value is fully initialized. Only initialized values may be used as arguments. + | Cannot prove the argument is fully initialized. | - | The unsafe promotion may cause the following problem: + | Promoting the value to fully initialized failed due to the following problem: | Access non-initialized value num2. Calling trace: | -> val num2: LazyList[Int] = 1 #:: num2.iterator.map(_ + 1).to(LazyList) // error [ t3273.scala:5 ] | ^^^^ From 9a9db23fe14939abdfd759acba1cd7a37830356b Mon Sep 17 00:00:00 2001 From: Fengyun Liu Date: Mon, 23 May 2022 02:16:48 +0200 Subject: [PATCH 02/11] Refactor error reporting to stop on first error --- .../tools/dotc/transform/init/Semantic.scala | 469 +++++++++--------- tests/init/neg/default-this.check | 2 + tests/init/neg/inherit-non-hot.check | 32 +- tests/init/neg/promotion-loop.check | 4 +- 4 files changed, 248 insertions(+), 259 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala b/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala index 1abb46a175a3..4cc4a7005a97 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala @@ -301,7 +301,7 @@ object Semantic { * * Then, runs `fun` and update the caches if the values change. */ - def assume(value: Value, expr: Tree, cacheResult: Boolean)(fun: => Result): Contextual[Result] = + def assume(value: Value, expr: Tree, cacheResult: Boolean)(fun: => Value): Contextual[Value] = val assumeValue: Value = if last.contains(value, expr) then last.get(value, expr) @@ -312,10 +312,10 @@ object Semantic { current.put(value, expr, assumeValue) val actual = fun - if actual.value != assumeValue then + if actual != assumeValue then this.changed = true - last.put(value, expr, actual.value) - current.put(value, expr, actual.value) + last.put(value, expr, actual) + current.put(value, expr, actual) else // It's tempting to cache the value in stable, but it's unsound. // The reason is that the current value may depend on other values @@ -396,34 +396,48 @@ object Semantic { inline def cache(using c: Cache): Cache = c - /** Result of abstract interpretation */ - case class Result(value: Value, errors: Seq[Error]) { - def show(using Context) = value.show + ", errors = " + errors.map(_.toString) + /** Error reporting */ + trait Reporter: + def errors: List[Error] + def report(err: Error): Unit - def ++(errors: Seq[Error]): Result = this.copy(errors = this.errors ++ errors) + object Reporter: + class BufferReporter extends Reporter: + val buf = new mutable.ArrayBuffer[Error] + def errors = buf.toList + def report(err: Error) = buf += err - def +(error: Error): Result = this.copy(errors = this.errors :+ error) + class ErrorFound(val error: Error) extends Exception + class StopEarlyReporter extends Reporter: + def report(err: Error) = throw new ErrorFound(err) + def errors = ??? - def ensureHot(msg: String, source: Tree): Contextual[Result] = - this ++ value.promote(msg, source) + def fresh(): Reporter = new BufferReporter - def select(f: Symbol, source: Tree): Contextual[Result] = - value.select(f, source) ++ errors + /** Capture all errors and return as a list */ + def errorsIn(fn: Reporter ?=> Unit): List[Error] = + val reporter = Reporter.fresh() + fn(using reporter) + reporter.errors.toList - def call(meth: Symbol, args: List[ArgInfo], receiver: Type, superType: Type, source: Tree): Contextual[Result] = - value.call(meth, args, receiver, superType, source) ++ errors + /** Stop on first found error */ + def stopEarly(fn: Reporter ?=> Unit): List[Error] = + // use promotion reporter to stop the analysis on the first error + val promotionReporter: Reporter = new StopEarlyReporter - def callConstructor(ctor: Symbol, args: List[ArgInfo], source: Tree): Contextual[Result] = - value.callConstructor(ctor, args, source) ++ errors + try + fn(using promotionReporter) + Nil + catch case ex: ErrorFound => + ex.error :: Nil - def instantiate(klass: ClassSymbol, ctor: Symbol, args: List[ArgInfo], source: Tree): Contextual[Result] = - value.instantiate(klass, ctor, args, source) ++ errors - } + + inline def reporter(using r: Reporter): Reporter = r // ----- Checker State ----------------------------------- /** The state that threads through the interpreter */ - type Contextual[T] = (Env, Context, Trace, Promoted, Cache) ?=> T + type Contextual[T] = (Env, Context, Trace, Promoted, Cache, Reporter) ?=> T // ----- Error Handling ----------------------------------- @@ -536,15 +550,20 @@ object Semantic { end extension extension (value: Value) - def select(field: Symbol, source: Tree, needResolve: Boolean = true): Contextual[Result] = log("select " + field.show + ", this = " + value, printer, (_: Result).show) { - if promoted.isCurrentObjectPromoted then Result(Hot, Nil) + def ensureHot(msg: String, source: Tree): Contextual[Value] = + value.promote(msg, source) + value + + def select(field: Symbol, source: Tree, needResolve: Boolean = true): Contextual[Value] = log("select " + field.show + ", this = " + value, printer, (_: Value).show) { + if promoted.isCurrentObjectPromoted then Hot else value match { case Hot => - Result(Hot, Errors.empty) + Hot case Cold => val error = AccessCold(field, source, trace.toVector) - Result(Hot, error :: Nil) + reporter.report(error) + Hot case ref: Ref => val target = if needResolve then resolve(ref.klass, field) else field @@ -556,7 +575,7 @@ object Semantic { else val obj = ref.objekt if obj.hasField(target) then - Result(obj.field(target), Nil) + obj.field(target) else if ref.isInstanceOf[Warm] then assert(obj.klass.isSubClass(target.owner)) if target.is(Flags.ParamAccessor) then @@ -564,31 +583,30 @@ object Semantic { // see tests/init/neg/trait2.scala // // return `Hot` here, errors are reported in checking `ThisRef` - Result(Hot, Nil) + Hot else if target.hasSource then val rhs = target.defTree.asInstanceOf[ValOrDefDef].rhs eval(rhs, ref, target.owner.asClass, cacheResult = true) else val error = CallUnknown(field, source, trace.toVector) - Result(Hot, error :: Nil) + reporter.report(error) + Hot else val error = AccessNonInit(target, trace.add(source).toVector) - Result(Hot, error :: Nil) + reporter.report(error) + Hot case fun: Fun => report.error("unexpected tree in selecting a function, fun = " + fun.expr.show, source) - Result(Hot, Nil) + Hot case RefSet(refs) => - val resList = refs.map(_.select(field, source)) - val value2 = resList.map(_.value).join - val errors = resList.flatMap(_.errors) - Result(value2, errors) + refs.map(_.select(field, source)).join } } - def call(meth: Symbol, args: List[ArgInfo], receiver: Type, superType: Type, source: Tree, needResolve: Boolean = true): Contextual[Result] = log("call " + meth.show + ", args = " + args, printer, (_: Result).show) { - def checkArgs = args.flatMap(_.promote) + def call(meth: Symbol, args: List[ArgInfo], receiver: Type, superType: Type, source: Tree, needResolve: Boolean = true): Contextual[Value] = log("call " + meth.show + ", args = " + args, printer, (_: Value).show) { + def checkArgs = args.foreach(_.promote) def isSyntheticApply(meth: Symbol) = meth.is(Flags.Synthetic) @@ -605,7 +623,7 @@ object Semantic { var allArgsPromote = true val allParamTypes = methodType.paramInfoss.flatten.map(_.repeatedToSingle) val errors = allParamTypes.zip(args).flatMap { (info, arg) => - val errors = arg.promote + val errors = Reporter.errorsIn { arg.promote } allArgsPromote = allArgsPromote && errors.isEmpty info match case typeParamRef: TypeParamRef => @@ -622,9 +640,9 @@ object Semantic { (errors, allArgsPromote) // fast track if the current object is already initialized - if promoted.isCurrentObjectPromoted then Result(Hot, Nil) - else if isAlwaysSafe(meth) then Result(Hot, Nil) - else if meth eq defn.Any_asInstanceOf then Result(value, Nil) + if promoted.isCurrentObjectPromoted then Hot + else if isAlwaysSafe(meth) then Hot + else if meth eq defn.Any_asInstanceOf then value else value match { case Hot => if isSyntheticApply(meth) then @@ -633,16 +651,22 @@ object Semantic { else if receiver.typeSymbol.isStaticOwner then val (errors, allArgsPromote) = checkArgsWithParametricity() - if allArgsPromote || errors.nonEmpty then - Result(Hot, errors) + if allArgsPromote then + Hot + else if errors.nonEmpty then + for error <- errors do reporter.report(error) + Hot else - Result(Cold, errors) + Cold else - Result(Hot, checkArgs) + checkArgs + Hot case Cold => + checkArgs val error = CallCold(meth, source, trace.toVector) - Result(Hot, error :: checkArgs) + reporter.report(error) + Hot case ref: Ref => val isLocal = !meth.owner.isClass @@ -660,7 +684,7 @@ object Semantic { given Trace = trace1 val cls = target.owner.enclosingClass.asClass val ddef = target.defTree.asInstanceOf[DefDef] - val argErrors = checkArgs + val argErrors = Reporter.errorsIn { args.foreach(_.promote) } // normal method call if argErrors.nonEmpty && isSyntheticApply(meth) then val klass = meth.owner.companionClass.asClass @@ -668,40 +692,41 @@ object Semantic { val outer = resolveOuterSelect(outerCls, ref, 1, source) outer.instantiate(klass, klass.primaryConstructor, args, source) else + for error <- argErrors do reporter.report(error) withEnv(if isLocal then env else Env.empty) { - eval(ddef.rhs, ref, cls, cacheResult = true) ++ argErrors + eval(ddef.rhs, ref, cls, cacheResult = true) } else if ref.canIgnoreMethodCall(target) then - Result(Hot, Nil) + Hot else // no source code available + checkArgs val error = CallUnknown(target, source, trace.toVector) - Result(Hot, error :: checkArgs) + reporter.report(error) + Hot else // method call resolves to a field val obj = ref.objekt if obj.hasField(target) then - Result(obj.field(target), Nil) + obj.field(target) else value.select(target, source, needResolve = false) case Fun(body, thisV, klass, env) => // meth == NoSymbol for poly functions - if meth.name.toString == "tupled" then Result(value, Nil) // a call like `fun.tupled` + if meth.name.toString == "tupled" then value // a call like `fun.tupled` else + checkArgs withEnv(env) { - eval(body, thisV, klass, cacheResult = true) ++ checkArgs + eval(body, thisV, klass, cacheResult = true) } case RefSet(refs) => - val resList = refs.map(_.call(meth, args, receiver, superType, source)) - val value2 = resList.map(_.value).join - val errors = resList.flatMap(_.errors) - Result(value2, errors) + refs.map(_.call(meth, args, receiver, superType, source)).join } } - def callConstructor(ctor: Symbol, args: List[ArgInfo], source: Tree): Contextual[Result] = log("call " + ctor.show + ", args = " + args, printer, (_: Result).show) { + def callConstructor(ctor: Symbol, args: List[ArgInfo], source: Tree): Contextual[Value] = log("call " + ctor.show + ", args = " + args, printer, (_: Value).show) { // init "fake" param fields for the secondary constructor def addParamsAsFields(env: Env, ref: Ref, ctorDef: DefDef) = { val paramSyms = ctorDef.termParamss.flatten.map(_.symbol) @@ -714,7 +739,7 @@ object Semantic { value match { case Hot | Cold | _: RefSet | _: Fun => report.error("unexpected constructor call, meth = " + ctor + ", value = " + value, source) - Result(Hot, Nil) + Hot case ref: Warm if ref.isPopulatingParams => val trace1 = trace.add(source) @@ -734,7 +759,7 @@ object Semantic { eval(initCall, ref, cls) end if else - Result(Hot, Nil) + Hot case ref: Ref => val trace1 = trace.add(source) @@ -745,47 +770,50 @@ object Semantic { given Env = Env(ddef, args.map(_.value).widenArgs) if ctor.isPrimaryConstructor then val tpl = cls.defTree.asInstanceOf[TypeDef].rhs.asInstanceOf[Template] - val res = withTrace(trace.add(cls.defTree)) { eval(tpl, ref, cls, cacheResult = true) } - Result(ref, res.errors) + withTrace(trace.add(cls.defTree)) { eval(tpl, ref, cls, cacheResult = true) } + ref else addParamsAsFields(env, ref, ddef) eval(ddef.rhs, ref, cls, cacheResult = true) else if ref.canIgnoreMethodCall(ctor) then - Result(Hot, Nil) + Hot else // no source code available val error = CallUnknown(ctor, source, trace.toVector) - Result(Hot, error :: Nil) + reporter.report(error) + Hot } } /** Handle a new expression `new p.C` where `p` is abstracted by `value` */ - def instantiate(klass: ClassSymbol, ctor: Symbol, args: List[ArgInfo], source: Tree): Contextual[Result] = log("instantiating " + klass.show + ", value = " + value + ", args = " + args, printer, (_: Result).show) { + def instantiate(klass: ClassSymbol, ctor: Symbol, args: List[ArgInfo], source: Tree): Contextual[Value] = log("instantiating " + klass.show + ", value = " + value + ", args = " + args, printer, (_: Value).show) { val trace1 = trace.add(source) - if promoted.isCurrentObjectPromoted then Result(Hot, Nil) + if promoted.isCurrentObjectPromoted then Hot else value match { case Hot => val buffer = new mutable.ArrayBuffer[Error] val args2 = args.map { arg => - val errors = arg.promote - buffer ++= errors - if errors.isEmpty then Hot + given reporter: Reporter = Reporter.fresh() + arg.promote + buffer ++= reporter.errors + if reporter.errors.isEmpty then Hot else arg.value.widenArg } if buffer.isEmpty then - Result(Hot, Errors.empty) + Hot else val outer = Hot val warm = Warm(klass, outer, ctor, args2).ensureObjectExists() val argInfos2 = args.zip(args2).map { (argInfo, v) => argInfo.copy(value = v) } - val res = warm.callConstructor(ctor, argInfos2, source) - Result(warm, res.errors) + warm.callConstructor(ctor, argInfos2, source) + warm case Cold => val error = CallCold(ctor, source, trace1.toVector) - Result(Hot, error :: Nil) + reporter.report(error) + Hot case ref: Ref => given Trace = trace1 @@ -799,65 +827,60 @@ object Semantic { val argsWidened = args.map(_.value).widenArgs val argInfos2 = args.zip(argsWidened).map { (argInfo, v) => argInfo.copy(value = v) } val warm = Warm(klass, outer, ctor, argsWidened).ensureObjectExists() - val res = warm.callConstructor(ctor, argInfos2, source) - Result(warm, res.errors) + warm.callConstructor(ctor, argInfos2, source) + warm case Fun(body, thisV, klass, env) => report.error("unexpected tree in instantiating a function, fun = " + body.show, source) - Result(Hot, Nil) + Hot case RefSet(refs) => - val resList = refs.map(_.instantiate(klass, ctor, args, source)) - val value2 = resList.map(_.value).join - val errors = resList.flatMap(_.errors) - Result(value2, errors) + refs.map(_.instantiate(klass, ctor, args, source)).join } } end extension extension (ref: Ref) - def accessLocal(tmref: TermRef, klass: ClassSymbol, source: Tree): Contextual[Result] = + def accessLocal(tmref: TermRef, klass: ClassSymbol, source: Tree): Contextual[Value] = val sym = tmref.symbol - def default() = Result(Hot, Nil) - if sym.is(Flags.Param) && sym.owner.isConstructor then // if we can get the field from the Ref (which can only possibly be // a secondary constructor parameter), then use it. if (ref.objekt.hasField(sym)) - Result(ref.objekt.field(sym), Errors.empty) + ref.objekt.field(sym) // instances of local classes inside secondary constructors cannot // reach here, as those values are abstracted by Cold instead of Warm. // This enables us to simplify the domain without sacrificing // expressiveness nor soundess, as local classes inside secondary // constructors are uncommon. else if sym.isContainedIn(klass) then - Result(env.lookup(sym), Nil) + env.lookup(sym) else // We don't know much about secondary constructor parameters in outer scope. // It's always safe to approximate them with `Cold`. - Result(Cold, Nil) + Cold else if sym.is(Flags.Param) then - default() + Hot else sym.defTree match { case vdef: ValDef => // resolve this for local variable val enclosingClass = sym.owner.enclosingClass.asClass val thisValue2 = resolveThis(enclosingClass, ref, klass, source) - thisValue2 match { - case Hot => Result(Hot, Errors.empty) + thisValue2 match + case Hot => Hot - case Cold => Result(Cold, Nil) + case Cold => Cold case ref: Ref => eval(vdef.rhs, ref, enclosingClass) case _ => report.error("unexpected defTree when accessing local variable, sym = " + sym.show + ", defTree = " + sym.defTree.show, source) - default() - } + Hot + end match - case _ => default() + case _ => Hot } end extension @@ -891,7 +914,7 @@ object Semantic { end extension extension (thisRef: ThisRef) - def tryPromoteCurrentObject: Contextual[Boolean] = log("tryPromoteCurrentObject ", printer) { + def tryPromoteCurrentObject(): Contextual[Boolean] = log("tryPromoteCurrentObject ", printer) { if promoted.isCurrentObjectPromoted then true else if thisRef.isFullyFilled then @@ -904,41 +927,39 @@ object Semantic { extension (value: Value) /** Promotion of values to hot */ - def promote(msg: String, source: Tree): Contextual[List[Error]] = log("promoting " + value + ", promoted = " + promoted, printer) { + def promote(msg: String, source: Tree): Contextual[Unit] = log("promoting " + value + ", promoted = " + promoted, printer) { if promoted.isCurrentObjectPromoted then Nil else value.match - case Hot => Nil + case Hot => - case Cold => PromoteError(msg, source, trace.toVector) :: Nil + case Cold => + reporter.report(PromoteError(msg, source, trace.toVector)) case thisRef: ThisRef => - if thisRef.tryPromoteCurrentObject then Nil - else PromoteError(msg, source, trace.toVector) :: Nil + if !thisRef.tryPromoteCurrentObject() then + reporter.report(PromoteError(msg, source, trace.toVector)) case warm: Warm => - if promoted.contains(warm) then Nil - else + if !promoted.contains(warm) then promoted.add(warm) val errors = warm.tryPromote(msg, source) if errors.nonEmpty then promoted.remove(warm) - errors + for error <- errors do reporter.report(error) case fun @ Fun(body, thisV, klass, env) => - if promoted.contains(fun) then Nil - else - val res = withEnv(env) { eval(body, thisV, klass) } - val errors = - if res.value == Hot then res.errors - else res.errors :+ PromoteError("The function return value is not fully initialized.", source, trace.toVector) - if (res.errors.nonEmpty || res.value != Hot) - UnsafePromotion(msg, source, trace.toVector, errors) :: Nil + if !promoted.contains(fun) then + val errors = Reporter.stopEarly { + val res = withEnv(env) { eval(body, thisV, klass) } + res.promote("The function return value is not fully initialized.", source) + } + if (errors.nonEmpty) + reporter.report(UnsafePromotion(msg, source, trace.toVector, errors)) else promoted.add(fun) - Nil case RefSet(refs) => - refs.flatMap(_.promote(msg, source)) + refs.foreach(_.promote(msg, source)) } end extension @@ -965,32 +986,26 @@ object Semantic { if classRef.memberClasses.nonEmpty || !warm.isFullyFilled then return PromoteError(msg, source, trace.toVector) :: Nil - val buffer = new mutable.ArrayBuffer[Error] - - def checkHot(res: Result, msg: String, source: Tree): Errors = - if res.value == Hot || promoted.contains(res.value) then res.errors - else res.errors :+ PromoteError(msg, source, trace.toVector) - - warm.klass.baseClasses.exists { klass => - klass.hasSource && klass.info.decls.exists { member => - if !member.isType && !member.isConstructor && member.hasSource && !member.is(Flags.Deferred) then - if member.is(Flags.Method, butNot = Flags.Accessor) then - val trace2 = trace.add(source) - locally { - given Trace = trace2 - val args = member.info.paramInfoss.flatten.map(_ => ArgInfo(Hot, EmptyTree)) - val res = warm.call(member, args, receiver = NoType, superType = NoType, source = member.defTree) - buffer ++= checkHot(res, "Cannot prove that the return value of " + member + " is fully initialized.", source) - } - else - val res = warm.select(member, source) - buffer ++= checkHot(res, "Cannot prove that the field " + member + " is fully initialized.", source) - buffer.nonEmpty - } + val errors = Reporter.stopEarly { + for klass <- warm.klass.baseClasses if klass.hasSource do + for member <- klass.info.decls do + if !member.isType && !member.isConstructor && member.hasSource && !member.is(Flags.Deferred) then + given Trace = Trace.empty.add(source) + if member.is(Flags.Method, butNot = Flags.Accessor) then + locally { + val args = member.info.paramInfoss.flatten.map(_ => ArgInfo(Hot, EmptyTree)) + val res = warm.call(member, args, receiver = NoType, superType = NoType, source = member.defTree) + res.promote("Cannot prove that the return value of " + member + " is fully initialized.", source) + } + else + val res = warm.select(member, source) + res.promote("Cannot prove that the field " + member + " is fully initialized.", source) + end for + end for } - if buffer.isEmpty then Nil - else UnsafePromotion(msg, source, trace.toVector, buffer.toList) :: Nil + if errors.isEmpty then Nil + else UnsafePromotion(msg, source, trace.toVector, errors) :: Nil } end extension @@ -1036,12 +1051,13 @@ object Semantic { given Promoted = Promoted.empty given Trace = Trace.empty given Env = Env(paramValues) + given Reporter = Reporter.fresh() thisRef.ensureFresh() - val res = log("checking " + task) { eval(tpl, thisRef, thisRef.klass) } - res.errors.foreach(_.issue) + log("checking " + task) { eval(tpl, thisRef, thisRef.klass) } + reporter.errors.foreach(_.issue) - if cache.hasChanged && res.errors.isEmpty then + if cache.hasChanged && reporter.errors.isEmpty then // code to prepare cache and heap for next iteration cache.prepareForNextIteration() iterate() @@ -1081,7 +1097,7 @@ object Semantic { /** Utility definition used for better error-reporting of argument errors */ case class ArgInfo(value: Value, source: Tree) { - def promote: Contextual[List[Error]] = value.promote("Cannot prove the argument is fully initialized.", source) + def promote: Contextual[Unit] = value.promote("Cannot prove the argument is fully initialized.", source) } /** Evaluate an expression with the given value for `this` in a given class `klass` @@ -1099,41 +1115,38 @@ object Semantic { * * This method only handles cache logic and delegates the work to `cases`. */ - def eval(expr: Tree, thisV: Ref, klass: ClassSymbol, cacheResult: Boolean = false): Contextual[Result] = log("evaluating " + expr.show + ", this = " + thisV.show + " in " + klass.show, printer, (_: Result).show) { - if (cache.contains(thisV, expr)) Result(cache(thisV, expr), Errors.empty) + def eval(expr: Tree, thisV: Ref, klass: ClassSymbol, cacheResult: Boolean = false): Contextual[Value] = log("evaluating " + expr.show + ", this = " + thisV.show + " in " + klass.show, printer, (_: Value).show) { + if (cache.contains(thisV, expr)) cache(thisV, expr) else cache.assume(thisV, expr, cacheResult) { cases(expr, thisV, klass) } } /** Evaluate a list of expressions */ - def eval(exprs: List[Tree], thisV: Ref, klass: ClassSymbol): Contextual[List[Result]] = + def eval(exprs: List[Tree], thisV: Ref, klass: ClassSymbol): Contextual[List[Value]] = exprs.map { expr => eval(expr, thisV, klass) } /** Evaluate arguments of methods */ - def evalArgs(args: List[Arg], thisV: Ref, klass: ClassSymbol): Contextual[(List[Error], List[ArgInfo])] = - val errors = new mutable.ArrayBuffer[Error] + def evalArgs(args: List[Arg], thisV: Ref, klass: ClassSymbol): Contextual[List[ArgInfo]] = val argInfos = new mutable.ArrayBuffer[ArgInfo] args.foreach { arg => val res = if arg.isByName then - val fun = Fun(arg.tree, thisV, klass, env) - Result(fun, Nil) + Fun(arg.tree, thisV, klass, env) else eval(arg.tree, thisV, klass) - errors ++= res.errors - argInfos += ArgInfo(res.value, arg.tree) + argInfos += ArgInfo(res, arg.tree) } - (errors.toList, argInfos.toList) + argInfos.toList /** Handles the evaluation of different expressions * * Note: Recursive call should go to `eval` instead of `cases`. */ - def cases(expr: Tree, thisV: Ref, klass: ClassSymbol): Contextual[Result] = + def cases(expr: Tree, thisV: Ref, klass: ClassSymbol): Contextual[Value] = expr match { case Ident(nme.WILDCARD) => // TODO: disallow `var x: T = _` - Result(Hot, Errors.empty) + Hot case id @ Ident(name) if !id.symbol.is(Flags.Method) => assert(name.isTermName, "type trees should not reach here") @@ -1141,32 +1154,32 @@ object Semantic { case NewExpr(tref, New(tpt), ctor, argss) => // check args - val (errors, args) = evalArgs(argss.flatten, thisV, klass) + val args = evalArgs(argss.flatten, thisV, klass) val cls = tref.classSymbol.asClass - val res = outerValue(tref, thisV, klass, tpt) + val outer = outerValue(tref, thisV, klass, tpt) val trace2 = trace.add(expr) locally { given Trace = trace2 - (res ++ errors).instantiate(cls, ctor, args, source = expr) + outer.instantiate(cls, ctor, args, source = expr) } case Call(ref, argss) => // check args - val (errors, args) = evalArgs(argss.flatten, thisV, klass) + val args = evalArgs(argss.flatten, thisV, klass) ref match case Select(supert: Super, _) => val SuperType(thisTp, superTp) = supert.tpe: @unchecked val thisValue2 = resolveThis(thisTp.classSymbol.asClass, thisV, klass, ref) - Result(thisValue2, errors).call(ref.symbol, args, thisTp, superTp, expr) + thisValue2.call(ref.symbol, args, thisTp, superTp, expr) case Select(qual, _) => - val res = eval(qual, thisV, klass) ++ errors + val receiver = eval(qual, thisV, klass) if ref.symbol.isConstructor then - res.callConstructor(ref.symbol, args, source = expr) + receiver.callConstructor(ref.symbol, args, source = expr) else - res.call(ref.symbol, args, receiver = qual.tpe, superType = NoType, source = expr) + receiver.call(ref.symbol, args, receiver = qual.tpe, superType = NoType, source = expr) case id: Ident => id.tpe match @@ -1177,32 +1190,34 @@ object Semantic { // local methods are not a member, but we can reuse the method `call` thisValue2.call(id.symbol, args, receiver = NoType, superType = NoType, expr, needResolve = false) case TermRef(prefix, _) => - val res = cases(prefix, thisV, klass, id) ++ errors + val receiver = cases(prefix, thisV, klass, id) if id.symbol.isConstructor then - res.callConstructor(id.symbol, args, source = expr) + receiver.callConstructor(id.symbol, args, source = expr) else - res.call(id.symbol, args, receiver = prefix, superType = NoType, source = expr) + receiver.call(id.symbol, args, receiver = prefix, superType = NoType, source = expr) case Select(qualifier, name) => - val qualRes = eval(qualifier, thisV, klass) + val qual = eval(qualifier, thisV, klass) name match case OuterSelectName(_, hops) => val SkolemType(tp) = expr.tpe: @unchecked - val outer = resolveOuterSelect(tp.classSymbol.asClass, qualRes.value, hops, source = expr) - Result(outer, qualRes.errors) + resolveOuterSelect(tp.classSymbol.asClass, qual, hops, source = expr) case _ => - qualRes.select(expr.symbol, expr) + qual.select(expr.symbol, expr) case _: This => cases(expr.tpe, thisV, klass, expr) case Literal(_) => - Result(Hot, Errors.empty) + Hot case Typed(expr, tpt) => - if (tpt.tpe.hasAnnotation(defn.UncheckedAnnot)) Result(Hot, Errors.empty) - else eval(expr, thisV, klass) ++ checkTermUsage(tpt, thisV, klass) + if (tpt.tpe.hasAnnotation(defn.UncheckedAnnot)) + Hot + else + checkTermUsage(tpt, thisV, klass) + eval(expr, thisV, klass) case NamedArg(name, arg) => eval(arg, thisV, klass) @@ -1210,75 +1225,59 @@ object Semantic { case Assign(lhs, rhs) => lhs match case Select(qual, _) => - val res = eval(qual, thisV, klass) - eval(rhs, thisV, klass).ensureHot("May only assign fully initialized value.", rhs) ++ res.errors + eval(qual, thisV, klass) + eval(rhs, thisV, klass).ensureHot("May only assign fully initialized value.", rhs) case id: Ident => eval(rhs, thisV, klass).ensureHot("May only assign fully initialized value.", rhs) case closureDef(ddef) => - val value = Fun(ddef.rhs, thisV, klass, env) - Result(value, Nil) + Fun(ddef.rhs, thisV, klass, env) case PolyFun(body) => - val value = Fun(body, thisV, klass, env) - Result(value, Nil) + Fun(body, thisV, klass, env) case Block(stats, expr) => - val ress = eval(stats, thisV, klass) - eval(expr, thisV, klass) ++ ress.flatMap(_.errors) + eval(stats, thisV, klass) + eval(expr, thisV, klass) case If(cond, thenp, elsep) => - val ress = eval(cond :: thenp :: elsep :: Nil, thisV, klass) - val value = ress.map(_.value).join - val errors = ress.flatMap(_.errors) - Result(value, errors) + eval(cond :: thenp :: elsep :: Nil, thisV, klass).join case Annotated(arg, annot) => - if (expr.tpe.hasAnnotation(defn.UncheckedAnnot)) Result(Hot, Errors.empty) + if (expr.tpe.hasAnnotation(defn.UncheckedAnnot)) Hot else eval(arg, thisV, klass) case Match(selector, cases) => - val res1 = eval(selector, thisV, klass).ensureHot("The value to be matched needs to be fully 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) + eval(selector, thisV, klass).ensureHot("The value to be matched needs to be fully initialized.", selector) + eval(cases.map(_.body), thisV, klass).join case Return(expr, from) => eval(expr, thisV, klass).ensureHot("return expression may only be initialized value.", expr) case WhileDo(cond, body) => - val ress = eval(cond :: body :: Nil, thisV, klass) - Result(Hot, ress.flatMap(_.errors)) + eval(cond :: body :: Nil, thisV, klass) + Hot case Labeled(_, expr) => eval(expr, thisV, klass) case Try(block, cases, finalizer) => - val res1 = eval(block, thisV, klass) - 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 - val res2 = eval(finalizer, thisV, klass) - Result(resValue, res1.errors ++ errors ++ res2.errors) + eval(block, thisV, klass) + if !finalizer.isEmpty then + eval(finalizer, thisV, klass) + eval(cases.map(_.body), thisV, klass).join case SeqLiteral(elems, elemtpt) => - val ress = elems.map { elem => - eval(elem, thisV, klass) - } - Result(ress.map(_.value).join, ress.flatMap(_.errors)) + elems.map { elem => eval(elem, thisV, klass) }.join case Inlined(call, bindings, expansion) => val trace1 = trace.add(expr) - val ress = eval(bindings, thisV, klass) - withTrace(trace1)(eval(expansion, thisV, klass)) ++ ress.flatMap(_.errors) + eval(bindings, thisV, klass) + withTrace(trace1)(eval(expansion, thisV, klass)) case Thicket(List()) => // possible in try/catch/finally, see tests/crash/i6914.scala - Result(Hot, Errors.empty) + Hot case vdef : ValDef => // local val definition @@ -1287,28 +1286,31 @@ object Semantic { case ddef : DefDef => // local method - Result(Hot, Errors.empty) + Hot case tdef: TypeDef => // local type definition - if tdef.isClassDef then Result(Hot, Errors.empty) - else Result(Hot, checkTermUsage(tdef.rhs, thisV, klass)) + if tdef.isClassDef then + Hot + else + checkTermUsage(tdef.rhs, thisV, klass) + Hot case tpl: Template => init(tpl, thisV, klass) case _: Import | _: Export => - Result(Hot, Errors.empty) + Hot case _ => throw new Exception("unexpected tree: " + expr.show) } /** Handle semantics of leaf nodes */ - def cases(tp: Type, thisV: Ref, klass: ClassSymbol, source: Tree): Contextual[Result] = log("evaluating " + tp.show, printer, (_: Result).show) { + def cases(tp: Type, thisV: Ref, klass: ClassSymbol, source: Tree): Contextual[Value] = log("evaluating " + tp.show, printer, (_: Value).show) { tp match { case _: ConstantType => - Result(Hot, Errors.empty) + Hot case tmref: TermRef if tmref.prefix == NoPrefix => thisV.accessLocal(tmref, klass, source) @@ -1320,14 +1322,14 @@ object Semantic { val cls = tref.classSymbol.asClass if cls.isStaticOwner && !klass.isContainedIn(cls) then // O.this outside the body of the object O - Result(Hot, Nil) + Hot else val value = resolveThis(cls, thisV, klass, source) - Result(value, Errors.empty) + value case _: TermParamRef | _: RecThis => // possible from checking effects of types - Result(Hot, Errors.empty) + Hot case _ => throw new Exception("unexpected type: " + tp) @@ -1395,20 +1397,18 @@ object Semantic { } /** Compute the outer value that correspond to `tref.prefix` */ - def outerValue(tref: TypeRef, thisV: Ref, klass: ClassSymbol, source: Tree): Contextual[Result] = + def outerValue(tref: TypeRef, thisV: Ref, klass: ClassSymbol, source: Tree): Contextual[Value] = val cls = tref.classSymbol.asClass if tref.prefix == NoPrefix then val enclosing = cls.owner.lexicallyEnclosingClass.asClass val outerV = resolveThis(enclosing, thisV, klass, source) - Result(outerV, Errors.empty) + outerV else - if cls.isAllOf(Flags.JavaInterface) then Result(Hot, Nil) + if cls.isAllOf(Flags.JavaInterface) then Hot else cases(tref.prefix, thisV, klass, source) /** Initialize part of an abstract object in `klass` of the inheritance chain */ - def init(tpl: Template, thisV: Ref, klass: ClassSymbol): Contextual[Result] = log("init " + klass.show, printer, (_: Result).show) { - val errorBuffer = new mutable.ArrayBuffer[Error] - + def init(tpl: Template, thisV: Ref, klass: ClassSymbol): Contextual[Value] = log("init " + klass.show, printer, (_: Value).show) { val paramsMap = tpl.constr.termParamss.flatten.map { vdef => vdef.name -> env.lookup(vdef.symbol) }.toMap @@ -1427,29 +1427,25 @@ object Semantic { val cls = tref.classSymbol.asClass // update outer for super class val res = outerValue(tref, thisV, klass, source) - errorBuffer ++= res.errors - thisV.updateOuter(cls, res.value) + thisV.updateOuter(cls, res) // follow constructor if cls.hasSource then tasks.append { () => printer.println("init super class " + cls.show) - val res2 = thisV.callConstructor(ctor, args, source) - errorBuffer ++= res2.errors + thisV.callConstructor(ctor, args, source) () } // parents def initParent(parent: Tree, tasks: Tasks)(using Env) = parent match { case tree @ Block(stats, NewExpr(tref, New(tpt), ctor, argss)) => // can happen - eval(stats, thisV, klass).foreach { res => errorBuffer ++= res.errors } - val (errors, args) = evalArgs(argss.flatten, thisV, klass) - errorBuffer ++= errors + eval(stats, thisV, klass) + val args = evalArgs(argss.flatten, thisV, klass) superCall(tref, ctor, args, tree, tasks) case tree @ NewExpr(tref, New(tpt), ctor, argss) => // extends A(args) - val (errors, args) = evalArgs(argss.flatten, thisV, klass) - errorBuffer ++= errors + val args = evalArgs(argss.flatten, thisV, klass) superCall(tref, ctor, args, tree, tasks) case _ => // extends A or extends A[T] @@ -1506,40 +1502,37 @@ object Semantic { case vdef : ValDef if !vdef.symbol.is(Flags.Lazy) && !vdef.rhs.isEmpty => given Env = Env.empty val res = eval(vdef.rhs, thisV, klass) - errorBuffer ++= res.errors - thisV.updateField(vdef.symbol, res.value) + thisV.updateField(vdef.symbol, res) fieldsChanged = true case _: MemberDef => case tree => - if fieldsChanged && thisV.isThisRef then thisV.asInstanceOf[ThisRef].tryPromoteCurrentObject + if fieldsChanged && thisV.isThisRef then thisV.asInstanceOf[ThisRef].tryPromoteCurrentObject() fieldsChanged = false given Env = Env.empty - errorBuffer ++= eval(tree, thisV, klass).errors + eval(tree, thisV, klass) } // The result value is ignored, use Hot to avoid futile fixed point computation - Result(Hot, errorBuffer.toList) + Hot } /** Check that path in path-dependent types are initialized * * This is intended to avoid type soundness issues in Dotty. */ - def checkTermUsage(tpt: Tree, thisV: Ref, klass: ClassSymbol): Contextual[List[Error]] = - val buf = new mutable.ArrayBuffer[Error] + def checkTermUsage(tpt: Tree, thisV: Ref, klass: ClassSymbol): Contextual[Unit] = val traverser = new TypeTraverser { def traverse(tp: Type): Unit = tp match { case TermRef(_: SingletonType, _) => - buf ++= cases(tp, thisV, klass, tpt).errors + cases(tp, thisV, klass, tpt) case _ => traverseChildren(tp) } } traverser.traverse(tpt.tpe) - buf.toList // ----- Utility methods and extractors -------------------------------- diff --git a/tests/init/neg/default-this.check b/tests/init/neg/default-this.check index fd111c559819..8cb2860d1ce4 100644 --- a/tests/init/neg/default-this.check +++ b/tests/init/neg/default-this.check @@ -4,3 +4,5 @@ | Cannot prove the argument is fully initialized. Calling trace: | -> val result = updateThenCompare(5) [ default-this.scala:11 ] | ^^^^^^^^^^^^^^^^^^^^ + | -> compare() // error [ default-this.scala:9 ] + | ^^^^^^^^^ diff --git a/tests/init/neg/inherit-non-hot.check b/tests/init/neg/inherit-non-hot.check index 1b3d907e61de..2fbb506dd29b 100644 --- a/tests/init/neg/inherit-non-hot.check +++ b/tests/init/neg/inherit-non-hot.check @@ -1,24 +1,16 @@ -- Error: tests/init/neg/inherit-non-hot.scala:6:34 -------------------------------------------------------------------- 6 | if b == null then b = new B(this) // error | ^^^^^^^^^^^ - | May only assign fully initialized value. - | Calling trace: - | -> val c = new C [ inherit-non-hot.scala:19 ] - | ^^^^^ - | -> class C extends A { [ inherit-non-hot.scala:15 ] - | ^ - | -> val bAgain = toB.getBAgain [ inherit-non-hot.scala:16 ] - | ^^^ + | May only assign fully initialized value. + | Calling trace: + | -> val c = new C [ inherit-non-hot.scala:19 ] + | ^^^^^ + | -> class C extends A { [ inherit-non-hot.scala:15 ] + | ^ + | -> val bAgain = toB.getBAgain [ inherit-non-hot.scala:16 ] + | ^^^ | - | Promoting the value to fully initialized failed due to the following problem: - | Call method Foo.B.this.aCopy.toB on a value with an unknown initialization. Calling trace: - | -> val c = new C [ inherit-non-hot.scala:19 ] - | ^^^^^ - | -> class C extends A { [ inherit-non-hot.scala:15 ] - | ^ - | -> val bAgain = toB.getBAgain [ inherit-non-hot.scala:16 ] - | ^^^ - | -> if b == null then b = new B(this) // error [ inherit-non-hot.scala:6 ] - | ^^^^^^^^^^^ - | -> def getBAgain: B = aCopy.toB [ inherit-non-hot.scala:12 ] - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + | Promoting the value to fully initialized failed due to the following problem: + | Cannot prove that the field val a is fully initialized. Calling trace: + | -> if b == null then b = new B(this) // error [ inherit-non-hot.scala:6 ] + | ^^^^^^^^^^^ diff --git a/tests/init/neg/promotion-loop.check b/tests/init/neg/promotion-loop.check index d92eff2d4173..90d48e4c3dba 100644 --- a/tests/init/neg/promotion-loop.check +++ b/tests/init/neg/promotion-loop.check @@ -4,4 +4,6 @@ | Cannot prove the argument is fully initialized. | | Promoting the value to fully initialized failed due to the following problem: - | Cannot prove that the field val outer is fully initialized. + | Cannot prove that the field val outer is fully initialized. Calling trace: + | -> println(b) // error [ promotion-loop.scala:16 ] + | ^ From c1b83f6836223f72e90fb6c40330c50a85f9df1b Mon Sep 17 00:00:00 2001 From: Fengyun Liu Date: Mon, 23 May 2022 02:16:48 +0200 Subject: [PATCH 03/11] Refine marker positions --- .../tools/dotc/transform/init/Errors.scala | 42 ++++++-------- .../tools/dotc/transform/init/Semantic.scala | 58 ++++++++++--------- tests/init/neg/closureLeak.check | 5 +- tests/init/neg/default-this.check | 2 +- tests/init/neg/inherit-non-hot.check | 5 +- tests/init/neg/inlined-method.check | 8 ++- tests/init/neg/local-warm4.check | 4 +- tests/init/neg/promotion-loop.check | 1 - tests/init/neg/t3273.check | 2 - 9 files changed, 63 insertions(+), 64 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/transform/init/Errors.scala b/compiler/src/dotty/tools/dotc/transform/init/Errors.scala index 219953c43f4c..aac600743efb 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Errors.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Errors.scala @@ -9,13 +9,9 @@ import util.SourcePosition import Decorators._, printing.SyntaxHighlighting import Types._, Symbols._, Contexts._ -object Errors { - type Errors = Seq[Error] - val empty: Errors = Nil - - def show(errs: Errors)(using Context): String = - errs.map(_.show).mkString(", ") +import scala.collection.mutable +object Errors: sealed trait Error { def source: Tree def trace: Seq[Tree] @@ -24,11 +20,12 @@ object Errors { def issue(using Context): Unit = report.warning(show + stacktrace, source.srcPos) - def toErrors: Errors = this :: Nil + private def isTraceInformative: Boolean = + trace.size > 1 || trace.size == 1 && trace.head.ne(source) - def stacktrace(using Context): String = if (trace.isEmpty) "" else " Calling trace:\n" + { - var last: String = "" - val sb = new StringBuilder + def stacktrace(using Context): String = if !isTraceInformative then "" else " Calling trace:\n" + { + var lastLineNum = -1 + var lines: mutable.ArrayBuffer[String] = new mutable.ArrayBuffer trace.foreach { tree => val pos = tree.sourcePos val prefix = "-> " @@ -44,10 +41,16 @@ object Errors { positionMarker(pos) else "" - if (last != line) sb.append(prefix + line + "\n" + positionMarkerLine ) + // always use the more precise trace location + if lastLineNum == pos.line then + lines.dropRightInPlace(1) - last = line + lines += (prefix + line + "\n" + positionMarkerLine) + + lastLineNum = pos.line } + val sb = new StringBuilder + for line <- lines do sb.append(line) sb.toString } @@ -65,13 +68,6 @@ object Errors { s"$padding$carets\n" } - /** Flatten UnsafePromotion errors - */ - def flatten: Errors = this match { - case unsafe: UnsafePromotion => unsafe.errors.flatMap(_.flatten) - case _ => this :: Nil - } - override def toString() = this.getClass.getName.nn } @@ -107,16 +103,14 @@ object Errors { } /** Promote a value under initialization to fully-initialized */ - case class UnsafePromotion(msg: String, source: Tree, trace: Seq[Tree], errors: Errors) extends Error { - assert(errors.nonEmpty) + case class UnsafePromotion(msg: String, source: Tree, trace: Seq[Tree], error: Error) extends Error { override def issue(using Context): Unit = report.warning(show, source.srcPos) def show(using Context): String = { var index = 0 - msg + "\n" + stacktrace + "\n" + + msg + stacktrace + "\n" + "Promoting the value to fully initialized failed due to the following problem:\n" + - errors.head.show + errors.head.stacktrace + error.show + error.stacktrace } } -} \ No newline at end of file diff --git a/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala b/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala index 4cc4a7005a97..b078b406bb09 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala @@ -928,38 +928,41 @@ object Semantic { extension (value: Value) /** Promotion of values to hot */ def promote(msg: String, source: Tree): Contextual[Unit] = log("promoting " + value + ", promoted = " + promoted, printer) { + val trace2 = trace.add(source) if promoted.isCurrentObjectPromoted then Nil else + given Trace = trace2 - value.match - case Hot => + value.match + case Hot => - case Cold => - reporter.report(PromoteError(msg, source, trace.toVector)) - - case thisRef: ThisRef => - if !thisRef.tryPromoteCurrentObject() then + case Cold => reporter.report(PromoteError(msg, source, trace.toVector)) - case warm: Warm => - if !promoted.contains(warm) then - promoted.add(warm) - val errors = warm.tryPromote(msg, source) - if errors.nonEmpty then promoted.remove(warm) - for error <- errors do reporter.report(error) - - case fun @ Fun(body, thisV, klass, env) => - if !promoted.contains(fun) then - val errors = Reporter.stopEarly { - val res = withEnv(env) { eval(body, thisV, klass) } - res.promote("The function return value is not fully initialized.", source) - } - if (errors.nonEmpty) - reporter.report(UnsafePromotion(msg, source, trace.toVector, errors)) - else - promoted.add(fun) + case thisRef: ThisRef => + if !thisRef.tryPromoteCurrentObject() then + reporter.report(PromoteError(msg, source, trace.toVector)) + + case warm: Warm => + if !promoted.contains(warm) then + promoted.add(warm) + val errors = warm.tryPromote(msg, source) + if errors.nonEmpty then promoted.remove(warm) + for error <- errors do reporter.report(error) + + case fun @ Fun(body, thisV, klass, env) => + if !promoted.contains(fun) then + val errors = Reporter.stopEarly { + given Trace = Trace.empty.add(body) + val res = withEnv(env) { eval(body, thisV, klass) } + res.promote("The function return value is not fully initialized.", body) + } + if (errors.nonEmpty) + reporter.report(UnsafePromotion(msg, source, trace.toVector, errors.head)) + else + promoted.add(fun) - case RefSet(refs) => - refs.foreach(_.promote(msg, source)) + case RefSet(refs) => + refs.foreach(_.promote(msg, source)) } end extension @@ -1005,7 +1008,7 @@ object Semantic { } if errors.isEmpty then Nil - else UnsafePromotion(msg, source, trace.toVector, errors) :: Nil + else UnsafePromotion(msg, source, trace.toVector, errors.head) :: Nil } end extension @@ -1281,7 +1284,6 @@ object Semantic { case vdef : ValDef => // local val definition - // TODO: support explicit @cold annotation for local definitions eval(vdef.rhs, thisV, klass) case ddef : DefDef => diff --git a/tests/init/neg/closureLeak.check b/tests/init/neg/closureLeak.check index 12d0316bb3b0..b6459ff2f4d6 100644 --- a/tests/init/neg/closureLeak.check +++ b/tests/init/neg/closureLeak.check @@ -2,6 +2,7 @@ 11 | l.foreach(a => a.addX(this)) // error | ^^^^^^^^^^^^^^^^^ | Cannot prove the argument is fully initialized. - | | Promoting the value to fully initialized failed due to the following problem: - | Cannot prove the argument is fully initialized. + | Cannot prove the argument is fully initialized. Calling trace: + | -> l.foreach(a => a.addX(this)) // error [ closureLeak.scala:11 ] + | ^^^^ diff --git a/tests/init/neg/default-this.check b/tests/init/neg/default-this.check index 8cb2860d1ce4..734da0aebf71 100644 --- a/tests/init/neg/default-this.check +++ b/tests/init/neg/default-this.check @@ -5,4 +5,4 @@ | -> val result = updateThenCompare(5) [ default-this.scala:11 ] | ^^^^^^^^^^^^^^^^^^^^ | -> compare() // error [ default-this.scala:9 ] - | ^^^^^^^^^ + | ^^^^^^^ diff --git a/tests/init/neg/inherit-non-hot.check b/tests/init/neg/inherit-non-hot.check index 2fbb506dd29b..1158b2506a2c 100644 --- a/tests/init/neg/inherit-non-hot.check +++ b/tests/init/neg/inherit-non-hot.check @@ -1,14 +1,15 @@ -- Error: tests/init/neg/inherit-non-hot.scala:6:34 -------------------------------------------------------------------- 6 | if b == null then b = new B(this) // error | ^^^^^^^^^^^ - | May only assign fully initialized value. - | Calling trace: + | May only assign fully initialized value. Calling trace: | -> val c = new C [ inherit-non-hot.scala:19 ] | ^^^^^ | -> class C extends A { [ inherit-non-hot.scala:15 ] | ^ | -> val bAgain = toB.getBAgain [ inherit-non-hot.scala:16 ] | ^^^ + | -> if b == null then b = new B(this) // error [ inherit-non-hot.scala:6 ] + | ^^^^^^^^^^^ | | Promoting the value to fully initialized failed due to the following problem: | Cannot prove that the field val a is fully initialized. Calling trace: diff --git a/tests/init/neg/inlined-method.check b/tests/init/neg/inlined-method.check index d89fc7b2841b..9dca5c1fa4c2 100644 --- a/tests/init/neg/inlined-method.check +++ b/tests/init/neg/inlined-method.check @@ -1,6 +1,8 @@ -- Error: tests/init/neg/inlined-method.scala:8:45 --------------------------------------------------------------------- 8 | scala.runtime.Scala3RunTime.assertFailed(message) // error | ^^^^^^^ - | Cannot prove the argument is fully initialized. Calling trace: - | -> Assertion.failAssert(this) [ inlined-method.scala:2 ] - | ^^^^^^^^^^^^^^^^^^^^^^^^^^ + | Cannot prove the argument is fully initialized. Calling trace: + | -> Assertion.failAssert(this) [ inlined-method.scala:2 ] + | ^^^^^^^^^^^^^^^^^^^^^^^^^^ + | -> scala.runtime.Scala3RunTime.assertFailed(message) // error [ inlined-method.scala:8 ] + | ^^^^^^^ diff --git a/tests/init/neg/local-warm4.check b/tests/init/neg/local-warm4.check index b2b78b0076bc..4d46d4758401 100644 --- a/tests/init/neg/local-warm4.check +++ b/tests/init/neg/local-warm4.check @@ -9,10 +9,12 @@ | -> val b = new B(y) [ local-warm4.scala:10 ] | ^^^^^^^^ | -> class B(x: Int) extends A(x) { [ local-warm4.scala:13 ] - | ^ + | ^^^^ | -> class A(x: Int) extends Foo(x) { [ local-warm4.scala:6 ] | ^ | -> increment() [ local-warm4.scala:9 ] | ^^^^^^^^^^^ | -> updateA() [ local-warm4.scala:21 ] | ^^^^^^^^^ + | -> a = newA // error [ local-warm4.scala:18 ] + | ^^^^ diff --git a/tests/init/neg/promotion-loop.check b/tests/init/neg/promotion-loop.check index 90d48e4c3dba..05d677002bef 100644 --- a/tests/init/neg/promotion-loop.check +++ b/tests/init/neg/promotion-loop.check @@ -2,7 +2,6 @@ 16 | println(b) // error | ^ | Cannot prove the argument is fully initialized. - | | Promoting the value to fully initialized failed due to the following problem: | Cannot prove that the field val outer is fully initialized. Calling trace: | -> println(b) // error [ promotion-loop.scala:16 ] diff --git a/tests/init/neg/t3273.check b/tests/init/neg/t3273.check index 7f0ba4768618..6ffa607164f2 100644 --- a/tests/init/neg/t3273.check +++ b/tests/init/neg/t3273.check @@ -2,7 +2,6 @@ 4 | val num1: LazyList[Int] = 1 #:: num1.map(_ + 1) // error | ^^^^^^^^^^^^^^^ | Cannot prove the argument is fully initialized. - | | Promoting the value to fully initialized failed due to the following problem: | Access non-initialized value num1. Calling trace: | -> val num1: LazyList[Int] = 1 #:: num1.map(_ + 1) // error [ t3273.scala:4 ] @@ -11,7 +10,6 @@ 5 | val num2: LazyList[Int] = 1 #:: num2.iterator.map(_ + 1).to(LazyList) // error | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | Cannot prove the argument is fully initialized. - | | Promoting the value to fully initialized failed due to the following problem: | Access non-initialized value num2. Calling trace: | -> val num2: LazyList[Int] = 1 #:: num2.iterator.map(_ + 1).to(LazyList) // error [ t3273.scala:5 ] From efb97765ceb7dea05a1765746ad30d7c89b8cd75 Mon Sep 17 00:00:00 2001 From: Fengyun Liu Date: Mon, 23 May 2022 02:16:48 +0200 Subject: [PATCH 04/11] Make TreeChecker happy The failure comes from the tree checker, due to a limitation in subtyping checker: ``` dotty.tools.dotc.transform.init.Semantic.Hot.type | (dotty.tools.dotc.transform.init.Semantic.Hot.type | dotty.tools.dotc.transform.init.Semantic.Cold.type ) <: dotty.tools.dotc.transform.init.Semantic.Hot.type | dotty.tools.dotc.transform.init.Semantic.Cold.type = false ``` --- .../dotty/tools/dotc/transform/init/Semantic.scala | 13 ++++++------- 1 file changed, 6 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 b078b406bb09..a3834916fc13 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala @@ -422,11 +422,10 @@ object Semantic { /** Stop on first found error */ def stopEarly(fn: Reporter ?=> Unit): List[Error] = - // use promotion reporter to stop the analysis on the first error - val promotionReporter: Reporter = new StopEarlyReporter + val reporter: Reporter = new StopEarlyReporter try - fn(using promotionReporter) + fn(using reporter) Nil catch case ex: ErrorFound => ex.error :: Nil @@ -629,7 +628,7 @@ object Semantic { case typeParamRef: TypeParamRef => val bounds = typeParamRef.underlying.bounds val isWithinBounds = bounds.lo <:< defn.NothingType && defn.AnyType <:< bounds.hi - def otherParamContains = allParamTypes.exists { param => param != info && param.typeSymbol != defn.ClassTagClass && info.occursIn(param) } + def otherParamContains = allParamTypes.exists { param => param != typeParamRef && param.typeSymbol != defn.ClassTagClass && typeParamRef.occursIn(param) } // A non-hot method argument is allowed if the corresponding parameter type is a // type parameter T with Any as its upper bound and Nothing as its lower bound. // the other arguments should either correspond to a parameter type that is T @@ -652,12 +651,12 @@ object Semantic { if receiver.typeSymbol.isStaticOwner then val (errors, allArgsPromote) = checkArgsWithParametricity() if allArgsPromote then - Hot + Hot: Value else if errors.nonEmpty then for error <- errors do reporter.report(error) - Hot + Hot: Value else - Cold + Cold: Value else checkArgs Hot From a98076f85be36262be03f58aa7a66a331401242e Mon Sep 17 00:00:00 2001 From: Fengyun Liu Date: Mon, 23 May 2022 08:25:44 +0200 Subject: [PATCH 05/11] Clean up --- .../tools/dotc/transform/init/Semantic.scala | 21 +++++++------------ 1 file changed, 8 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 a3834916fc13..6e85e60eb913 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala @@ -398,29 +398,25 @@ object Semantic { /** Error reporting */ trait Reporter: - def errors: List[Error] def report(err: Error): Unit object Reporter: - class BufferReporter extends Reporter: - val buf = new mutable.ArrayBuffer[Error] + class BufferedReporter extends Reporter: + private val buf = new mutable.ArrayBuffer[Error] def errors = buf.toList def report(err: Error) = buf += err class ErrorFound(val error: Error) extends Exception class StopEarlyReporter extends Reporter: def report(err: Error) = throw new ErrorFound(err) - def errors = ??? - - def fresh(): Reporter = new BufferReporter /** Capture all errors and return as a list */ def errorsIn(fn: Reporter ?=> Unit): List[Error] = - val reporter = Reporter.fresh() + val reporter = new BufferedReporter fn(using reporter) reporter.errors.toList - /** Stop on first found error */ + /** Stop on first error */ def stopEarly(fn: Reporter ?=> Unit): List[Error] = val reporter: Reporter = new StopEarlyReporter @@ -793,10 +789,9 @@ object Semantic { case Hot => val buffer = new mutable.ArrayBuffer[Error] val args2 = args.map { arg => - given reporter: Reporter = Reporter.fresh() - arg.promote - buffer ++= reporter.errors - if reporter.errors.isEmpty then Hot + val errors = Reporter.errorsIn { arg.promote } + buffer ++= errors + if errors.isEmpty then Hot else arg.value.widenArg } @@ -1053,7 +1048,7 @@ object Semantic { given Promoted = Promoted.empty given Trace = Trace.empty given Env = Env(paramValues) - given Reporter = Reporter.fresh() + given reporter: Reporter.BufferedReporter = new Reporter.BufferedReporter thisRef.ensureFresh() log("checking " + task) { eval(tpl, thisRef, thisRef.klass) } From c1595c51d2aca5dd2be061d2a708ada485601ffb Mon Sep 17 00:00:00 2001 From: Fengyun Liu Date: Tue, 24 May 2022 11:37:45 +0200 Subject: [PATCH 06/11] Organize code --- .../tools/dotc/transform/init/Semantic.scala | 46 +++++++++---------- 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala b/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala index 6e85e60eb913..29648b72ed13 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala @@ -396,6 +396,29 @@ object Semantic { inline def cache(using c: Cache): Cache = c +// ----- Checker State ----------------------------------- + + /** The state that threads through the interpreter */ + type Contextual[T] = (Env, Context, Trace, Promoted, Cache, Reporter) ?=> T + +// ----- Error Handling ----------------------------------- + + object Trace { + opaque type Trace = Vector[Tree] + + val empty: Trace = Vector.empty + + extension (trace: Trace) + def add(node: Tree): Trace = trace :+ node + def toVector: Vector[Tree] = trace + } + + type Trace = Trace.Trace + + import Trace._ + def trace(using t: Trace): Trace = t + inline def withTrace[T](t: Trace)(op: Trace ?=> T): T = op(using t) + /** Error reporting */ trait Reporter: def report(err: Error): Unit @@ -429,29 +452,6 @@ object Semantic { inline def reporter(using r: Reporter): Reporter = r -// ----- Checker State ----------------------------------- - - /** The state that threads through the interpreter */ - type Contextual[T] = (Env, Context, Trace, Promoted, Cache, Reporter) ?=> T - -// ----- Error Handling ----------------------------------- - - object Trace { - opaque type Trace = Vector[Tree] - - val empty: Trace = Vector.empty - - extension (trace: Trace) - def add(node: Tree): Trace = trace :+ node - def toVector: Vector[Tree] = trace - } - - type Trace = Trace.Trace - - import Trace._ - def trace(using t: Trace): Trace = t - inline def withTrace[T](t: Trace)(op: Trace ?=> T): T = op(using t) - // ----- Operations on domains ----------------------------- extension (a: Value) def join(b: Value): Value = From 43553eaa5178446b1138d20342028919292af35d Mon Sep 17 00:00:00 2001 From: Fengyun Liu Date: Tue, 24 May 2022 11:38:11 +0200 Subject: [PATCH 07/11] Improve error message --- .../src/dotty/tools/dotc/transform/init/Semantic.scala | 8 ++++---- tests/init/neg/closureLeak.check | 10 +++++----- tests/init/neg/default-this.check | 10 +++++----- tests/init/neg/inherit-non-hot.check | 2 +- tests/init/neg/inlined-method.check | 10 +++++----- tests/init/neg/local-warm4.check | 2 +- tests/init/neg/promotion-loop.check | 2 +- tests/init/neg/t3273.check | 4 ++-- 8 files changed, 24 insertions(+), 24 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala b/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala index 29648b72ed13..8ea6d9fdd7d9 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala @@ -1094,7 +1094,7 @@ object Semantic { /** Utility definition used for better error-reporting of argument errors */ case class ArgInfo(value: Value, source: Tree) { - def promote: Contextual[Unit] = value.promote("Cannot prove the argument is fully initialized.", source) + def promote: Contextual[Unit] = value.promote("Cannot prove the argument is fully initialized. Only fully initialized values are safe to leak.", source) } /** Evaluate an expression with the given value for `this` in a given class `klass` @@ -1223,9 +1223,9 @@ object Semantic { lhs match case Select(qual, _) => eval(qual, thisV, klass) - eval(rhs, thisV, klass).ensureHot("May only assign fully initialized value.", rhs) + eval(rhs, thisV, klass).ensureHot("The RHS of an assignment to a field must be fully initialized.", rhs) case id: Ident => - eval(rhs, thisV, klass).ensureHot("May only assign fully initialized value.", rhs) + eval(rhs, thisV, klass).ensureHot("The RHS of an assignment to a field must be fully initialized.", rhs) case closureDef(ddef) => Fun(ddef.rhs, thisV, klass, env) @@ -1249,7 +1249,7 @@ object Semantic { eval(cases.map(_.body), thisV, klass).join case Return(expr, from) => - eval(expr, thisV, klass).ensureHot("return expression may only be initialized value.", expr) + eval(expr, thisV, klass).ensureHot("return expression must be fully initialized.", expr) case WhileDo(cond, body) => eval(cond :: body :: Nil, thisV, klass) diff --git a/tests/init/neg/closureLeak.check b/tests/init/neg/closureLeak.check index b6459ff2f4d6..306a0eebed44 100644 --- a/tests/init/neg/closureLeak.check +++ b/tests/init/neg/closureLeak.check @@ -1,8 +1,8 @@ -- Error: tests/init/neg/closureLeak.scala:11:14 ----------------------------------------------------------------------- 11 | l.foreach(a => a.addX(this)) // error | ^^^^^^^^^^^^^^^^^ - | Cannot prove the argument is fully initialized. - | Promoting the value to fully initialized failed due to the following problem: - | Cannot prove the argument is fully initialized. Calling trace: - | -> l.foreach(a => a.addX(this)) // error [ closureLeak.scala:11 ] - | ^^^^ + | Cannot prove the argument is fully initialized. Only fully initialized values are safe to leak. + | Promoting the value to fully initialized failed due to the following problem: + | Cannot prove the argument is fully initialized. Only fully initialized values are safe to leak. Calling trace: + | -> l.foreach(a => a.addX(this)) // error [ closureLeak.scala:11 ] + | ^^^^ diff --git a/tests/init/neg/default-this.check b/tests/init/neg/default-this.check index 734da0aebf71..fc93c740350c 100644 --- a/tests/init/neg/default-this.check +++ b/tests/init/neg/default-this.check @@ -1,8 +1,8 @@ -- Error: tests/init/neg/default-this.scala:9:8 ------------------------------------------------------------------------ 9 | compare() // error | ^^^^^^^ - | Cannot prove the argument is fully initialized. Calling trace: - | -> val result = updateThenCompare(5) [ default-this.scala:11 ] - | ^^^^^^^^^^^^^^^^^^^^ - | -> compare() // error [ default-this.scala:9 ] - | ^^^^^^^ + | Cannot prove the argument is fully initialized. Only fully initialized values are safe to leak. Calling trace: + | -> val result = updateThenCompare(5) [ default-this.scala:11 ] + | ^^^^^^^^^^^^^^^^^^^^ + | -> compare() // error [ default-this.scala:9 ] + | ^^^^^^^ diff --git a/tests/init/neg/inherit-non-hot.check b/tests/init/neg/inherit-non-hot.check index 1158b2506a2c..462143a5d159 100644 --- a/tests/init/neg/inherit-non-hot.check +++ b/tests/init/neg/inherit-non-hot.check @@ -1,7 +1,7 @@ -- Error: tests/init/neg/inherit-non-hot.scala:6:34 -------------------------------------------------------------------- 6 | if b == null then b = new B(this) // error | ^^^^^^^^^^^ - | May only assign fully initialized value. Calling trace: + | The RHS of an assignment to a field must be fully initialized. Calling trace: | -> val c = new C [ inherit-non-hot.scala:19 ] | ^^^^^ | -> class C extends A { [ inherit-non-hot.scala:15 ] diff --git a/tests/init/neg/inlined-method.check b/tests/init/neg/inlined-method.check index 9dca5c1fa4c2..a2119050a83c 100644 --- a/tests/init/neg/inlined-method.check +++ b/tests/init/neg/inlined-method.check @@ -1,8 +1,8 @@ -- Error: tests/init/neg/inlined-method.scala:8:45 --------------------------------------------------------------------- 8 | scala.runtime.Scala3RunTime.assertFailed(message) // error | ^^^^^^^ - | Cannot prove the argument is fully initialized. Calling trace: - | -> Assertion.failAssert(this) [ inlined-method.scala:2 ] - | ^^^^^^^^^^^^^^^^^^^^^^^^^^ - | -> scala.runtime.Scala3RunTime.assertFailed(message) // error [ inlined-method.scala:8 ] - | ^^^^^^^ + | Cannot prove the argument is fully initialized. Only fully initialized values are safe to leak. Calling trace: + | -> Assertion.failAssert(this) [ inlined-method.scala:2 ] + | ^^^^^^^^^^^^^^^^^^^^^^^^^^ + | -> scala.runtime.Scala3RunTime.assertFailed(message) // error [ inlined-method.scala:8 ] + | ^^^^^^^ diff --git a/tests/init/neg/local-warm4.check b/tests/init/neg/local-warm4.check index 4d46d4758401..b3551f219e9e 100644 --- a/tests/init/neg/local-warm4.check +++ b/tests/init/neg/local-warm4.check @@ -1,7 +1,7 @@ -- Error: tests/init/neg/local-warm4.scala:18:20 ----------------------------------------------------------------------- 18 | a = newA // error | ^^^^ - | May only assign fully initialized value. Calling trace: + | The RHS of an assignment to a field must be fully initialized. Calling trace: | -> val a = new A(5) [ local-warm4.scala:26 ] | ^^^^^^^^ | -> class A(x: Int) extends Foo(x) { [ local-warm4.scala:6 ] diff --git a/tests/init/neg/promotion-loop.check b/tests/init/neg/promotion-loop.check index 05d677002bef..2398b118050e 100644 --- a/tests/init/neg/promotion-loop.check +++ b/tests/init/neg/promotion-loop.check @@ -1,7 +1,7 @@ -- Error: tests/init/neg/promotion-loop.scala:16:10 -------------------------------------------------------------------- 16 | println(b) // error | ^ - | Cannot prove the argument is fully initialized. + | Cannot prove the argument is fully initialized. Only fully initialized values are safe to leak. | Promoting the value to fully initialized failed due to the following problem: | Cannot prove that the field val outer is fully initialized. Calling trace: | -> println(b) // error [ promotion-loop.scala:16 ] diff --git a/tests/init/neg/t3273.check b/tests/init/neg/t3273.check index 6ffa607164f2..331db32b72bc 100644 --- a/tests/init/neg/t3273.check +++ b/tests/init/neg/t3273.check @@ -1,7 +1,7 @@ -- Error: tests/init/neg/t3273.scala:4:42 ------------------------------------------------------------------------------ 4 | val num1: LazyList[Int] = 1 #:: num1.map(_ + 1) // error | ^^^^^^^^^^^^^^^ - | Cannot prove the argument is fully initialized. + | Cannot prove the argument is fully initialized. Only fully initialized values are safe to leak. | Promoting the value to fully initialized failed due to the following problem: | Access non-initialized value num1. Calling trace: | -> val num1: LazyList[Int] = 1 #:: num1.map(_ + 1) // error [ t3273.scala:4 ] @@ -9,7 +9,7 @@ -- Error: tests/init/neg/t3273.scala:5:61 ------------------------------------------------------------------------------ 5 | val num2: LazyList[Int] = 1 #:: num2.iterator.map(_ + 1).to(LazyList) // error | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | Cannot prove the argument is fully initialized. + | Cannot prove the argument is fully initialized. Only fully initialized values are safe to leak. | Promoting the value to fully initialized failed due to the following problem: | Access non-initialized value num2. Calling trace: | -> val num2: LazyList[Int] = 1 #:: num2.iterator.map(_ + 1).to(LazyList) // error [ t3273.scala:5 ] From a4c78a46810b5cb85929cd8d1baee2a0a01f6d46 Mon Sep 17 00:00:00 2001 From: Fengyun Liu Date: Wed, 25 May 2022 00:53:21 +0200 Subject: [PATCH 08/11] Always show current class at the top of trace --- .../tools/dotc/transform/init/Errors.scala | 28 +- .../tools/dotc/transform/init/Semantic.scala | 250 +++++++++--------- tests/init/neg/closureLeak.check | 7 +- tests/init/neg/cycle-structure.check | 12 +- tests/init/neg/default-this.check | 4 + tests/init/neg/inherit-non-hot.check | 8 +- tests/init/neg/inlined-method.check | 2 + tests/init/neg/leak-warm.check | 11 +- tests/init/neg/local-warm4.check | 8 +- tests/init/neg/promotion-loop.check | 13 +- tests/init/neg/t3273.check | 26 +- tests/init/neg/unsound1.check | 6 +- tests/init/neg/unsound2.check | 6 +- tests/init/neg/unsound3.check | 12 +- tests/init/neg/unsound4.check | 2 + 15 files changed, 228 insertions(+), 167 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/transform/init/Errors.scala b/compiler/src/dotty/tools/dotc/transform/init/Errors.scala index aac600743efb..8db962d7e823 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Errors.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Errors.scala @@ -13,15 +13,16 @@ import scala.collection.mutable object Errors: sealed trait Error { - def source: Tree def trace: Seq[Tree] def show(using Context): String + def pos(using Context): SourcePosition = trace.last.sourcePos + def issue(using Context): Unit = - report.warning(show + stacktrace, source.srcPos) + report.warning(show + stacktrace, this.pos) - private def isTraceInformative: Boolean = - trace.size > 1 || trace.size == 1 && trace.head.ne(source) + private def isTraceInformative(using Context): Boolean = + trace.size > 1 || trace.size == 1 && trace.head.sourcePos.ne(pos) def stacktrace(using Context): String = if !isTraceInformative then "" else " Calling trace:\n" + { var lastLineNum = -1 @@ -77,35 +78,34 @@ object Errors: def show(using Context): String = "Access non-initialized " + field.show + "." - override def issue(using Context): Unit = - report.warning(show + stacktrace, field.srcPos) + override def pos(using Context): SourcePosition = field.sourcePos } /** Promote a value under initialization to fully-initialized */ - case class PromoteError(msg: String, source: Tree, trace: Seq[Tree]) extends Error { + case class PromoteError(msg: String, trace: Seq[Tree]) extends Error { def show(using Context): String = msg } - case class AccessCold(field: Symbol, source: Tree, trace: Seq[Tree]) extends Error { + case class AccessCold(field: Symbol, trace: Seq[Tree]) extends Error { def show(using Context): String = - "Access field " + source.show + " on a value with an unknown initialization status." + "Access field on a value with an unknown initialization status." } - case class CallCold(meth: Symbol, source: Tree, trace: Seq[Tree]) extends Error { + case class CallCold(meth: Symbol, trace: Seq[Tree]) extends Error { def show(using Context): String = - "Call method " + source.show + " on a value with an unknown initialization" + "." + "Call method on a value with an unknown initialization" + "." } - case class CallUnknown(meth: Symbol, source: Tree, trace: Seq[Tree]) extends Error { + case class CallUnknown(meth: Symbol, trace: Seq[Tree]) extends Error { def show(using Context): String = 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 */ - case class UnsafePromotion(msg: String, source: Tree, trace: Seq[Tree], error: Error) extends Error { + case class UnsafePromotion(msg: String, trace: Seq[Tree], error: Error) extends Error { override def issue(using Context): Unit = - report.warning(show, source.srcPos) + report.warning(show, this.pos) def show(using Context): String = { var index = 0 diff --git a/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala b/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala index 8ea6d9fdd7d9..763c378a5a24 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala @@ -109,7 +109,7 @@ object Semantic { assert(!populatingParams, "the object is already populating parameters") populatingParams = true val tpl = klass.defTree.asInstanceOf[TypeDef].rhs.asInstanceOf[Template] - this.callConstructor(ctor, args.map(arg => ArgInfo(arg, EmptyTree)), tpl) + extendTrace(klass.defTree) { this.callConstructor(ctor, args.map(arg => ArgInfo(arg, trace))) } populatingParams = false this } @@ -418,6 +418,7 @@ object Semantic { import Trace._ def trace(using t: Trace): Trace = t inline def withTrace[T](t: Trace)(op: Trace ?=> T): T = op(using t) + inline def extendTrace[T](node: Tree)(using t: Trace)(op: Trace ?=> T): T = op(using t.add(node)) /** Error reporting */ trait Reporter: @@ -545,26 +546,24 @@ object Semantic { end extension extension (value: Value) - def ensureHot(msg: String, source: Tree): Contextual[Value] = - value.promote(msg, source) + def ensureHot(msg: String): Contextual[Value] = + value.promote(msg) value - def select(field: Symbol, source: Tree, needResolve: Boolean = true): Contextual[Value] = log("select " + field.show + ", this = " + value, printer, (_: Value).show) { + def select(field: Symbol, needResolve: Boolean = true): Contextual[Value] = log("select " + field.show + ", this = " + value, printer, (_: Value).show) { if promoted.isCurrentObjectPromoted then Hot else value match { case Hot => Hot case Cold => - val error = AccessCold(field, source, trace.toVector) + val error = AccessCold(field, trace.toVector) reporter.report(error) Hot case ref: Ref => val target = if needResolve then resolve(ref.klass, field) else field - val trace1 = trace.add(source) if target.is(Flags.Lazy) then - given Trace = trace1 val rhs = target.defTree.asInstanceOf[ValDef].rhs eval(rhs, ref, target.owner.asClass, cacheResult = true) else @@ -583,24 +582,24 @@ object Semantic { val rhs = target.defTree.asInstanceOf[ValOrDefDef].rhs eval(rhs, ref, target.owner.asClass, cacheResult = true) else - val error = CallUnknown(field, source, trace.toVector) + val error = CallUnknown(field, trace.toVector) reporter.report(error) Hot else - val error = AccessNonInit(target, trace.add(source).toVector) + val error = AccessNonInit(target, trace.toVector) reporter.report(error) Hot case fun: Fun => - report.error("unexpected tree in selecting a function, fun = " + fun.expr.show, source) + report.error("unexpected tree in selecting a function, fun = " + fun.expr.show, fun.expr) Hot case RefSet(refs) => - refs.map(_.select(field, source)).join + refs.map(_.select(field)).join } } - def call(meth: Symbol, args: List[ArgInfo], receiver: Type, superType: Type, source: Tree, needResolve: Boolean = true): Contextual[Value] = log("call " + meth.show + ", args = " + args, printer, (_: Value).show) { + def call(meth: Symbol, args: List[ArgInfo], receiver: Type, superType: Type, needResolve: Boolean = true): Contextual[Value] = log("call " + meth.show + ", args = " + args, printer, (_: Value).show) { def checkArgs = args.foreach(_.promote) def isSyntheticApply(meth: Symbol) = @@ -642,7 +641,7 @@ object Semantic { case Hot => if isSyntheticApply(meth) then val klass = meth.owner.companionClass.asClass - instantiate(klass, klass.primaryConstructor, args, source) + instantiate(klass, klass.primaryConstructor, args) else if receiver.typeSymbol.isStaticOwner then val (errors, allArgsPromote) = checkArgsWithParametricity() @@ -659,7 +658,7 @@ object Semantic { case Cold => checkArgs - val error = CallCold(meth, source, trace.toVector) + val error = CallCold(meth, trace.toVector) reporter.report(error) Hot @@ -674,9 +673,7 @@ object Semantic { resolve(ref.klass, meth) if target.isOneOf(Flags.Method) then - val trace1 = trace.add(source) if target.hasSource then - given Trace = trace1 val cls = target.owner.enclosingClass.asClass val ddef = target.defTree.asInstanceOf[DefDef] val argErrors = Reporter.errorsIn { args.foreach(_.promote) } @@ -684,19 +681,21 @@ object Semantic { if argErrors.nonEmpty && isSyntheticApply(meth) then val klass = meth.owner.companionClass.asClass val outerCls = klass.owner.lexicallyEnclosingClass.asClass - val outer = resolveOuterSelect(outerCls, ref, 1, source) - outer.instantiate(klass, klass.primaryConstructor, args, source) + val outer = resolveOuterSelect(outerCls, ref, 1) + outer.instantiate(klass, klass.primaryConstructor, args) else for error <- argErrors do reporter.report(error) withEnv(if isLocal then env else Env.empty) { - eval(ddef.rhs, ref, cls, cacheResult = true) + extendTrace(ddef) { + eval(ddef.rhs, ref, cls, cacheResult = true) + } } else if ref.canIgnoreMethodCall(target) then Hot else // no source code available checkArgs - val error = CallUnknown(target, source, trace.toVector) + val error = CallUnknown(target, trace.toVector) reporter.report(error) Hot else @@ -705,7 +704,7 @@ object Semantic { if obj.hasField(target) then obj.field(target) else - value.select(target, source, needResolve = false) + value.select(target, needResolve = false) case Fun(body, thisV, klass, env) => // meth == NoSymbol for poly functions @@ -717,11 +716,11 @@ object Semantic { } case RefSet(refs) => - refs.map(_.call(meth, args, receiver, superType, source)).join + refs.map(_.call(meth, args, receiver, superType)).join } } - def callConstructor(ctor: Symbol, args: List[ArgInfo], source: Tree): Contextual[Value] = log("call " + ctor.show + ", args = " + args, printer, (_: Value).show) { + def callConstructor(ctor: Symbol, args: List[ArgInfo]): Contextual[Value] = log("call " + ctor.show + ", args = " + args, printer, (_: Value).show) { // init "fake" param fields for the secondary constructor def addParamsAsFields(env: Env, ref: Ref, ctorDef: DefDef) = { val paramSyms = ctorDef.termParamss.flatten.map(_.symbol) @@ -733,48 +732,44 @@ object Semantic { } value match { case Hot | Cold | _: RefSet | _: Fun => - report.error("unexpected constructor call, meth = " + ctor + ", value = " + value, source) + report.error("unexpected constructor call, meth = " + ctor + ", value = " + value, trace.toVector.last) Hot case ref: Warm if ref.isPopulatingParams => - val trace1 = trace.add(source) if ctor.hasSource then - given Trace = trace1 val cls = ctor.owner.enclosingClass.asClass val ddef = ctor.defTree.asInstanceOf[DefDef] given Env = Env(ddef, args.map(_.value).widenArgs) if ctor.isPrimaryConstructor then val tpl = cls.defTree.asInstanceOf[TypeDef].rhs.asInstanceOf[Template] - init(tpl, ref, cls) + extendTrace(cls.defTree) { init(tpl, ref, cls) } else addParamsAsFields(env, ref, ddef) val initCall = ddef.rhs match case Block(call :: _, _) => call case call => call - eval(initCall, ref, cls) + extendTrace(ddef) { eval(initCall, ref, cls) } end if else Hot case ref: Ref => - val trace1 = trace.add(source) if ctor.hasSource then - given Trace = trace1 val cls = ctor.owner.enclosingClass.asClass val ddef = ctor.defTree.asInstanceOf[DefDef] given Env = Env(ddef, args.map(_.value).widenArgs) if ctor.isPrimaryConstructor then val tpl = cls.defTree.asInstanceOf[TypeDef].rhs.asInstanceOf[Template] - withTrace(trace.add(cls.defTree)) { eval(tpl, ref, cls, cacheResult = true) } + extendTrace(cls.defTree) { eval(tpl, ref, cls, cacheResult = true) } ref else addParamsAsFields(env, ref, ddef) - eval(ddef.rhs, ref, cls, cacheResult = true) + extendTrace(ddef) { eval(ddef.rhs, ref, cls, cacheResult = true) } else if ref.canIgnoreMethodCall(ctor) then Hot else // no source code available - val error = CallUnknown(ctor, source, trace.toVector) + val error = CallUnknown(ctor, trace.toVector) reporter.report(error) Hot } @@ -782,8 +777,7 @@ object Semantic { } /** Handle a new expression `new p.C` where `p` is abstracted by `value` */ - def instantiate(klass: ClassSymbol, ctor: Symbol, args: List[ArgInfo], source: Tree): Contextual[Value] = log("instantiating " + klass.show + ", value = " + value + ", args = " + args, printer, (_: Value).show) { - val trace1 = trace.add(source) + def instantiate(klass: ClassSymbol, ctor: Symbol, args: List[ArgInfo]): Contextual[Value] = log("instantiating " + klass.show + ", value = " + value + ", args = " + args, printer, (_: Value).show) { if promoted.isCurrentObjectPromoted then Hot else value match { case Hot => @@ -801,16 +795,15 @@ object Semantic { val outer = Hot val warm = Warm(klass, outer, ctor, args2).ensureObjectExists() val argInfos2 = args.zip(args2).map { (argInfo, v) => argInfo.copy(value = v) } - warm.callConstructor(ctor, argInfos2, source) + warm.callConstructor(ctor, argInfos2) warm case Cold => - val error = CallCold(ctor, source, trace1.toVector) + val error = CallCold(ctor, trace.toVector) reporter.report(error) Hot case ref: Ref => - given Trace = trace1 // widen the outer to finitize the domain val outer = ref match case warm @ Warm(_, _: Warm, _, _) => @@ -821,21 +814,21 @@ object Semantic { val argsWidened = args.map(_.value).widenArgs val argInfos2 = args.zip(argsWidened).map { (argInfo, v) => argInfo.copy(value = v) } val warm = Warm(klass, outer, ctor, argsWidened).ensureObjectExists() - warm.callConstructor(ctor, argInfos2, source) + warm.callConstructor(ctor, argInfos2) warm case Fun(body, thisV, klass, env) => - report.error("unexpected tree in instantiating a function, fun = " + body.show, source) + report.error("unexpected tree in instantiating a function, fun = " + body.show, trace.toVector.last) Hot case RefSet(refs) => - refs.map(_.instantiate(klass, ctor, args, source)).join + refs.map(_.instantiate(klass, ctor, args)).join } } end extension extension (ref: Ref) - def accessLocal(tmref: TermRef, klass: ClassSymbol, source: Tree): Contextual[Value] = + def accessLocal(tmref: TermRef, klass: ClassSymbol): Contextual[Value] = val sym = tmref.symbol if sym.is(Flags.Param) && sym.owner.isConstructor then @@ -861,7 +854,7 @@ object Semantic { case vdef: ValDef => // resolve this for local variable val enclosingClass = sym.owner.enclosingClass.asClass - val thisValue2 = resolveThis(enclosingClass, ref, klass, source) + val thisValue2 = resolveThis(enclosingClass, ref, klass) thisValue2 match case Hot => Hot @@ -870,7 +863,7 @@ object Semantic { case ref: Ref => eval(vdef.rhs, ref, enclosingClass) case _ => - report.error("unexpected defTree when accessing local variable, sym = " + sym.show + ", defTree = " + sym.defTree.show, source) + report.error("unexpected defTree when accessing local variable, sym = " + sym.show + ", defTree = " + sym.defTree.show, trace.toVector.last) Hot end match @@ -921,42 +914,43 @@ object Semantic { extension (value: Value) /** Promotion of values to hot */ - def promote(msg: String, source: Tree): Contextual[Unit] = log("promoting " + value + ", promoted = " + promoted, printer) { - val trace2 = trace.add(source) + def promote(msg: String): Contextual[Unit] = log("promoting " + value + ", promoted = " + promoted, printer) { if promoted.isCurrentObjectPromoted then Nil else - given Trace = trace2 value.match case Hot => case Cold => - reporter.report(PromoteError(msg, source, trace.toVector)) + reporter.report(PromoteError(msg, trace.toVector)) case thisRef: ThisRef => if !thisRef.tryPromoteCurrentObject() then - reporter.report(PromoteError(msg, source, trace.toVector)) + reporter.report(PromoteError(msg, trace.toVector)) case warm: Warm => if !promoted.contains(warm) then promoted.add(warm) - val errors = warm.tryPromote(msg, source) + val errors = warm.tryPromote(msg) if errors.nonEmpty then promoted.remove(warm) for error <- errors do reporter.report(error) case fun @ Fun(body, thisV, klass, env) => if !promoted.contains(fun) then val errors = Reporter.stopEarly { + val res = withEnv(env) { + given Trace = Trace.empty + eval(body, thisV, klass) + } given Trace = Trace.empty.add(body) - val res = withEnv(env) { eval(body, thisV, klass) } - res.promote("The function return value is not fully initialized.", body) + res.promote("The function return value is not fully initialized.") } if (errors.nonEmpty) - reporter.report(UnsafePromotion(msg, source, trace.toVector, errors.head)) + reporter.report(UnsafePromotion(msg, trace.toVector, errors.head)) else promoted.add(fun) case RefSet(refs) => - refs.foreach(_.promote(msg, source)) + refs.foreach(_.promote(msg)) } end extension @@ -978,31 +972,32 @@ object Semantic { * 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 + ", promoted = " + promoted, printer) { + def tryPromote(msg: String): Contextual[List[Error]] = log("promote " + warm.show + ", promoted = " + promoted, printer) { val classRef = warm.klass.appliedRef if classRef.memberClasses.nonEmpty || !warm.isFullyFilled then - return PromoteError(msg, source, trace.toVector) :: Nil + return PromoteError(msg, trace.toVector) :: Nil val errors = Reporter.stopEarly { for klass <- warm.klass.baseClasses if klass.hasSource do for member <- klass.info.decls do if !member.isType && !member.isConstructor && member.hasSource && !member.is(Flags.Deferred) then - given Trace = Trace.empty.add(source) if member.is(Flags.Method, butNot = Flags.Accessor) then - locally { - val args = member.info.paramInfoss.flatten.map(_ => ArgInfo(Hot, EmptyTree)) - val res = warm.call(member, args, receiver = NoType, superType = NoType, source = member.defTree) - res.promote("Cannot prove that the return value of " + member + " is fully initialized.", source) + withTrace(Trace.empty) { + val args = member.info.paramInfoss.flatten.map(_ => ArgInfo(Hot, Trace.empty)) + val res = warm.call(member, args, receiver = NoType, superType = NoType) + res.promote("Cannot prove that the return value of " + member + " is fully initialized.") } else - val res = warm.select(member, source) - res.promote("Cannot prove that the field " + member + " is fully initialized.", source) + withTrace(Trace.empty) { + val res = warm.select(member) + res.promote("Cannot prove that the field " + member + " is fully initialized.") + } end for end for } if errors.isEmpty then Nil - else UnsafePromotion(msg, source, trace.toVector, errors.head) :: Nil + else UnsafePromotion(msg, trace.toVector, errors.head) :: Nil } end extension @@ -1046,7 +1041,7 @@ object Semantic { @tailrec def iterate(): Unit = { given Promoted = Promoted.empty - given Trace = Trace.empty + given Trace = Trace.empty.add(thisRef.klass.defTree) given Env = Env(paramValues) given reporter: Reporter.BufferedReporter = new Reporter.BufferedReporter @@ -1093,8 +1088,10 @@ object Semantic { // ----- Semantic definition -------------------------------- /** Utility definition used for better error-reporting of argument errors */ - case class ArgInfo(value: Value, source: Tree) { - def promote: Contextual[Unit] = value.promote("Cannot prove the argument is fully initialized. Only fully initialized values are safe to leak.", source) + case class ArgInfo(value: Value, trace: Trace) { + def promote: Contextual[Unit] = withTrace(trace) { + value.promote("Cannot prove the argument is fully initialized. Only fully initialized values are safe to leak.") + } } /** Evaluate an expression with the given value for `this` in a given class `klass` @@ -1131,7 +1128,7 @@ object Semantic { else eval(arg.tree, thisV, klass) - argInfos += ArgInfo(res, arg.tree) + argInfos += ArgInfo(res, trace.add(arg.tree)) } argInfos.toList @@ -1140,6 +1137,7 @@ object Semantic { * Note: Recursive call should go to `eval` instead of `cases`. */ def cases(expr: Tree, thisV: Ref, klass: ClassSymbol): Contextual[Value] = + val trace2 = trace.add(expr) expr match { case Ident(nme.WILDCARD) => // TODO: disallow `var x: T = _` @@ -1147,18 +1145,16 @@ object Semantic { 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) + cases(expr.tpe, thisV, klass) case NewExpr(tref, New(tpt), ctor, argss) => // check args val args = evalArgs(argss.flatten, thisV, klass) val cls = tref.classSymbol.asClass - val outer = outerValue(tref, thisV, klass, tpt) - val trace2 = trace.add(expr) - locally { - given Trace = trace2 - outer.instantiate(cls, ctor, args, source = expr) + withTrace(trace2) { + val outer = outerValue(tref, thisV, klass) + outer.instantiate(cls, ctor, args) } case Call(ref, argss) => @@ -1168,30 +1164,30 @@ object Semantic { ref match case Select(supert: Super, _) => val SuperType(thisTp, superTp) = supert.tpe: @unchecked - val thisValue2 = resolveThis(thisTp.classSymbol.asClass, thisV, klass, ref) - thisValue2.call(ref.symbol, args, thisTp, superTp, expr) + val thisValue2 = extendTrace(ref) { resolveThis(thisTp.classSymbol.asClass, thisV, klass) } + withTrace(trace2) { thisValue2.call(ref.symbol, args, thisTp, superTp) } case Select(qual, _) => val receiver = eval(qual, thisV, klass) if ref.symbol.isConstructor then - receiver.callConstructor(ref.symbol, args, source = expr) + withTrace(trace2) { receiver.callConstructor(ref.symbol, args) } else - receiver.call(ref.symbol, args, receiver = qual.tpe, superType = NoType, source = expr) + withTrace(trace2) { receiver.call(ref.symbol, args, receiver = qual.tpe, superType = NoType) } case id: Ident => id.tpe match case TermRef(NoPrefix, _) => // resolve this for the local method val enclosingClass = id.symbol.owner.enclosingClass.asClass - val thisValue2 = resolveThis(enclosingClass, thisV, klass, id) + val thisValue2 = extendTrace(ref) { resolveThis(enclosingClass, thisV, klass) } // local methods are not a member, but we can reuse the method `call` - thisValue2.call(id.symbol, args, receiver = NoType, superType = NoType, expr, needResolve = false) + withTrace(trace2) { thisValue2.call(id.symbol, args, receiver = NoType, superType = NoType, needResolve = false) } case TermRef(prefix, _) => - val receiver = cases(prefix, thisV, klass, id) + val receiver = cases(prefix, thisV, klass) if id.symbol.isConstructor then - receiver.callConstructor(id.symbol, args, source = expr) + withTrace(trace2) { receiver.callConstructor(id.symbol, args) } else - receiver.call(id.symbol, args, receiver = prefix, superType = NoType, source = expr) + withTrace(trace2) { receiver.call(id.symbol, args, receiver = prefix, superType = NoType) } case Select(qualifier, name) => val qual = eval(qualifier, thisV, klass) @@ -1199,12 +1195,12 @@ object Semantic { name match case OuterSelectName(_, hops) => val SkolemType(tp) = expr.tpe: @unchecked - resolveOuterSelect(tp.classSymbol.asClass, qual, hops, source = expr) + withTrace(trace2) { resolveOuterSelect(tp.classSymbol.asClass, qual, hops) } case _ => - qual.select(expr.symbol, expr) + withTrace(trace2) { qual.select(expr.symbol) } case _: This => - cases(expr.tpe, thisV, klass, expr) + cases(expr.tpe, thisV, klass) case Literal(_) => Hot @@ -1223,9 +1219,15 @@ object Semantic { lhs match case Select(qual, _) => eval(qual, thisV, klass) - eval(rhs, thisV, klass).ensureHot("The RHS of an assignment to a field must be fully initialized.", rhs) + val res = eval(rhs, thisV, klass) + extendTrace(rhs) { + res.ensureHot("The RHS of an assignment to a field must be fully initialized.") + } case id: Ident => - eval(rhs, thisV, klass).ensureHot("The RHS of an assignment to a field must be fully initialized.", rhs) + val res = eval(rhs, thisV, klass) + extendTrace(rhs) { + res.ensureHot("The RHS of an assignment to a field must be fully initialized.") + } case closureDef(ddef) => Fun(ddef.rhs, thisV, klass, env) @@ -1245,11 +1247,17 @@ object Semantic { else eval(arg, thisV, klass) case Match(selector, cases) => - eval(selector, thisV, klass).ensureHot("The value to be matched needs to be fully initialized.", selector) + val res = eval(selector, thisV, klass) + extendTrace(selector) { + res.ensureHot("The value to be matched needs to be fully initialized.") + } eval(cases.map(_.body), thisV, klass).join case Return(expr, from) => - eval(expr, thisV, klass).ensureHot("return expression must be fully initialized.", expr) + val res = eval(expr, thisV, klass) + extendTrace(expr) { + res.ensureHot("return expression must be fully initialized.") + } case WhileDo(cond, body) => eval(cond :: body :: Nil, thisV, klass) @@ -1268,9 +1276,8 @@ object Semantic { elems.map { elem => eval(elem, thisV, klass) }.join case Inlined(call, bindings, expansion) => - val trace1 = trace.add(expr) eval(bindings, thisV, klass) - withTrace(trace1)(eval(expansion, thisV, klass)) + withTrace(trace2) { eval(expansion, thisV, klass) } case Thicket(List()) => // possible in try/catch/finally, see tests/crash/i6914.scala @@ -1303,16 +1310,16 @@ object Semantic { } /** Handle semantics of leaf nodes */ - def cases(tp: Type, thisV: Ref, klass: ClassSymbol, source: Tree): Contextual[Value] = log("evaluating " + tp.show, printer, (_: Value).show) { + def cases(tp: Type, thisV: Ref, klass: ClassSymbol): Contextual[Value] = log("evaluating " + tp.show, printer, (_: Value).show) { tp match { case _: ConstantType => Hot case tmref: TermRef if tmref.prefix == NoPrefix => - thisV.accessLocal(tmref, klass, source) + thisV.accessLocal(tmref, klass) case tmref: TermRef => - cases(tmref.prefix, thisV, klass, source).select(tmref.symbol, source) + cases(tmref.prefix, thisV, klass).select(tmref.symbol) case tp @ ThisType(tref) => val cls = tref.classSymbol.asClass @@ -1320,7 +1327,7 @@ object Semantic { // O.this outside the body of the object O Hot else - val value = resolveThis(cls, thisV, klass, source) + val value = resolveThis(cls, thisV, klass) value case _: TermParamRef | _: RecThis => @@ -1333,7 +1340,7 @@ object Semantic { } /** Resolve C.this that appear in `klass` */ - def resolveThis(target: ClassSymbol, thisV: Value, klass: ClassSymbol, source: Tree): Contextual[Value] = log("resolving " + target.show + ", this = " + thisV.show + " in " + klass.show, printer, (_: Value).show) { + def resolveThis(target: ClassSymbol, thisV: Value, klass: ClassSymbol): Contextual[Value] = log("resolving " + target.show + ", this = " + thisV.show + " in " + klass.show, printer, (_: Value).show) { if target == klass then thisV else if target.is(Flags.Package) then Hot else @@ -1343,15 +1350,15 @@ object Semantic { val obj = ref.objekt val outerCls = klass.owner.lexicallyEnclosingClass.asClass if !obj.hasOuter(klass) then - val error = PromoteError("outer not yet initialized, target = " + target + ", klass = " + klass + ", object = " + obj, source, trace.toVector) - report.error(error.show + error.stacktrace, source) + val error = PromoteError("outer not yet initialized, target = " + target + ", klass = " + klass + ", object = " + obj, trace.toVector) + report.error(error.show + error.stacktrace, trace.toVector.last) Hot else - resolveThis(target, obj.outer(klass), outerCls, source) + resolveThis(target, obj.outer(klass), outerCls) case RefSet(refs) => - refs.map(ref => resolveThis(target, ref, klass, source)).join + refs.map(ref => resolveThis(target, ref, klass)).join case fun: Fun => - report.warning("unexpected thisV = " + thisV + ", target = " + target.show + ", klass = " + klass.show, source.srcPos) + report.warning("unexpected thisV = " + thisV + ", target = " + target.show + ", klass = " + klass.show, trace.toVector.last) Cold case Cold => Cold @@ -1361,7 +1368,7 @@ object Semantic { * * See `tpd.outerSelect` and `ElimOuterSelect`. */ - def resolveOuterSelect(target: ClassSymbol, thisV: Value, hops: Int, source: Tree): Contextual[Value] = log("resolving outer " + target.show + ", this = " + thisV.show + ", hops = " + hops, printer, (_: Value).show) { + def resolveOuterSelect(target: ClassSymbol, thisV: Value, hops: Int): Contextual[Value] = log("resolving outer " + target.show + ", this = " + thisV.show + ", hops = " + hops, printer, (_: Value).show) { // Is `target` reachable from `cls` with the given `hops`? def reachable(cls: ClassSymbol, hops: Int): Boolean = log("reachable from " + cls + " -> " + target + " in " + hops, printer) { if hops == 0 then cls == target @@ -1376,32 +1383,32 @@ object Semantic { val curOpt = obj.klass.baseClasses.find(cls => reachable(cls, hops)) curOpt match case Some(cur) => - resolveThis(target, thisV, cur, source) + resolveThis(target, thisV, cur) case None => - report.warning("unexpected outerSelect, thisV = " + thisV + ", target = " + target.show + ", hops = " + hops, source.srcPos) + report.warning("unexpected outerSelect, thisV = " + thisV + ", target = " + target.show + ", hops = " + hops, trace.toVector.last.srcPos) Cold case RefSet(refs) => - refs.map(ref => resolveOuterSelect(target, ref, hops, source)).join + refs.map(ref => resolveOuterSelect(target, ref, hops)).join case fun: Fun => - report.warning("unexpected thisV = " + thisV + ", target = " + target.show + ", hops = " + hops, source.srcPos) + report.warning("unexpected thisV = " + thisV + ", target = " + target.show + ", hops = " + hops, trace.toVector.last.srcPos) Cold case Cold => Cold } /** Compute the outer value that correspond to `tref.prefix` */ - def outerValue(tref: TypeRef, thisV: Ref, klass: ClassSymbol, source: Tree): Contextual[Value] = + def outerValue(tref: TypeRef, thisV: Ref, klass: ClassSymbol): Contextual[Value] = val cls = tref.classSymbol.asClass if tref.prefix == NoPrefix then val enclosing = cls.owner.lexicallyEnclosingClass.asClass - val outerV = resolveThis(enclosing, thisV, klass, source) + val outerV = resolveThis(enclosing, thisV, klass) outerV else if cls.isAllOf(Flags.JavaInterface) then Hot - else cases(tref.prefix, thisV, klass, source) + else cases(tref.prefix, thisV, klass) /** Initialize part of an abstract object in `klass` of the inheritance chain */ def init(tpl: Template, thisV: Ref, klass: ClassSymbol): Contextual[Value] = log("init " + klass.show, printer, (_: Value).show) { @@ -1419,17 +1426,17 @@ object Semantic { // Tasks is used to schedule super constructor calls. // Super constructor calls are delayed until all outers are set. type Tasks = mutable.ArrayBuffer[() => Unit] - def superCall(tref: TypeRef, ctor: Symbol, args: List[ArgInfo], source: Tree, tasks: Tasks)(using Env): Unit = + def superCall(tref: TypeRef, ctor: Symbol, args: List[ArgInfo], tasks: Tasks)(using Env): Unit = val cls = tref.classSymbol.asClass // update outer for super class - val res = outerValue(tref, thisV, klass, source) + val res = outerValue(tref, thisV, klass) thisV.updateOuter(cls, res) // follow constructor if cls.hasSource then tasks.append { () => printer.println("init super class " + cls.show) - thisV.callConstructor(ctor, args, source) + thisV.callConstructor(ctor, args) () } @@ -1438,15 +1445,15 @@ object Semantic { case tree @ Block(stats, NewExpr(tref, New(tpt), ctor, argss)) => // can happen eval(stats, thisV, klass) val args = evalArgs(argss.flatten, thisV, klass) - superCall(tref, ctor, args, tree, tasks) + superCall(tref, ctor, args, tasks) case tree @ NewExpr(tref, New(tpt), ctor, argss) => // extends A(args) val args = evalArgs(argss.flatten, thisV, klass) - superCall(tref, ctor, args, tree, tasks) + superCall(tref, ctor, args, tasks) case _ => // extends A or extends A[T] val tref = typeRefOf(parent.tpe) - superCall(tref, tref.classSymbol.primaryConstructor, Nil, parent, tasks) + superCall(tref, tref.classSymbol.primaryConstructor, Nil, tasks) } // see spec 5.1 about "Template Evaluation". @@ -1461,7 +1468,7 @@ object Semantic { // 2. initialize traits according to linearization order val superParent = tpl.parents.head val superCls = superParent.tpe.classSymbol.asClass - initParent(superParent, tasks) + extendTrace(superParent) { initParent(superParent, tasks) } val parents = tpl.parents.tail val mixins = klass.baseClasses.tail.takeWhile(_ != superCls) @@ -1473,7 +1480,8 @@ object Semantic { // calls and user code in the class body. mixins.reverse.foreach { mixin => parents.find(_.tpe.classSymbol == mixin) match - case Some(parent) => initParent(parent, tasks) + case Some(parent) => + extendTrace(parent) { initParent(parent, tasks) } case None => // According to the language spec, if the mixin trait requires // arguments, then the class must provide arguments to it explicitly @@ -1484,7 +1492,9 @@ object Semantic { // term arguments to B. That can only be done in a concrete class. val tref = typeRefOf(klass.typeRef.baseType(mixin).typeConstructor) val ctor = tref.classSymbol.primaryConstructor - if ctor.exists then superCall(tref, ctor, Nil, superParent, tasks) + if ctor.exists then extendTrace(superParent) { + superCall(tref, ctor, Nil, tasks) + } } // initialize super classes after outers are set @@ -1523,7 +1533,7 @@ object Semantic { val traverser = new TypeTraverser { def traverse(tp: Type): Unit = tp match { case TermRef(_: SingletonType, _) => - cases(tp, thisV, klass, tpt) + extendTrace(tpt) { cases(tp, thisV, klass) } case _ => traverseChildren(tp) } diff --git a/tests/init/neg/closureLeak.check b/tests/init/neg/closureLeak.check index 306a0eebed44..a90355fce1d4 100644 --- a/tests/init/neg/closureLeak.check +++ b/tests/init/neg/closureLeak.check @@ -1,7 +1,12 @@ -- Error: tests/init/neg/closureLeak.scala:11:14 ----------------------------------------------------------------------- 11 | l.foreach(a => a.addX(this)) // error | ^^^^^^^^^^^^^^^^^ - | Cannot prove the argument is fully initialized. Only fully initialized values are safe to leak. + | Cannot prove the argument is fully initialized. Only fully initialized values are safe to leak. Calling trace: + | -> class Outer { [ closureLeak.scala:1 ] + | ^ + | -> l.foreach(a => a.addX(this)) // error [ closureLeak.scala:11 ] + | ^^^^^^^^^^^^^^^^^ + | | Promoting the value to fully initialized failed due to the following problem: | Cannot prove the argument is fully initialized. Only fully initialized values are safe to leak. Calling trace: | -> l.foreach(a => a.addX(this)) // error [ closureLeak.scala:11 ] diff --git a/tests/init/neg/cycle-structure.check b/tests/init/neg/cycle-structure.check index 2838532e08ed..788d010d39d2 100644 --- a/tests/init/neg/cycle-structure.check +++ b/tests/init/neg/cycle-structure.check @@ -1,15 +1,21 @@ -- Error: tests/init/neg/cycle-structure.scala:2:15 -------------------------------------------------------------------- 2 | val x1 = b.x // error | ^^^ - | Access field A.this.b.x on a value with an unknown initialization status. Calling trace: + | Access field on a value with an unknown initialization status. Calling trace: + | -> case class B(a: A) { [ cycle-structure.scala:7 ] + | ^ | -> val x = A(this) [ cycle-structure.scala:9 ] | ^^^^^^^ | -> case class A(b: B) { [ cycle-structure.scala:1 ] | ^ + | -> val x1 = b.x // error [ cycle-structure.scala:2 ] + | ^^^ -- Error: tests/init/neg/cycle-structure.scala:8:15 -------------------------------------------------------------------- 8 | val x1 = a.x // error | ^^^ - | Access field B.this.a.x on a value with an unknown initialization status. Calling trace: + | Access field on a value with an unknown initialization status. Calling trace: + | -> case class B(a: A) { [ cycle-structure.scala:7 ] + | ^ | -> val x = A(this) [ cycle-structure.scala:9 ] | ^^^^^^^ | -> case class A(b: B) { [ cycle-structure.scala:1 ] @@ -18,3 +24,5 @@ | ^^^^^^^ | -> case class B(a: A) { [ cycle-structure.scala:7 ] | ^ + | -> val x1 = a.x // error [ cycle-structure.scala:8 ] + | ^^^ diff --git a/tests/init/neg/default-this.check b/tests/init/neg/default-this.check index fc93c740350c..cccfa47a1fe7 100644 --- a/tests/init/neg/default-this.check +++ b/tests/init/neg/default-this.check @@ -2,7 +2,11 @@ 9 | compare() // error | ^^^^^^^ | Cannot prove the argument is fully initialized. Only fully initialized values are safe to leak. Calling trace: + | -> class B extends A { [ default-this.scala:6 ] + | ^ | -> val result = updateThenCompare(5) [ default-this.scala:11 ] | ^^^^^^^^^^^^^^^^^^^^ + | -> def updateThenCompare(c: Int): Boolean = { [ default-this.scala:7 ] + | ^ | -> compare() // error [ default-this.scala:9 ] | ^^^^^^^ diff --git a/tests/init/neg/inherit-non-hot.check b/tests/init/neg/inherit-non-hot.check index 462143a5d159..59826db6c86a 100644 --- a/tests/init/neg/inherit-non-hot.check +++ b/tests/init/neg/inherit-non-hot.check @@ -2,16 +2,18 @@ 6 | if b == null then b = new B(this) // error | ^^^^^^^^^^^ | The RHS of an assignment to a field must be fully initialized. Calling trace: + | -> object Foo { [ inherit-non-hot.scala:2 ] + | ^ | -> val c = new C [ inherit-non-hot.scala:19 ] | ^^^^^ | -> class C extends A { [ inherit-non-hot.scala:15 ] | ^ | -> val bAgain = toB.getBAgain [ inherit-non-hot.scala:16 ] | ^^^ + | -> def toB: B = [ inherit-non-hot.scala:5 ] + | ^ | -> if b == null then b = new B(this) // error [ inherit-non-hot.scala:6 ] | ^^^^^^^^^^^ | | Promoting the value to fully initialized failed due to the following problem: - | Cannot prove that the field val a is fully initialized. Calling trace: - | -> if b == null then b = new B(this) // error [ inherit-non-hot.scala:6 ] - | ^^^^^^^^^^^ + | Cannot prove that the field val a is fully initialized. diff --git a/tests/init/neg/inlined-method.check b/tests/init/neg/inlined-method.check index a2119050a83c..72637948ceb9 100644 --- a/tests/init/neg/inlined-method.check +++ b/tests/init/neg/inlined-method.check @@ -2,6 +2,8 @@ 8 | scala.runtime.Scala3RunTime.assertFailed(message) // error | ^^^^^^^ | Cannot prove the argument is fully initialized. Only fully initialized values are safe to leak. Calling trace: + | -> class InlineError { [ inlined-method.scala:1 ] + | ^ | -> Assertion.failAssert(this) [ inlined-method.scala:2 ] | ^^^^^^^^^^^^^^^^^^^^^^^^^^ | -> scala.runtime.Scala3RunTime.assertFailed(message) // error [ inlined-method.scala:8 ] diff --git a/tests/init/neg/leak-warm.check b/tests/init/neg/leak-warm.check index dd5b8cad92dc..5cc284d885bc 100644 --- a/tests/init/neg/leak-warm.check +++ b/tests/init/neg/leak-warm.check @@ -1,9 +1,8 @@ -- Error: tests/init/neg/leak-warm.scala:19:18 ------------------------------------------------------------------------- 19 | val l2 = l.map(_.m()) // error | ^^^^^^^^^^^^ - | Call method leakWarm.l.map[leakWarm.A#B]( - | { - | def $anonfun(_$1: leakWarm.A): leakWarm.A#B = _$1.m() - | closure($anonfun) - | } - | ) on a value with an unknown initialization. + | Call method on a value with an unknown initialization. Calling trace: + | -> object leakWarm { [ leak-warm.scala:1 ] + | ^ + | -> val l2 = l.map(_.m()) // error [ leak-warm.scala:19 ] + | ^^^^^^^^^^^^ diff --git a/tests/init/neg/local-warm4.check b/tests/init/neg/local-warm4.check index b3551f219e9e..ba96e8674135 100644 --- a/tests/init/neg/local-warm4.check +++ b/tests/init/neg/local-warm4.check @@ -2,6 +2,8 @@ 18 | a = newA // error | ^^^^ | The RHS of an assignment to a field must be fully initialized. Calling trace: + | -> object localWarm { [ local-warm4.scala:1 ] + | ^ | -> val a = new A(5) [ local-warm4.scala:26 ] | ^^^^^^^^ | -> class A(x: Int) extends Foo(x) { [ local-warm4.scala:6 ] @@ -9,12 +11,16 @@ | -> val b = new B(y) [ local-warm4.scala:10 ] | ^^^^^^^^ | -> class B(x: Int) extends A(x) { [ local-warm4.scala:13 ] - | ^^^^ + | ^ | -> class A(x: Int) extends Foo(x) { [ local-warm4.scala:6 ] | ^ | -> increment() [ local-warm4.scala:9 ] | ^^^^^^^^^^^ + | -> override def increment(): Unit = { [ local-warm4.scala:15 ] + | ^ | -> updateA() [ local-warm4.scala:21 ] | ^^^^^^^^^ + | -> def updateA(): Unit = { [ local-warm4.scala:16 ] + | ^ | -> a = newA // error [ local-warm4.scala:18 ] | ^^^^ diff --git a/tests/init/neg/promotion-loop.check b/tests/init/neg/promotion-loop.check index 2398b118050e..5fae90f0ebfa 100644 --- a/tests/init/neg/promotion-loop.check +++ b/tests/init/neg/promotion-loop.check @@ -1,8 +1,11 @@ -- Error: tests/init/neg/promotion-loop.scala:16:10 -------------------------------------------------------------------- 16 | println(b) // error | ^ - | Cannot prove the argument is fully initialized. Only fully initialized values are safe to leak. - | Promoting the value to fully initialized failed due to the following problem: - | Cannot prove that the field val outer is fully initialized. Calling trace: - | -> println(b) // error [ promotion-loop.scala:16 ] - | ^ + | Cannot prove the argument is fully initialized. Only fully initialized values are safe to leak. Calling trace: + | -> class Test { test => [ promotion-loop.scala:1 ] + | ^ + | -> println(b) // error [ promotion-loop.scala:16 ] + | ^ + | + | Promoting the value to fully initialized failed due to the following problem: + | Cannot prove that the field val outer is fully initialized. diff --git a/tests/init/neg/t3273.check b/tests/init/neg/t3273.check index 331db32b72bc..2531ffd3b386 100644 --- a/tests/init/neg/t3273.check +++ b/tests/init/neg/t3273.check @@ -1,16 +1,22 @@ -- Error: tests/init/neg/t3273.scala:4:42 ------------------------------------------------------------------------------ 4 | val num1: LazyList[Int] = 1 #:: num1.map(_ + 1) // error | ^^^^^^^^^^^^^^^ - | Cannot prove the argument is fully initialized. Only fully initialized values are safe to leak. - | Promoting the value to fully initialized failed due to the following problem: - | Access non-initialized value num1. Calling trace: - | -> val num1: LazyList[Int] = 1 #:: num1.map(_ + 1) // error [ t3273.scala:4 ] - | ^^^^ + | Cannot prove the argument is fully initialized. Only fully initialized values are safe to leak. Calling trace: + | -> object Test { [ t3273.scala:3 ] + | ^ + | -> val num1: LazyList[Int] = 1 #:: num1.map(_ + 1) // error [ t3273.scala:4 ] + | ^^^^^^^^^^^^^^^ + | + | Promoting the value to fully initialized failed due to the following problem: + | Access non-initialized value num1. -- Error: tests/init/neg/t3273.scala:5:61 ------------------------------------------------------------------------------ 5 | val num2: LazyList[Int] = 1 #:: num2.iterator.map(_ + 1).to(LazyList) // error | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | Cannot prove the argument is fully initialized. Only fully initialized values are safe to leak. - | Promoting the value to fully initialized failed due to the following problem: - | Access non-initialized value num2. Calling trace: - | -> val num2: LazyList[Int] = 1 #:: num2.iterator.map(_ + 1).to(LazyList) // error [ t3273.scala:5 ] - | ^^^^ + | Cannot prove the argument is fully initialized. Only fully initialized values are safe to leak. Calling trace: + | -> object Test { [ t3273.scala:3 ] + | ^ + | -> val num2: LazyList[Int] = 1 #:: num2.iterator.map(_ + 1).to(LazyList) // error [ t3273.scala:5 ] + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + | + | Promoting the value to fully initialized failed due to the following problem: + | Access non-initialized value num2. diff --git a/tests/init/neg/unsound1.check b/tests/init/neg/unsound1.check index 54e24546845c..b1fc520d680b 100644 --- a/tests/init/neg/unsound1.check +++ b/tests/init/neg/unsound1.check @@ -1,4 +1,8 @@ -- Error: tests/init/neg/unsound1.scala:2:35 --------------------------------------------------------------------------- 2 | if (m > 0) println(foo(m - 1).a2.n) // error | ^^^^^^^^^^^^^^^ - | Access field A.this.foo(A.this.m.-(1)).a2.n on a value with an unknown initialization status. + | Access field on a value with an unknown initialization status. Calling trace: + | -> class A(m: Int) { [ unsound1.scala:1 ] + | ^ + | -> if (m > 0) println(foo(m - 1).a2.n) // error [ unsound1.scala:2 ] + | ^^^^^^^^^^^^^^^ diff --git a/tests/init/neg/unsound2.check b/tests/init/neg/unsound2.check index 3e54219e0819..f8066e791e76 100644 --- a/tests/init/neg/unsound2.check +++ b/tests/init/neg/unsound2.check @@ -1,8 +1,12 @@ -- Error: tests/init/neg/unsound2.scala:5:26 --------------------------------------------------------------------------- 5 | def getN: Int = a.n // error | ^^^ - | Access field B.this.a.n on a value with an unknown initialization status. Calling trace: + | Access field on a value with an unknown initialization status. Calling trace: + | -> case class A(x: Int) { [ unsound2.scala:1 ] + | ^ | -> println(foo(x).getB) [ unsound2.scala:8 ] | ^^^^^^ | -> def foo(y: Int): B = if (y > 10) then B(bar(y - 1), foo(y - 1).getN) else B(bar(y), 10) [ unsound2.scala:2 ] | ^^^^^^^^^^^^^^^ + | -> def getN: Int = a.n // error [ unsound2.scala:5 ] + | ^^^ diff --git a/tests/init/neg/unsound3.check b/tests/init/neg/unsound3.check index 6f8a440f186e..d17a96220128 100644 --- a/tests/init/neg/unsound3.check +++ b/tests/init/neg/unsound3.check @@ -1,6 +1,12 @@ -- Error: tests/init/neg/unsound3.scala:10:38 -------------------------------------------------------------------------- 10 | if (x < 12) then foo().getC().b else newB // error | ^^^^^^^^^^^^^^ - | Access field C.this.foo().getC().b on a value with an unknown initialization status. Calling trace: - | -> val b = foo() [ unsound3.scala:12 ] - | ^^^^^ + | Access field on a value with an unknown initialization status. Calling trace: + | -> class C { [ unsound3.scala:5 ] + | ^ + | -> val b = foo() [ unsound3.scala:12 ] + | ^^^^^ + | -> def foo(): B = { [ unsound3.scala:7 ] + | ^ + | -> if (x < 12) then foo().getC().b else newB // error [ unsound3.scala:10 ] + | ^^^^^^^^^^^^^^ diff --git a/tests/init/neg/unsound4.check b/tests/init/neg/unsound4.check index c09e878c8e42..9b356b35a3c2 100644 --- a/tests/init/neg/unsound4.check +++ b/tests/init/neg/unsound4.check @@ -2,6 +2,8 @@ 3 | val aAgain = foo(5) // error | ^ | Access non-initialized value aAgain. Calling trace: + | -> class A { [ unsound4.scala:1 ] + | ^ | -> val aAgain = foo(5) // error [ unsound4.scala:3 ] | ^^^^^^ | -> def foo(x: Int): A = if (x < 5) then this else foo(x - 1).aAgain [ unsound4.scala:2 ] From 73e64a771022fc4f55a1fa1d81323cd546eeab76 Mon Sep 17 00:00:00 2001 From: Fengyun Liu Date: Wed, 25 May 2022 08:36:22 +0200 Subject: [PATCH 09/11] Fix missing stacktrace item --- .../src/dotty/tools/dotc/transform/init/Errors.scala | 9 ++------- .../src/dotty/tools/dotc/transform/init/Semantic.scala | 2 +- tests/init/neg/t3273.check | 8 ++++++-- 3 files changed, 9 insertions(+), 10 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/transform/init/Errors.scala b/compiler/src/dotty/tools/dotc/transform/init/Errors.scala index 8db962d7e823..e05ecbcd924b 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Errors.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Errors.scala @@ -21,10 +21,7 @@ object Errors: def issue(using Context): Unit = report.warning(show + stacktrace, this.pos) - private def isTraceInformative(using Context): Boolean = - trace.size > 1 || trace.size == 1 && trace.head.sourcePos.ne(pos) - - def stacktrace(using Context): String = if !isTraceInformative then "" else " Calling trace:\n" + { + def stacktrace(using Context): String = if trace.isEmpty then "" else " Calling trace:\n" + { var lastLineNum = -1 var lines: mutable.ArrayBuffer[String] = new mutable.ArrayBuffer trace.foreach { tree => @@ -107,10 +104,8 @@ object Errors: override def issue(using Context): Unit = report.warning(show, this.pos) - def show(using Context): String = { - var index = 0 + def show(using Context): String = msg + stacktrace + "\n" + "Promoting the value to fully initialized failed due to the following problem:\n" + error.show + error.stacktrace - } } diff --git a/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala b/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala index 763c378a5a24..77c6bd20b5de 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala @@ -1145,7 +1145,7 @@ object Semantic { case id @ Ident(name) if !id.symbol.is(Flags.Method) => assert(name.isTermName, "type trees should not reach here") - cases(expr.tpe, thisV, klass) + withTrace(trace2) { cases(expr.tpe, thisV, klass) } case NewExpr(tref, New(tpt), ctor, argss) => // check args diff --git a/tests/init/neg/t3273.check b/tests/init/neg/t3273.check index 2531ffd3b386..68352dab06c2 100644 --- a/tests/init/neg/t3273.check +++ b/tests/init/neg/t3273.check @@ -8,7 +8,9 @@ | ^^^^^^^^^^^^^^^ | | Promoting the value to fully initialized failed due to the following problem: - | Access non-initialized value num1. + | Access non-initialized value num1. Calling trace: + | -> val num1: LazyList[Int] = 1 #:: num1.map(_ + 1) // error [ t3273.scala:4 ] + | ^^^^ -- Error: tests/init/neg/t3273.scala:5:61 ------------------------------------------------------------------------------ 5 | val num2: LazyList[Int] = 1 #:: num2.iterator.map(_ + 1).to(LazyList) // error | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -19,4 +21,6 @@ | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | | Promoting the value to fully initialized failed due to the following problem: - | Access non-initialized value num2. + | Access non-initialized value num2. Calling trace: + | -> val num2: LazyList[Int] = 1 #:: num2.iterator.map(_ + 1).to(LazyList) // error [ t3273.scala:5 ] + | ^^^^ From 7c12df044474bff8c95d6162af6660fe46eaefc7 Mon Sep 17 00:00:00 2001 From: Fengyun Liu Date: Wed, 25 May 2022 08:37:49 +0200 Subject: [PATCH 10/11] Tweak message: make clear it is reassignment --- compiler/src/dotty/tools/dotc/transform/init/Semantic.scala | 4 ++-- tests/init/neg/inherit-non-hot.check | 2 +- tests/init/neg/local-warm4.check | 2 +- 3 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 77c6bd20b5de..05f01e14c65a 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala @@ -1221,12 +1221,12 @@ object Semantic { eval(qual, thisV, klass) val res = eval(rhs, thisV, klass) extendTrace(rhs) { - res.ensureHot("The RHS of an assignment to a field must be fully initialized.") + res.ensureHot("The RHS of reassignment must be fully initialized.") } case id: Ident => val res = eval(rhs, thisV, klass) extendTrace(rhs) { - res.ensureHot("The RHS of an assignment to a field must be fully initialized.") + res.ensureHot("The RHS of reassignment must be fully initialized.") } case closureDef(ddef) => diff --git a/tests/init/neg/inherit-non-hot.check b/tests/init/neg/inherit-non-hot.check index 59826db6c86a..1669e423d0d1 100644 --- a/tests/init/neg/inherit-non-hot.check +++ b/tests/init/neg/inherit-non-hot.check @@ -1,7 +1,7 @@ -- Error: tests/init/neg/inherit-non-hot.scala:6:34 -------------------------------------------------------------------- 6 | if b == null then b = new B(this) // error | ^^^^^^^^^^^ - | The RHS of an assignment to a field must be fully initialized. Calling trace: + | The RHS of reassignment must be fully initialized. Calling trace: | -> object Foo { [ inherit-non-hot.scala:2 ] | ^ | -> val c = new C [ inherit-non-hot.scala:19 ] diff --git a/tests/init/neg/local-warm4.check b/tests/init/neg/local-warm4.check index ba96e8674135..6a9955c40801 100644 --- a/tests/init/neg/local-warm4.check +++ b/tests/init/neg/local-warm4.check @@ -1,7 +1,7 @@ -- Error: tests/init/neg/local-warm4.scala:18:20 ----------------------------------------------------------------------- 18 | a = newA // error | ^^^^ - | The RHS of an assignment to a field must be fully initialized. Calling trace: + | The RHS of reassignment must be fully initialized. Calling trace: | -> object localWarm { [ local-warm4.scala:1 ] | ^ | -> val a = new A(5) [ local-warm4.scala:26 ] From ce3c803f71985a61eb8a9f0a89478f08cf62a632 Mon Sep 17 00:00:00 2001 From: Fengyun Liu Date: Wed, 25 May 2022 15:08:42 +0200 Subject: [PATCH 11/11] Address review comments --- .../tools/dotc/transform/init/Semantic.scala | 19 ++++++++++--------- 1 file changed, 10 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 05f01e14c65a..6126ca6587d0 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala @@ -423,6 +423,7 @@ object Semantic { /** Error reporting */ trait Reporter: def report(err: Error): Unit + def reportAll(errs: Seq[Error]): Unit = for err <- errs do report(err) object Reporter: class BufferedReporter extends Reporter: @@ -600,7 +601,7 @@ object Semantic { } def call(meth: Symbol, args: List[ArgInfo], receiver: Type, superType: Type, needResolve: Boolean = true): Contextual[Value] = log("call " + meth.show + ", args = " + args, printer, (_: Value).show) { - def checkArgs = args.foreach(_.promote) + def promoteArgs(): Contextual[Unit] = args.foreach(_.promote) def isSyntheticApply(meth: Symbol) = meth.is(Flags.Synthetic) @@ -648,16 +649,16 @@ object Semantic { if allArgsPromote then Hot: Value else if errors.nonEmpty then - for error <- errors do reporter.report(error) + reporter.reportAll(errors) Hot: Value else Cold: Value else - checkArgs + promoteArgs() Hot case Cold => - checkArgs + promoteArgs() val error = CallCold(meth, trace.toVector) reporter.report(error) Hot @@ -676,7 +677,7 @@ object Semantic { if target.hasSource then val cls = target.owner.enclosingClass.asClass val ddef = target.defTree.asInstanceOf[DefDef] - val argErrors = Reporter.errorsIn { args.foreach(_.promote) } + val argErrors = Reporter.errorsIn { promoteArgs() } // normal method call if argErrors.nonEmpty && isSyntheticApply(meth) then val klass = meth.owner.companionClass.asClass @@ -684,7 +685,7 @@ object Semantic { val outer = resolveOuterSelect(outerCls, ref, 1) outer.instantiate(klass, klass.primaryConstructor, args) else - for error <- argErrors do reporter.report(error) + reporter.reportAll(argErrors) withEnv(if isLocal then env else Env.empty) { extendTrace(ddef) { eval(ddef.rhs, ref, cls, cacheResult = true) @@ -694,7 +695,7 @@ object Semantic { Hot else // no source code available - checkArgs + promoteArgs() val error = CallUnknown(target, trace.toVector) reporter.report(error) Hot @@ -710,7 +711,7 @@ object Semantic { // meth == NoSymbol for poly functions if meth.name.toString == "tupled" then value // a call like `fun.tupled` else - checkArgs + promoteArgs() withEnv(env) { eval(body, thisV, klass, cacheResult = true) } @@ -932,7 +933,7 @@ object Semantic { promoted.add(warm) val errors = warm.tryPromote(msg) if errors.nonEmpty then promoted.remove(warm) - for error <- errors do reporter.report(error) + reporter.reportAll(errors) case fun @ Fun(body, thisV, klass, env) => if !promoted.contains(fun) then