Skip to content

Commit 60f2b96

Browse files
authored
Fix Splicer.isEscapedVariable (#16838)
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
2 parents 9b8db9f + 5024353 commit 60f2b96

File tree

4 files changed

+125
-2
lines changed

4 files changed

+125
-2
lines changed

compiler/src/dotty/tools/dotc/transform/Splicer.scala

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ object Splicer {
8686
}
8787
}
8888

89-
/** Checks that no symbol that whas generated within the macro expansion has an out of scope reference */
89+
/** Checks that no symbol that was generated within the macro expansion has an out of scope reference */
9090
def checkEscapedVariables(tree: Tree, expansionOwner: Symbol)(using Context): tree.type =
9191
new TreeTraverser {
9292
private[this] var locals = Set.empty[Symbol]
@@ -119,7 +119,10 @@ object Splicer {
119119
sym.exists && !sym.is(Package)
120120
&& sym.owner.ownersIterator.exists(x =>
121121
x == expansionOwner || // symbol was generated within this macro expansion
122-
isMacroOwner(x) // symbol was generated within another macro expansion
122+
{ // symbol was generated within another macro expansion
123+
isMacroOwner(x) &&
124+
!ctx.owner.ownersIterator.contains(x)
125+
}
123126
)
124127
&& !locals.contains(sym) // symbol is not in current scope
125128
}.traverse(tree)

tests/pos-macros/i16835/Macro_1.scala

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import scala.quoted.*
2+
import scala.deriving.Mirror
3+
4+
// derivation code is a slightly modified version of: https://github.com/lampepfl/dotty-macro-examples/blob/main/macroTypeClassDerivation/src/macro.scala
5+
object Derivation {
6+
7+
// Typeclass instance gets constructed as part of a macro
8+
inline given deriveFullyConstrucedByMacro[A](using Mirror.ProductOf[A]): Show[A] = Derivation.deriveShow[A]
9+
10+
// Typeclass instance is built inside as part of a method, only the 'show' impl is filled in by a macro
11+
inline given derivePartiallyConstructedByMacro[A](using Mirror.ProductOf[A]): Show[A] =
12+
new {
13+
def show(value: A): String = Derivation.show(value)
14+
}
15+
16+
inline def show[T](value: T): String = ${ showValue('value) }
17+
18+
inline def deriveShow[T]: Show[T] = ${ deriveCaseClassShow[T] }
19+
20+
private def deriveCaseClassShow[T](using quotes: Quotes, tpe: Type[T]): Expr[Show[T]] = {
21+
import quotes.reflect.*
22+
// Getting the case fields of the case class
23+
val fields: List[Symbol] = TypeTree.of[T].symbol.caseFields
24+
25+
'{
26+
new Show[T] {
27+
override def show(t: T): String =
28+
${ showValue('t) }
29+
}
30+
}
31+
}
32+
33+
def showValue[T: Type](value: Expr[T])(using Quotes): Expr[String] = {
34+
import quotes.reflect.*
35+
36+
val fields: List[Symbol] = TypeTree.of[T].symbol.caseFields
37+
38+
val vTerm: Term = value.asTerm
39+
val valuesExprs: List[Expr[String]] = fields.map(showField(vTerm, _))
40+
val exprOfList: Expr[List[String]] = Expr.ofList(valuesExprs)
41+
'{ "{ " + $exprOfList.mkString(", ") + " }" }
42+
}
43+
44+
/** Create a quoted String representation of a given field of the case class */
45+
private def showField(using Quotes)(caseClassTerm: quotes.reflect.Term, field: quotes.reflect.Symbol): Expr[String] = {
46+
import quotes.reflect.*
47+
48+
val fieldValDef: ValDef = field.tree.asInstanceOf[ValDef]
49+
val fieldTpe: TypeRepr = fieldValDef.tpt.tpe
50+
val fieldName: String = fieldValDef.name
51+
52+
val tcl: Term = lookupShowFor(fieldTpe) // Show[$fieldTpe]
53+
val fieldValue: Term = Select(caseClassTerm, field) // v.field
54+
val strRepr: Expr[String] = applyShow(tcl, fieldValue).asExprOf[String]
55+
'{ ${ Expr(fieldName) } + ": " + $strRepr } // summon[Show[$fieldTpe]].show(v.field)
56+
}
57+
58+
/** Look up the Show[$t] typeclass for a given type t */
59+
private def lookupShowFor(using Quotes)(t: quotes.reflect.TypeRepr): quotes.reflect.Term = {
60+
import quotes.reflect.*
61+
t.asType match {
62+
case '[tpe] =>
63+
Implicits.search(TypeRepr.of[Show[tpe]]) match {
64+
case res: ImplicitSearchSuccess => res.tree
65+
case failure: DivergingImplicit => report.errorAndAbort(s"Diverving: ${failure.explanation}")
66+
case failure: NoMatchingImplicits => report.errorAndAbort(s"NoMatching: ${failure.explanation}")
67+
case failure: AmbiguousImplicits => report.errorAndAbort(s"Ambiguous: ${failure.explanation}")
68+
case failure: ImplicitSearchFailure =>
69+
report.errorAndAbort(s"catch all: ${failure.explanation}")
70+
}
71+
}
72+
}
73+
74+
/** Composes the tree: $tcl.show($arg) */
75+
private def applyShow(using Quotes)(tcl: quotes.reflect.Term, arg: quotes.reflect.Term): quotes.reflect.Term = {
76+
import quotes.reflect.*
77+
Apply(Select.unique(tcl, "show"), arg :: Nil)
78+
}
79+
}

tests/pos-macros/i16835/Show_1.scala

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
trait Show[A] {
2+
def show(value: A): String
3+
}
4+
5+
object Show {
6+
given identity: Show[String] = a => a
7+
8+
given int: Show[Int] = _.toString()
9+
10+
given list[A](using A: Show[A]): Show[List[A]] = _.map(A.show).toString()
11+
}

tests/pos-macros/i16835/Test_2.scala

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import scala.deriving.*
2+
3+
object usage {
4+
final case class Person(name: String, age: Int, otherNames: List[String], p2: Person2)
5+
6+
final case class Person2(name: String, age: Int, otherNames: List[String])
7+
8+
locally {
9+
import Derivation.deriveFullyConstrucedByMacro
10+
// works for case classes without other nested case classes inside
11+
summon[Show[Person2]]
12+
13+
// also derives instances with nested case classes
14+
summon[Show[Person]]
15+
}
16+
17+
locally {
18+
import Derivation.derivePartiallyConstructedByMacro
19+
20+
// works for case classes without other nested case classes inside
21+
summon[Show[Person2]]
22+
23+
// fails for case classes with other nested case classes inside,
24+
// note how that error is not a `NonMatching', `Diverging` or `Ambiguous` implicit search error but something else
25+
/*
26+
catch all: given instance deriveWithConstructionOutsideMacro in object Derivation does not match type io.github.arainko.ducktape.issue_repros.Show[Person2]
27+
*/
28+
summon[Show[Person]]
29+
}
30+
}

0 commit comments

Comments
 (0)