From 50243538f7596afbd0bb0e79d5bb82aed2b61f85 Mon Sep 17 00:00:00 2001 From: Nicolas Stucki Date: Mon, 6 Feb 2023 15:11:44 +0100 Subject: [PATCH] Fix Splicer.isEscapedVariable Consider that `val macro` expansion in the context can come from an outer macro that is being expanded (i.e. this is a nested macro). Nested macro expansion can occur when a macro summons an implicit macro. Fixes partially #16835 --- .../dotty/tools/dotc/transform/Splicer.scala | 7 +- tests/pos-macros/i16835/Macro_1.scala | 79 +++++++++++++++++++ tests/pos-macros/i16835/Show_1.scala | 11 +++ tests/pos-macros/i16835/Test_2.scala | 30 +++++++ 4 files changed, 125 insertions(+), 2 deletions(-) create mode 100644 tests/pos-macros/i16835/Macro_1.scala create mode 100644 tests/pos-macros/i16835/Show_1.scala create mode 100644 tests/pos-macros/i16835/Test_2.scala diff --git a/compiler/src/dotty/tools/dotc/transform/Splicer.scala b/compiler/src/dotty/tools/dotc/transform/Splicer.scala index 3d3cd1f41c84..bc4119ad0cff 100644 --- a/compiler/src/dotty/tools/dotc/transform/Splicer.scala +++ b/compiler/src/dotty/tools/dotc/transform/Splicer.scala @@ -86,7 +86,7 @@ object Splicer { } } - /** Checks that no symbol that whas generated within the macro expansion has an out of scope reference */ + /** Checks that no symbol that was generated within the macro expansion has an out of scope reference */ def checkEscapedVariables(tree: Tree, expansionOwner: Symbol)(using Context): tree.type = new TreeTraverser { private[this] var locals = Set.empty[Symbol] @@ -119,7 +119,10 @@ object Splicer { sym.exists && !sym.is(Package) && sym.owner.ownersIterator.exists(x => x == expansionOwner || // symbol was generated within this macro expansion - isMacroOwner(x) // symbol was generated within another macro expansion + { // symbol was generated within another macro expansion + isMacroOwner(x) && + !ctx.owner.ownersIterator.contains(x) + } ) && !locals.contains(sym) // symbol is not in current scope }.traverse(tree) diff --git a/tests/pos-macros/i16835/Macro_1.scala b/tests/pos-macros/i16835/Macro_1.scala new file mode 100644 index 000000000000..133d9f38d1da --- /dev/null +++ b/tests/pos-macros/i16835/Macro_1.scala @@ -0,0 +1,79 @@ +import scala.quoted.* +import scala.deriving.Mirror + +// derivation code is a slightly modified version of: https://github.com/lampepfl/dotty-macro-examples/blob/main/macroTypeClassDerivation/src/macro.scala +object Derivation { + + // Typeclass instance gets constructed as part of a macro + inline given deriveFullyConstrucedByMacro[A](using Mirror.ProductOf[A]): Show[A] = Derivation.deriveShow[A] + + // Typeclass instance is built inside as part of a method, only the 'show' impl is filled in by a macro + inline given derivePartiallyConstructedByMacro[A](using Mirror.ProductOf[A]): Show[A] = + new { + def show(value: A): String = Derivation.show(value) + } + + inline def show[T](value: T): String = ${ showValue('value) } + + inline def deriveShow[T]: Show[T] = ${ deriveCaseClassShow[T] } + + private def deriveCaseClassShow[T](using quotes: Quotes, tpe: Type[T]): Expr[Show[T]] = { + import quotes.reflect.* + // Getting the case fields of the case class + val fields: List[Symbol] = TypeTree.of[T].symbol.caseFields + + '{ + new Show[T] { + override def show(t: T): String = + ${ showValue('t) } + } + } + } + + def showValue[T: Type](value: Expr[T])(using Quotes): Expr[String] = { + import quotes.reflect.* + + val fields: List[Symbol] = TypeTree.of[T].symbol.caseFields + + val vTerm: Term = value.asTerm + val valuesExprs: List[Expr[String]] = fields.map(showField(vTerm, _)) + val exprOfList: Expr[List[String]] = Expr.ofList(valuesExprs) + '{ "{ " + $exprOfList.mkString(", ") + " }" } + } + + /** Create a quoted String representation of a given field of the case class */ + private def showField(using Quotes)(caseClassTerm: quotes.reflect.Term, field: quotes.reflect.Symbol): Expr[String] = { + import quotes.reflect.* + + val fieldValDef: ValDef = field.tree.asInstanceOf[ValDef] + val fieldTpe: TypeRepr = fieldValDef.tpt.tpe + val fieldName: String = fieldValDef.name + + val tcl: Term = lookupShowFor(fieldTpe) // Show[$fieldTpe] + val fieldValue: Term = Select(caseClassTerm, field) // v.field + val strRepr: Expr[String] = applyShow(tcl, fieldValue).asExprOf[String] + '{ ${ Expr(fieldName) } + ": " + $strRepr } // summon[Show[$fieldTpe]].show(v.field) + } + + /** Look up the Show[$t] typeclass for a given type t */ + private def lookupShowFor(using Quotes)(t: quotes.reflect.TypeRepr): quotes.reflect.Term = { + import quotes.reflect.* + t.asType match { + case '[tpe] => + Implicits.search(TypeRepr.of[Show[tpe]]) match { + case res: ImplicitSearchSuccess => res.tree + case failure: DivergingImplicit => report.errorAndAbort(s"Diverving: ${failure.explanation}") + case failure: NoMatchingImplicits => report.errorAndAbort(s"NoMatching: ${failure.explanation}") + case failure: AmbiguousImplicits => report.errorAndAbort(s"Ambiguous: ${failure.explanation}") + case failure: ImplicitSearchFailure => + report.errorAndAbort(s"catch all: ${failure.explanation}") + } + } + } + + /** Composes the tree: $tcl.show($arg) */ + private def applyShow(using Quotes)(tcl: quotes.reflect.Term, arg: quotes.reflect.Term): quotes.reflect.Term = { + import quotes.reflect.* + Apply(Select.unique(tcl, "show"), arg :: Nil) + } +} diff --git a/tests/pos-macros/i16835/Show_1.scala b/tests/pos-macros/i16835/Show_1.scala new file mode 100644 index 000000000000..61f6b2dccd80 --- /dev/null +++ b/tests/pos-macros/i16835/Show_1.scala @@ -0,0 +1,11 @@ +trait Show[A] { + def show(value: A): String +} + +object Show { + given identity: Show[String] = a => a + + given int: Show[Int] = _.toString() + + given list[A](using A: Show[A]): Show[List[A]] = _.map(A.show).toString() +} diff --git a/tests/pos-macros/i16835/Test_2.scala b/tests/pos-macros/i16835/Test_2.scala new file mode 100644 index 000000000000..61019b1417b6 --- /dev/null +++ b/tests/pos-macros/i16835/Test_2.scala @@ -0,0 +1,30 @@ +import scala.deriving.* + +object usage { + final case class Person(name: String, age: Int, otherNames: List[String], p2: Person2) + + final case class Person2(name: String, age: Int, otherNames: List[String]) + + locally { + import Derivation.deriveFullyConstrucedByMacro + // works for case classes without other nested case classes inside + summon[Show[Person2]] + + // also derives instances with nested case classes + summon[Show[Person]] + } + + locally { + import Derivation.derivePartiallyConstructedByMacro + + // works for case classes without other nested case classes inside + summon[Show[Person2]] + + // fails for case classes with other nested case classes inside, + // note how that error is not a `NonMatching', `Diverging` or `Ambiguous` implicit search error but something else + /* + catch all: given instance deriveWithConstructionOutsideMacro in object Derivation does not match type io.github.arainko.ducktape.issue_repros.Show[Person2] + */ + summon[Show[Person]] + } +} \ No newline at end of file