From 28a8761b4eec19312ea8fccd455ed619caacca3c Mon Sep 17 00:00:00 2001 From: Fengyun Liu Date: Mon, 17 Jan 2022 07:13:50 +0100 Subject: [PATCH 1/7] Treat synthetic apply of case class as new expression --- .../tools/dotc/transform/init/Semantic.scala | 22 +++++++-- .../other-new-features/safe-initialization.md | 8 ---- tests/init/neg/apply.scala | 47 +++++++++++++++++++ 3 files changed, 64 insertions(+), 13 deletions(-) create mode 100644 tests/init/neg/apply.scala diff --git a/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala b/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala index 88794c609805..053cd4e119d1 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala @@ -590,11 +590,20 @@ object Semantic { def call(meth: Symbol, args: List[ArgInfo], superType: Type, source: Tree, needResolve: Boolean = true): Contextual[Result] = log("call " + meth.show + ", args = " + args, printer, (_: Result).show) { def checkArgs = args.flatMap(_.promote) + def isSyntheticApply(meth: Symbol) = + meth.is(Flags.Synthetic) + && meth.owner.is(Flags.Module) + && meth.owner.companionClass.is(Flags.Case) + // fast track if the current object is already initialized if promoted.isCurrentObjectPromoted then Result(Hot, Nil) else value match { case Hot => - Result(Hot, checkArgs) + if isSyntheticApply(meth) then + val klass = meth.owner.companionClass.asClass + instantiate(klass, klass.primaryConstructor, args, source) + else + Result(Hot, checkArgs) case Cold => val error = CallCold(meth, source, trace.toVector) @@ -616,11 +625,14 @@ object Semantic { given Trace = trace1 val cls = target.owner.enclosingClass.asClass val ddef = target.defTree.asInstanceOf[DefDef] - val env2 = Env(ddef, args.map(_.value).widenArgs) // normal method call - withEnv(if isLocal then env else Env.empty) { - eval(ddef.rhs, ref, cls, cacheResult = true) ++ checkArgs - } + if isSyntheticApply(meth) then + val klass = meth.owner.companionClass.asClass + instantiate(klass, klass.primaryConstructor, args, source) + else + withEnv(if isLocal then env else Env.empty) { + eval(ddef.rhs, ref, cls, cacheResult = true) ++ checkArgs + } else if ref.canIgnoreMethodCall(target) then Result(Hot, Nil) else diff --git a/docs/docs/reference/other-new-features/safe-initialization.md b/docs/docs/reference/other-new-features/safe-initialization.md index e56c640dd26b..57c962c9f5e8 100644 --- a/docs/docs/reference/other-new-features/safe-initialization.md +++ b/docs/docs/reference/other-new-features/safe-initialization.md @@ -194,14 +194,6 @@ With the established principles and design goals, following rules are imposed: non-initialized object is not used, i.e. calling methods or accessing fields on the escaped object is not allowed. -3. Local definitions may only refer to transitively initialized objects. - - It means that in a local definition `val x: T = e`, the expression `e` may - only evaluate to transitively initialized objects. The same goes for local - lazy variables and methods. This rule is again motivated for simplicity in - reasoning about initialization: programmers may safely assume that all local - definitions only point to transitively initialized objects. - ## Modularity The analysis takes the primary constructor of concrete classes as entry points. diff --git a/tests/init/neg/apply.scala b/tests/init/neg/apply.scala new file mode 100644 index 000000000000..2847115cc73c --- /dev/null +++ b/tests/init/neg/apply.scala @@ -0,0 +1,47 @@ +case class A(b: B) + +object A: + def foo(b: B) = new A(b) + inline def bar(b: B) = new A(b) + +class B: + val a = A(this) + val a2 = A.foo(this) // error + val a3 = A.bar(this) + +// test receiver is ThisRef + +object O: + case class A(b: B) + + object A: + def foo(b: B) = new A(b) + inline def bar(b: B) = new A(b) + + class B: + val a = A(this) + val a2 = A.foo(this) // error + val a3 = A.bar(this) + + val b = new B +end O + + +// test receiver is Warm + +class M(n: N): + case class A(b: B) + + object A: + def foo(b: B) = new A(b) + inline def bar(b: B) = new A(b) + + class B: + val a = A(this) + val a2 = A.foo(this) // error + val a3 = A.bar(this) +end M + +class N: + val m = new M(this) + val b = new m.B From 28cb972e1123736bfa155b02efeb6596304810c8 Mon Sep 17 00:00:00 2001 From: Fengyun Liu Date: Mon, 17 Jan 2022 07:41:33 +0100 Subject: [PATCH 2/7] Fix outer selection --- compiler/src/dotty/tools/dotc/transform/init/Semantic.scala | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala b/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala index 053cd4e119d1..234cb02a21d8 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala @@ -1321,9 +1321,10 @@ object Semantic { */ 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) { // Is `target` reachable from `cls` with the given `hops`? - def reachable(cls: ClassSymbol, hops: Int): Boolean = + def reachable(cls: ClassSymbol, hops: Int): Boolean = log("reachable from " + cls + " -> " + target + " in " + hops, printer) { if hops == 0 then cls == target - else reachable(cls.lexicallyEnclosingClass.asClass, hops - 1) + else reachable(cls.owner.lexicallyEnclosingClass.asClass, hops - 1) + } thisV match case Hot => Hot From 1790ef146c337284e5da6add4b813ce9762248c2 Mon Sep 17 00:00:00 2001 From: Fengyun Liu Date: Mon, 17 Jan 2022 07:46:00 +0100 Subject: [PATCH 3/7] Fix test: synthetic apply is now transparent --- tests/init/neg/cycle-structure.check | 22 ++++++++++++++-------- tests/init/neg/cycle-structure.scala | 8 ++++---- 2 files changed, 18 insertions(+), 12 deletions(-) diff --git a/tests/init/neg/cycle-structure.check b/tests/init/neg/cycle-structure.check index b370b16f4279..79e38ee80bb4 100644 --- a/tests/init/neg/cycle-structure.check +++ b/tests/init/neg/cycle-structure.check @@ -1,8 +1,14 @@ --- Error: tests/init/neg/cycle-structure.scala:9:14 -------------------------------------------------------------------- -9 | val x = A(this) // error - | ^^^^ - | Cannot prove that the value is fully initialized. May only use initialized value as arguments. --- Error: tests/init/neg/cycle-structure.scala:3:14 -------------------------------------------------------------------- -3 | val x = B(this) // error - | ^^^^ - | Cannot prove that the value is fully initialized. May only use initialized value as arguments. +-- 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: + | -> val x = A(this) [ cycle-structure.scala:9 ] + | -> case class A(b: B) { [ cycle-structure.scala:1 ] +-- 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: + | -> val x = A(this) [ cycle-structure.scala:9 ] + | -> case class A(b: B) { [ cycle-structure.scala:1 ] + | -> val x = B(this) [ cycle-structure.scala:3 ] + | -> case class B(a: A) { [ cycle-structure.scala:7 ] diff --git a/tests/init/neg/cycle-structure.scala b/tests/init/neg/cycle-structure.scala index acf889a33c1d..937df774ee14 100644 --- a/tests/init/neg/cycle-structure.scala +++ b/tests/init/neg/cycle-structure.scala @@ -1,11 +1,11 @@ case class A(b: B) { - val x1 = b.x - val x = B(this) // error + val x1 = b.x // error + val x = B(this) val y = x.a } case class B(a: A) { - val x1 = a.x - val x = A(this) // error + val x1 = a.x // error + val x = A(this) val h = x.b } From e5cd6fab4bedce28020e0ba6a8fa2ef6d03e87ed Mon Sep 17 00:00:00 2001 From: Fengyun Liu Date: Mon, 17 Jan 2022 08:05:13 +0100 Subject: [PATCH 4/7] Lazily use synthetic apply Otherwise, as the test tests/init/pos/local-warm5.scala shows, we may reach an outer that is cold. --- compiler/src/dotty/tools/dotc/transform/init/Semantic.scala | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala b/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala index 234cb02a21d8..5ff037272370 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala @@ -625,13 +625,14 @@ object Semantic { given Trace = trace1 val cls = target.owner.enclosingClass.asClass val ddef = target.defTree.asInstanceOf[DefDef] + val argErrors = checkArgs // normal method call - if isSyntheticApply(meth) then + if argErrors.nonEmpty && isSyntheticApply(meth) then val klass = meth.owner.companionClass.asClass instantiate(klass, klass.primaryConstructor, args, source) else withEnv(if isLocal then env else Env.empty) { - eval(ddef.rhs, ref, cls, cacheResult = true) ++ checkArgs + eval(ddef.rhs, ref, cls, cacheResult = true) ++ argErrors } else if ref.canIgnoreMethodCall(target) then Result(Hot, Nil) From d8fb65abdb9e84036b0c3a5fa83bc04ce23ad27a Mon Sep 17 00:00:00 2001 From: Fengyun Liu Date: Fri, 21 Jan 2022 21:22:27 +0100 Subject: [PATCH 5/7] Fix outer for calling instantiate --- .../src/dotty/tools/dotc/transform/init/Semantic.scala | 4 +++- tests/init/neg/apply2.scala | 10 ++++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) create mode 100644 tests/init/neg/apply2.scala diff --git a/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala b/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala index 5ff037272370..333b1ce2be3e 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala @@ -629,7 +629,9 @@ object Semantic { // normal method call if argErrors.nonEmpty && isSyntheticApply(meth) then val klass = meth.owner.companionClass.asClass - instantiate(klass, klass.primaryConstructor, args, source) + val outerCls = klass.owner.lexicallyEnclosingClass.asClass + val outer = resolveOuterSelect(outerCls, ref, 1, source) + Semantic.instantiate(outer)(klass, klass.primaryConstructor, args, source) else withEnv(if isLocal then env else Env.empty) { eval(ddef.rhs, ref, cls, cacheResult = true) ++ argErrors diff --git a/tests/init/neg/apply2.scala b/tests/init/neg/apply2.scala new file mode 100644 index 000000000000..6a42cc9a3acc --- /dev/null +++ b/tests/init/neg/apply2.scala @@ -0,0 +1,10 @@ +object O: + case class A(b: B): + println(n) + + class B: + val a = A(this) + + val b = new B + val n = 10 // error +end O From 01e0b3f57db1540f5a0f92b20171d32eaaf52c70 Mon Sep 17 00:00:00 2001 From: Fengyun Liu Date: Fri, 21 Jan 2022 21:42:14 +0100 Subject: [PATCH 6/7] Add assertion to make sure selection makes sense --- compiler/src/dotty/tools/dotc/transform/init/Semantic.scala | 1 + 1 file changed, 1 insertion(+) diff --git a/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala b/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala index 333b1ce2be3e..d1e9404986a7 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala @@ -559,6 +559,7 @@ object Semantic { if obj.hasField(target) then Result(obj.field(target), Nil) else if ref.isInstanceOf[Warm] then + assert(obj.klass.isSubClass(target.owner)) if target.is(Flags.ParamAccessor) then // possible for trait parameters // see tests/init/neg/trait2.scala From 3b38f122b3ec933b52857a77f0ecb73b2a8ea9ff Mon Sep 17 00:00:00 2001 From: Fengyun Liu Date: Mon, 24 Jan 2022 21:06:56 +0100 Subject: [PATCH 7/7] Use extension method syntax --- compiler/src/dotty/tools/dotc/transform/init/Semantic.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala b/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala index d1e9404986a7..fba7a6d531b1 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala @@ -632,7 +632,7 @@ object Semantic { val klass = meth.owner.companionClass.asClass val outerCls = klass.owner.lexicallyEnclosingClass.asClass val outer = resolveOuterSelect(outerCls, ref, 1, source) - Semantic.instantiate(outer)(klass, klass.primaryConstructor, args, source) + outer.instantiate(klass, klass.primaryConstructor, args, source) else withEnv(if isLocal then env else Env.empty) { eval(ddef.rhs, ref, cls, cacheResult = true) ++ argErrors