From e30411df5f24fe3970e90180123c0dcdc3a3bbdd Mon Sep 17 00:00:00 2001 From: odersky Date: Sun, 13 Jul 2025 13:55:35 +0200 Subject: [PATCH 1/2] Support cleanup actions in class completers Needed to break the loop between completion of class and companion object. If we try to complete the class first, and completion needs the companion object (for instance for processing an import) then the companion object completion would consult the companion class info for constructor that need a constructor proxy in the object. This can lead to a cyclic reference. We now break the cycle by delaying adding constructor proxies in this case to be the last completion action of the companion class. --- .../src/dotty/tools/dotc/core/NamerOps.scala | 27 ++++++++++++++++++- .../tools/dotc/core/tasty/TreeUnpickler.scala | 5 +++- .../src/dotty/tools/dotc/typer/Namer.scala | 4 ++- tests/pos/i22436/atest.scala | 3 +++ tests/pos/i22436/defs.scala | 7 +++++ 5 files changed, 43 insertions(+), 3 deletions(-) create mode 100644 tests/pos/i22436/atest.scala create mode 100644 tests/pos/i22436/defs.scala diff --git a/compiler/src/dotty/tools/dotc/core/NamerOps.scala b/compiler/src/dotty/tools/dotc/core/NamerOps.scala index 82ebb03b9662..91a5dfde1c47 100644 --- a/compiler/src/dotty/tools/dotc/core/NamerOps.scala +++ b/compiler/src/dotty/tools/dotc/core/NamerOps.scala @@ -9,6 +9,25 @@ import TypeApplications.EtaExpansion /** Operations that are shared between Namer and TreeUnpickler */ object NamerOps: + /** A completer supporting cleanup actions. + * Needed to break the loop between completion of class and companion object. + * If we try to complete the class first, and completion needs the companion + * object (for instance for processing an import) then the companion object + * completion would consult the companion class info for constructor that + * need a constructor proxy in the object. This can lead to a cyclic reference. + * We break the cycle by delaying adding constructor proxies to be a cleanuo + * action instead. + */ + trait CompleterWithCleanup extends LazyType: + private var cleanupActions: List[() => Unit] = Nil + def addCleanupAction(op: () => Unit): Unit = + cleanupActions = op :: cleanupActions + def cleanup(): Unit = + if cleanupActions.nonEmpty then + cleanupActions.reverse.foreach(_()) + cleanupActions = Nil + end CompleterWithCleanup + /** The type of the constructed instance is returned * * @param ctor the constructor @@ -118,8 +137,14 @@ object NamerOps: ApplyProxyCompleter(constr), cls.privateWithin, constr.coord) - for dcl <- cls.info.decls do + def doAdd() = for dcl <- cls.info.decls do if dcl.isConstructor then scope.enter(proxy(dcl)) + cls.infoOrCompleter match + case completer: CompleterWithCleanup if cls.is(Touched) => + // Taking the info would lead to a cyclic reference here - delay instead until cleanup of `cls` + completer.addCleanupAction(doAdd) + case _ => + doAdd() scope end addConstructorApplies diff --git a/compiler/src/dotty/tools/dotc/core/tasty/TreeUnpickler.scala b/compiler/src/dotty/tools/dotc/core/tasty/TreeUnpickler.scala index 6cc1d3c33c17..4edbb278504e 100644 --- a/compiler/src/dotty/tools/dotc/core/tasty/TreeUnpickler.scala +++ b/compiler/src/dotty/tools/dotc/core/tasty/TreeUnpickler.scala @@ -127,7 +127,8 @@ class TreeUnpickler(reader: TastyReader, } } - class Completer(reader: TastyReader)(using @constructorOnly _ctx: Context) extends LazyType { + class Completer(reader: TastyReader)(using @constructorOnly _ctx: Context) + extends LazyType, CompleterWithCleanup { import reader.* val owner = ctx.owner val mode = ctx.mode @@ -147,6 +148,8 @@ class TreeUnpickler(reader: TastyReader, case ex: CyclicReference => throw ex case ex: AssertionError => fail(ex) case ex: Exception => fail(ex) + finally + cleanup() } class TreeReader(val reader: TastyReader) { diff --git a/compiler/src/dotty/tools/dotc/typer/Namer.scala b/compiler/src/dotty/tools/dotc/typer/Namer.scala index 75f738abd00e..2269a07ec77c 100644 --- a/compiler/src/dotty/tools/dotc/typer/Namer.scala +++ b/compiler/src/dotty/tools/dotc/typer/Namer.scala @@ -1089,7 +1089,8 @@ class Namer { typer: Typer => end typeSig } - class ClassCompleter(cls: ClassSymbol, original: TypeDef)(ictx: Context) extends Completer(original)(ictx) { + class ClassCompleter(cls: ClassSymbol, original: TypeDef)(ictx: Context) + extends Completer(original)(ictx), CompleterWithCleanup { withDecls(newScope(using ictx)) protected implicit val completerCtx: Context = localContext(cls) @@ -1691,6 +1692,7 @@ class Namer { typer: Typer => processExports(using localCtx) defn.patchStdLibClass(cls) addConstructorProxies(cls) + cleanup() } } diff --git a/tests/pos/i22436/atest.scala b/tests/pos/i22436/atest.scala new file mode 100644 index 000000000000..7f77a5562385 --- /dev/null +++ b/tests/pos/i22436/atest.scala @@ -0,0 +1,3 @@ +object Case1 { + def myProps(transport: ProtocolTransport): Unit = ??? +} diff --git a/tests/pos/i22436/defs.scala b/tests/pos/i22436/defs.scala new file mode 100644 index 000000000000..c4bc3d74df25 --- /dev/null +++ b/tests/pos/i22436/defs.scala @@ -0,0 +1,7 @@ +object ProtocolTransport + +import ProtocolTransport.* + +@annotation.nowarn() +class ProtocolTransport() + From 599a015f3852e1affaa2697e6dff3fd761acdff6 Mon Sep 17 00:00:00 2001 From: Tomasz Godzik Date: Wed, 23 Jul 2025 15:05:26 +0200 Subject: [PATCH 2/2] Support cleanup actions in class completers Needed to break the loop between completion of class and companion object. If we try to complete the class first, and completion needs the companion object (for instance for processing an import) then the companion object completion would consult the companion class info for constructor that need a constructor proxy in the object. This can lead to a cyclic reference. We now break the cycle by delaying adding constructor proxies in this case to be the last completion action of the companion class. [Cherry-picked 9704b0ca6ed6a585f9011a2680b78256c39a47de][modified]