Skip to content

Commit bcc3d2b

Browse files
authored
Merge pull request #14283 from dotty-staging/init-synth-apply
Make initialization checker see through synthetic applys
2 parents 0e4e5d5 + 3b38f12 commit bcc3d2b

File tree

6 files changed

+99
-27
lines changed

6 files changed

+99
-27
lines changed

compiler/src/dotty/tools/dotc/transform/init/Semantic.scala

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -559,6 +559,7 @@ object Semantic {
559559
if obj.hasField(target) then
560560
Result(obj.field(target), Nil)
561561
else if ref.isInstanceOf[Warm] then
562+
assert(obj.klass.isSubClass(target.owner))
562563
if target.is(Flags.ParamAccessor) then
563564
// possible for trait parameters
564565
// see tests/init/neg/trait2.scala
@@ -590,11 +591,20 @@ object Semantic {
590591
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) {
591592
def checkArgs = args.flatMap(_.promote)
592593

594+
def isSyntheticApply(meth: Symbol) =
595+
meth.is(Flags.Synthetic)
596+
&& meth.owner.is(Flags.Module)
597+
&& meth.owner.companionClass.is(Flags.Case)
598+
593599
// fast track if the current object is already initialized
594600
if promoted.isCurrentObjectPromoted then Result(Hot, Nil)
595601
else value match {
596602
case Hot =>
597-
Result(Hot, checkArgs)
603+
if isSyntheticApply(meth) then
604+
val klass = meth.owner.companionClass.asClass
605+
instantiate(klass, klass.primaryConstructor, args, source)
606+
else
607+
Result(Hot, checkArgs)
598608

599609
case Cold =>
600610
val error = CallCold(meth, source, trace.toVector)
@@ -616,11 +626,17 @@ object Semantic {
616626
given Trace = trace1
617627
val cls = target.owner.enclosingClass.asClass
618628
val ddef = target.defTree.asInstanceOf[DefDef]
619-
val env2 = Env(ddef, args.map(_.value).widenArgs)
629+
val argErrors = checkArgs
620630
// normal method call
621-
withEnv(if isLocal then env else Env.empty) {
622-
eval(ddef.rhs, ref, cls, cacheResult = true) ++ checkArgs
623-
}
631+
if argErrors.nonEmpty && isSyntheticApply(meth) then
632+
val klass = meth.owner.companionClass.asClass
633+
val outerCls = klass.owner.lexicallyEnclosingClass.asClass
634+
val outer = resolveOuterSelect(outerCls, ref, 1, source)
635+
outer.instantiate(klass, klass.primaryConstructor, args, source)
636+
else
637+
withEnv(if isLocal then env else Env.empty) {
638+
eval(ddef.rhs, ref, cls, cacheResult = true) ++ argErrors
639+
}
624640
else if ref.canIgnoreMethodCall(target) then
625641
Result(Hot, Nil)
626642
else
@@ -1309,9 +1325,10 @@ object Semantic {
13091325
*/
13101326
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) {
13111327
// Is `target` reachable from `cls` with the given `hops`?
1312-
def reachable(cls: ClassSymbol, hops: Int): Boolean =
1328+
def reachable(cls: ClassSymbol, hops: Int): Boolean = log("reachable from " + cls + " -> " + target + " in " + hops, printer) {
13131329
if hops == 0 then cls == target
1314-
else reachable(cls.lexicallyEnclosingClass.asClass, hops - 1)
1330+
else reachable(cls.owner.lexicallyEnclosingClass.asClass, hops - 1)
1331+
}
13151332

13161333
thisV match
13171334
case Hot => Hot

docs/docs/reference/other-new-features/safe-initialization.md

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -194,14 +194,6 @@ With the established principles and design goals, following rules are imposed:
194194
non-initialized object is not used, i.e. calling methods or accessing fields
195195
on the escaped object is not allowed.
196196

197-
3. Local definitions may only refer to transitively initialized objects.
198-
199-
It means that in a local definition `val x: T = e`, the expression `e` may
200-
only evaluate to transitively initialized objects. The same goes for local
201-
lazy variables and methods. This rule is again motivated for simplicity in
202-
reasoning about initialization: programmers may safely assume that all local
203-
definitions only point to transitively initialized objects.
204-
205197
## Modularity
206198

207199
The analysis takes the primary constructor of concrete classes as entry points.

tests/init/neg/apply.scala

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
case class A(b: B)
2+
3+
object A:
4+
def foo(b: B) = new A(b)
5+
inline def bar(b: B) = new A(b)
6+
7+
class B:
8+
val a = A(this)
9+
val a2 = A.foo(this) // error
10+
val a3 = A.bar(this)
11+
12+
// test receiver is ThisRef
13+
14+
object O:
15+
case class A(b: B)
16+
17+
object A:
18+
def foo(b: B) = new A(b)
19+
inline def bar(b: B) = new A(b)
20+
21+
class B:
22+
val a = A(this)
23+
val a2 = A.foo(this) // error
24+
val a3 = A.bar(this)
25+
26+
val b = new B
27+
end O
28+
29+
30+
// test receiver is Warm
31+
32+
class M(n: N):
33+
case class A(b: B)
34+
35+
object A:
36+
def foo(b: B) = new A(b)
37+
inline def bar(b: B) = new A(b)
38+
39+
class B:
40+
val a = A(this)
41+
val a2 = A.foo(this) // error
42+
val a3 = A.bar(this)
43+
end M
44+
45+
class N:
46+
val m = new M(this)
47+
val b = new m.B

tests/init/neg/apply2.scala

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
object O:
2+
case class A(b: B):
3+
println(n)
4+
5+
class B:
6+
val a = A(this)
7+
8+
val b = new B
9+
val n = 10 // error
10+
end O

tests/init/neg/cycle-structure.check

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,14 @@
1-
-- Error: tests/init/neg/cycle-structure.scala:9:14 --------------------------------------------------------------------
2-
9 | val x = A(this) // error
3-
| ^^^^
4-
| Cannot prove that the value is fully initialized. May only use initialized value as arguments.
5-
-- Error: tests/init/neg/cycle-structure.scala:3:14 --------------------------------------------------------------------
6-
3 | val x = B(this) // error
7-
| ^^^^
8-
| Cannot prove that the value is fully initialized. May only use initialized value as arguments.
1+
-- Error: tests/init/neg/cycle-structure.scala:2:15 --------------------------------------------------------------------
2+
2 | val x1 = b.x // error
3+
| ^^^
4+
| Access field A.this.b.x on a value with an unknown initialization status. Calling trace:
5+
| -> val x = A(this) [ cycle-structure.scala:9 ]
6+
| -> case class A(b: B) { [ cycle-structure.scala:1 ]
7+
-- Error: tests/init/neg/cycle-structure.scala:8:15 --------------------------------------------------------------------
8+
8 | val x1 = a.x // error
9+
| ^^^
10+
| Access field B.this.a.x on a value with an unknown initialization status. Calling trace:
11+
| -> val x = A(this) [ cycle-structure.scala:9 ]
12+
| -> case class A(b: B) { [ cycle-structure.scala:1 ]
13+
| -> val x = B(this) [ cycle-structure.scala:3 ]
14+
| -> case class B(a: A) { [ cycle-structure.scala:7 ]

tests/init/neg/cycle-structure.scala

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
case class A(b: B) {
2-
val x1 = b.x
3-
val x = B(this) // error
2+
val x1 = b.x // error
3+
val x = B(this)
44
val y = x.a
55
}
66

77
case class B(a: A) {
8-
val x1 = a.x
9-
val x = A(this) // error
8+
val x1 = a.x // error
9+
val x = A(this)
1010
val h = x.b
1111
}

0 commit comments

Comments
 (0)