From ee89e58d9caaa98dfb1430304adaaf00bb7978e5 Mon Sep 17 00:00:00 2001 From: noti0na1 Date: Thu, 27 Nov 2025 17:06:44 +0100 Subject: [PATCH 1/4] Strip inferred retains annotation from Macro/inline call trees --- .../dotty/tools/dotc/inlines/Inlines.scala | 39 +++++++++---- tests/pos-macros/i24547/ReproMacro_1.scala | 55 +++++++++++++++++++ tests/pos-macros/i24547/ReproTest_2.scala | 16 ++++++ 3 files changed, 99 insertions(+), 11 deletions(-) create mode 100644 tests/pos-macros/i24547/ReproMacro_1.scala create mode 100644 tests/pos-macros/i24547/ReproTest_2.scala diff --git a/compiler/src/dotty/tools/dotc/inlines/Inlines.scala b/compiler/src/dotty/tools/dotc/inlines/Inlines.scala index 2dd86132fb97..b4f1897b89f0 100644 --- a/compiler/src/dotty/tools/dotc/inlines/Inlines.scala +++ b/compiler/src/dotty/tools/dotc/inlines/Inlines.scala @@ -18,6 +18,7 @@ import dotty.tools.dotc.transform.MegaPhase.MiniPhase import parsing.Parsers.Parser import transform.{PostTyper, Inlining, CrossVersionChecks} import staging.StagingLevel +import cc.CleanupRetains import collection.mutable import reporting.{NotConstant, trace} @@ -100,18 +101,34 @@ object Inlines: * and body that replace it. */ def inlineCall(tree: Tree)(using Context): Tree = ctx.profiler.onInlineCall(tree.symbol): - if tree.symbol.denot != SymDenotations.NoDenotation - && tree.symbol.effectiveOwner == defn.CompiletimeTestingPackage.moduleClass + + /** Strip @retains annotations from inferred types in the call tree */ + val stripRetains = CleanupRetains() + val stripper = new TreeTypeMap( + typeMap = stripRetains, + treeMap = { + case tree: InferredTypeTree => + val stripped = stripRetains(tree.tpe) + if stripped ne tree.tpe then tree.withType(stripped) + else tree + case tree => tree + } + ) + + val tree0 = stripper.transform(tree) + + if tree0.symbol.denot != SymDenotations.NoDenotation + && tree0.symbol.effectiveOwner == defn.CompiletimeTestingPackage.moduleClass then - if (tree.symbol == defn.CompiletimeTesting_typeChecks) return Intrinsics.typeChecks(tree) - if (tree.symbol == defn.CompiletimeTesting_typeCheckErrors) return Intrinsics.typeCheckErrors(tree) + if (tree0.symbol == defn.CompiletimeTesting_typeChecks) return Intrinsics.typeChecks(tree0) + if (tree0.symbol == defn.CompiletimeTesting_typeCheckErrors) return Intrinsics.typeCheckErrors(tree0) if ctx.isAfterTyper then // During typer we wait with cross version checks until PostTyper, in order // not to provoke cyclic references. See i16116 for a test case. - CrossVersionChecks.checkRef(tree.symbol, tree.srcPos) + CrossVersionChecks.checkRef(tree0.symbol, tree0.srcPos) - if tree.symbol.isConstructor then return tree // error already reported for the inline constructor definition + if tree0.symbol.isConstructor then return tree // error already reported for the inline constructor definition /** Set the position of all trees logically contained in the expansion of * inlined call `call` to the position of `call`. This transform is necessary @@ -159,17 +176,17 @@ object Inlines: tree } - // assertAllPositioned(tree) // debug - val tree1 = liftBindings(tree, identity) + // assertAllPositioned(tree0) // debug + val tree1 = liftBindings(tree0, identity) val tree2 = if bindings.nonEmpty then - cpy.Block(tree)(bindings.toList, inlineCall(tree1)) + cpy.Block(tree0)(bindings.toList, inlineCall(tree1)) else if enclosingInlineds.length < ctx.settings.XmaxInlines.value && !reachedInlinedTreesLimit then val body = - try bodyToInline(tree.symbol) // can typecheck the tree and thereby produce errors + try bodyToInline(tree0.symbol) // can typecheck the tree and thereby produce errors catch case _: MissingInlineInfo => throw CyclicReference(ctx.owner) - new InlineCall(tree).expand(body) + new InlineCall(tree0).expand(body) else ctx.base.stopInlining = true val (reason, setting) = diff --git a/tests/pos-macros/i24547/ReproMacro_1.scala b/tests/pos-macros/i24547/ReproMacro_1.scala new file mode 100644 index 000000000000..ac9c2e4948aa --- /dev/null +++ b/tests/pos-macros/i24547/ReproMacro_1.scala @@ -0,0 +1,55 @@ +// ReproMacro.scala +// Minimal macro that reproduces "Coll[Int] is not a legal path" error +// when creating new ValDefs with types containing @retains annotations + +import scala.quoted.* + +object ReproMacro: + + transparent inline def transform[A](inline expr: A): A = ${ transformImpl[A]('expr) } + + private def transformImpl[A: Type](expr: Expr[A])(using Quotes): Expr[A] = + import quotes.reflect.* + + val term = expr.asTerm + val owner = Symbol.spliceOwner + + // Rebuild the block, creating NEW ValDefs with the same types + val result = rebuildBlock(term, owner) + + result.asExprOf[A] + + private def rebuildBlock(using Quotes)(term: quotes.reflect.Term, owner: quotes.reflect.Symbol): quotes.reflect.Term = + import quotes.reflect.* + + term match + case Block(stats, expr) => + var symbolMap = Map.empty[Symbol, Symbol] + + val newStats = stats.map { + case vd @ ValDef(name, tpt, Some(rhs)) => + // Create a new symbol with the same type - this causes the error + // when tpt.tpe contains AnnotatedType(LazyRef(Coll[Int]), @retains(...)) + val newSym = Symbol.newVal(owner, name, tpt.tpe, Flags.EmptyFlags, Symbol.noSymbol) + symbolMap = symbolMap + (vd.symbol -> newSym) + ValDef(newSym, Some(substituteRefs(rhs, symbolMap))) + case other => other + } + + Block(newStats, substituteRefs(expr, symbolMap)) + + case Inlined(call, bindings, expansion) => + Inlined(call, bindings, rebuildBlock(expansion, owner)) + + case _ => term + + private def substituteRefs(using Quotes)(term: quotes.reflect.Term, map: Map[quotes.reflect.Symbol, quotes.reflect.Symbol]): quotes.reflect.Term = + import quotes.reflect.* + + val mapper = new TreeMap: + override def transformTerm(t: Term)(owner: Symbol): Term = t match + case Ident(name) if map.contains(t.symbol) => + Ref(map(t.symbol)) + case _ => super.transformTerm(t)(owner) + + mapper.transformTerm(term)(Symbol.spliceOwner) diff --git a/tests/pos-macros/i24547/ReproTest_2.scala b/tests/pos-macros/i24547/ReproTest_2.scala new file mode 100644 index 000000000000..f41281e13b84 --- /dev/null +++ b/tests/pos-macros/i24547/ReproTest_2.scala @@ -0,0 +1,16 @@ +// ReproTest.scala +// Test case: abstract type parameter Coll[Int] with @retains annotation +// causes "not a legal path" error when macro creates new ValDef + +import scala.collection.IterableOps + +def reproTest[Coll[X] <: Iterable[X] & IterableOps[X, Coll, Coll[X]]]: Unit = + def xsValues: Coll[Int] = ??? + + // The .span method returns (Coll[Int], Coll[Int]) + // With capture checking, these types get @retains annotations + // When the macro creates new ValDefs with these types, compilation fails + ReproMacro.transform { + val (take, drop) = xsValues.span(???) + take.toSeq + } From 63ec012a4a8dcfc56afe8e9421b6c0598414c0cd Mon Sep 17 00:00:00 2001 From: noti0na1 Date: Fri, 28 Nov 2025 13:05:37 +0100 Subject: [PATCH 2/4] Disable Ycheck for i24547 --- compiler/test/dotty/tools/dotc/CompilationTests.scala | 1 + tests/{pos-macros => pos-special}/i24547/ReproMacro_1.scala | 0 tests/{pos-macros => pos-special}/i24547/ReproTest_2.scala | 3 +++ 3 files changed, 4 insertions(+) rename tests/{pos-macros => pos-special}/i24547/ReproMacro_1.scala (100%) rename tests/{pos-macros => pos-special}/i24547/ReproTest_2.scala (77%) diff --git a/compiler/test/dotty/tools/dotc/CompilationTests.scala b/compiler/test/dotty/tools/dotc/CompilationTests.scala index 2af2f834db9a..85793e69b9cb 100644 --- a/compiler/test/dotty/tools/dotc/CompilationTests.scala +++ b/compiler/test/dotty/tools/dotc/CompilationTests.scala @@ -41,6 +41,7 @@ class CompilationTests { compileFile("tests/pos-special/utf8encoded.scala", defaultOptions.and("-encoding", "UTF8")), compileFile("tests/pos-special/utf16encoded.scala", defaultOptions.and("-encoding", "UTF16")), compileDir("tests/pos-special/i18589", defaultOptions.and("-Wsafe-init").without("-Ycheck:all")), + compileDir("tests/pos-special/i24547", defaultOptions.without("-Ycheck:all")), // Run tests for legacy lazy vals compileFilesInDir("tests/pos", defaultOptions.and("-Wsafe-init", "-Ylegacy-lazy-vals", "-Ycheck-constraint-deps"), FileFilter.include(TestSources.posLazyValsAllowlist)), compileDir("tests/pos-special/java-param-names", defaultOptions.withJavacOnlyOptions("-parameters")), diff --git a/tests/pos-macros/i24547/ReproMacro_1.scala b/tests/pos-special/i24547/ReproMacro_1.scala similarity index 100% rename from tests/pos-macros/i24547/ReproMacro_1.scala rename to tests/pos-special/i24547/ReproMacro_1.scala diff --git a/tests/pos-macros/i24547/ReproTest_2.scala b/tests/pos-special/i24547/ReproTest_2.scala similarity index 77% rename from tests/pos-macros/i24547/ReproTest_2.scala rename to tests/pos-special/i24547/ReproTest_2.scala index f41281e13b84..85226ca26f98 100644 --- a/tests/pos-macros/i24547/ReproTest_2.scala +++ b/tests/pos-special/i24547/ReproTest_2.scala @@ -2,6 +2,9 @@ // Test case: abstract type parameter Coll[Int] with @retains annotation // causes "not a legal path" error when macro creates new ValDef +// TODO: there are some other issues with this test case, so it is currently disabled +// for `Ycheck:all`. We should move it back to `pos-macro` once those issues are fixed. + import scala.collection.IterableOps def reproTest[Coll[X] <: Iterable[X] & IterableOps[X, Coll, Coll[X]]]: Unit = From cc921997cb7c66a109b88dc49c8f37e51c65fe40 Mon Sep 17 00:00:00 2001 From: noti0na1 Date: Fri, 28 Nov 2025 15:37:55 +0100 Subject: [PATCH 3/4] Simplify retains annotation cleanup at inlineCall --- .../dotty/tools/dotc/inlines/Inlines.scala | 19 ++++--------------- 1 file changed, 4 insertions(+), 15 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/inlines/Inlines.scala b/compiler/src/dotty/tools/dotc/inlines/Inlines.scala index b4f1897b89f0..d7367022fa1c 100644 --- a/compiler/src/dotty/tools/dotc/inlines/Inlines.scala +++ b/compiler/src/dotty/tools/dotc/inlines/Inlines.scala @@ -102,22 +102,11 @@ object Inlines: */ def inlineCall(tree: Tree)(using Context): Tree = ctx.profiler.onInlineCall(tree.symbol): - /** Strip @retains annotations from inferred types in the call tree */ - val stripRetains = CleanupRetains() - val stripper = new TreeTypeMap( - typeMap = stripRetains, - treeMap = { - case tree: InferredTypeTree => - val stripped = stripRetains(tree.tpe) - if stripped ne tree.tpe then tree.withType(stripped) - else tree - case tree => tree - } - ) - - val tree0 = stripper.transform(tree) + /** Strip @retains annotations from types in the call tree */ + val cleanupRetains = new TreeTypeMap(typeMap = CleanupRetains()) + val tree0 = cleanupRetains.transform(tree) - if tree0.symbol.denot != SymDenotations.NoDenotation + if tree0.symbol.denot.exists && tree0.symbol.effectiveOwner == defn.CompiletimeTestingPackage.moduleClass then if (tree0.symbol == defn.CompiletimeTesting_typeChecks) return Intrinsics.typeChecks(tree0) From 83735b996709ec33e90f7c72c44cade9a137ad3e Mon Sep 17 00:00:00 2001 From: noti0na1 Date: Fri, 28 Nov 2025 15:56:27 +0100 Subject: [PATCH 4/4] Revert to only mapping inferred types --- .../src/dotty/tools/dotc/inlines/Inlines.scala | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/inlines/Inlines.scala b/compiler/src/dotty/tools/dotc/inlines/Inlines.scala index d7367022fa1c..3aceec2a5b4e 100644 --- a/compiler/src/dotty/tools/dotc/inlines/Inlines.scala +++ b/compiler/src/dotty/tools/dotc/inlines/Inlines.scala @@ -102,9 +102,19 @@ object Inlines: */ def inlineCall(tree: Tree)(using Context): Tree = ctx.profiler.onInlineCall(tree.symbol): - /** Strip @retains annotations from types in the call tree */ - val cleanupRetains = new TreeTypeMap(typeMap = CleanupRetains()) - val tree0 = cleanupRetains.transform(tree) + /** Strip @retains annotations from inferred types in the call tree */ + val stripRetains = CleanupRetains() + val stripper = new TreeTypeMap( + treeMap = { + case tree: InferredTypeTree => + val stripped = stripRetains(tree.tpe) + if stripped ne tree.tpe then tree.withType(stripped) + else tree + case tree => tree + } + ) + + val tree0 = stripper.transform(tree) if tree0.symbol.denot.exists && tree0.symbol.effectiveOwner == defn.CompiletimeTestingPackage.moduleClass