diff --git a/compiler/src/dotty/tools/dotc/inlines/Inlines.scala b/compiler/src/dotty/tools/dotc/inlines/Inlines.scala index 2dd86132fb97..3aceec2a5b4e 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,33 @@ 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( + 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 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 +175,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/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-special/i24547/ReproMacro_1.scala b/tests/pos-special/i24547/ReproMacro_1.scala new file mode 100644 index 000000000000..ac9c2e4948aa --- /dev/null +++ b/tests/pos-special/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-special/i24547/ReproTest_2.scala b/tests/pos-special/i24547/ReproTest_2.scala new file mode 100644 index 000000000000..85226ca26f98 --- /dev/null +++ b/tests/pos-special/i24547/ReproTest_2.scala @@ -0,0 +1,19 @@ +// ReproTest.scala +// 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 = + 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 + }