diff --git a/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala b/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala index 88794c609805..fba7a6d531b1 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 @@ -590,11 +591,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 +626,17 @@ 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) + val argErrors = checkArgs // normal method call - withEnv(if isLocal then env else Env.empty) { - eval(ddef.rhs, ref, cls, cacheResult = true) ++ checkArgs - } + 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) + else + withEnv(if isLocal then env else Env.empty) { + eval(ddef.rhs, ref, cls, cacheResult = true) ++ argErrors + } else if ref.canIgnoreMethodCall(target) then Result(Hot, Nil) else @@ -1309,9 +1325,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 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 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 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 }