Skip to content

Commit 697432b

Browse files
committed
Fix QuotesCache caching quoted symbol definitions with incorrect owners
Previously we would always cache and reuse unpickled trees, which was a problem for quoted code which included symbol definitions. In those cases, even if those quotes were being created within another context (which should dictate owners of symbol definitions), it would be ignored, and previous potentially incorrect symbol definitions would be reused. Now we include the quote symbol owner while caching, which is only taken into account if the quoted code contains a symbol definition.
1 parent 48a823f commit 697432b

File tree

4 files changed

+132
-27
lines changed

4 files changed

+132
-27
lines changed

Diff for: compiler/src/dotty/tools/dotc/quoted/PickledQuotes.scala

+33-17
Original file line numberDiff line numberDiff line change
@@ -235,7 +235,21 @@ object PickledQuotes {
235235

236236
/** Unpickle TASTY bytes into it's tree */
237237
private def unpickle(pickled: String | List[String], isType: Boolean)(using Context): Tree = {
238-
QuotesCache.getTree(pickled) match
238+
val unpicklingContext =
239+
if ctx.owner.isClass then
240+
// When a quote is unpickled with a Quotes context that that has a class `spliceOwner`
241+
// we need to use a dummy owner to unpickle it. Otherwise any definitions defined
242+
// in the quoted block would be accidentally entered in the class.
243+
// When splicing this expression, this owner is replaced with the correct owner (see `quotedExprToTree` and `quotedTypeToTree` above).
244+
// On the other hand, if the expression is used as a reflect term, the user must call `changeOwner` (same as with other expressions used within a nested owner).
245+
// `-Xcheck-macros` will check for inconsistent owners and provide the users hints on how to improve them.
246+
//
247+
// Quotes context that that has a class `spliceOwner` can come from a macro annotation
248+
// or a user setting it explicitly using `Symbol.asQuotes`.
249+
ctx.withOwner(newSymbol(ctx.owner, "$quoteOwnedByClass$".toTermName, Private, defn.AnyType, NoSymbol))
250+
else ctx
251+
252+
QuotesCache.getTree(pickled, unpicklingContext.owner) match
239253
case Some(tree) =>
240254
quotePickling.println(s"**** Using cached quote for TASTY\n$tree")
241255
treeOwner(tree) match
@@ -250,20 +264,6 @@ object PickledQuotes {
250264
case pickled: String => TastyString.unpickle(pickled)
251265
case pickled: List[String] => TastyString.unpickle(pickled)
252266

253-
val unpicklingContext =
254-
if ctx.owner.isClass then
255-
// When a quote is unpickled with a Quotes context that that has a class `spliceOwner`
256-
// we need to use a dummy owner to unpickle it. Otherwise any definitions defined
257-
// in the quoted block would be accidentally entered in the class.
258-
// When splicing this expression, this owner is replaced with the correct owner (see `quotedExprToTree` and `quotedTypeToTree` above).
259-
// On the other hand, if the expression is used as a reflect term, the user must call `changeOwner` (same as with other expressions used within a nested owner).
260-
// `-Xcheck-macros` will check for inconsistent owners and provide the users hints on how to improve them.
261-
//
262-
// Quotes context that that has a class `spliceOwner` can come from a macro annotation
263-
// or a user setting it explicitly using `Symbol.asQuotes`.
264-
ctx.withOwner(newSymbol(ctx.owner, "$quoteOwnedByClass$".toTermName, Private, defn.AnyType, NoSymbol))
265-
else ctx
266-
267267
inContext(unpicklingContext) {
268268

269269
quotePickling.println(s"**** unpickling quote from TASTY\n${TastyPrinter.showContents(bytes, ctx.settings.color.value == "never", isBestEffortTasty = false)}")
@@ -273,10 +273,26 @@ object PickledQuotes {
273273
unpickler.enter(Set.empty)
274274

275275
val tree = unpickler.tree
276-
QuotesCache(pickled) = tree
277276

277+
var includesSymbolDefinition = false
278278
// Make sure trees and positions are fully loaded
279-
tree.foreachSubTree(identity)
279+
new TreeTraverser {
280+
def traverse(tree: Tree)(using Context): Unit =
281+
tree match
282+
case _: DefTree =>
283+
if !tree.symbol.hasAnnotation(defn.QuotedRuntime_SplicedTypeAnnot)
284+
&& !tree.symbol.hasAnnotation(defn.QuotedRuntimePatterns_patternTypeAnnot)
285+
then
286+
includesSymbolDefinition = true
287+
case _ =>
288+
traverseChildren(tree)
289+
}.traverse(tree)
290+
291+
// We cache with the context symbol owner only if we need to
292+
val symbolOwnerMaybe =
293+
if (includesSymbolDefinition) Some(ctx.owner)
294+
else None
295+
QuotesCache(pickled, symbolOwnerMaybe) = tree
280296

281297
quotePickling.println(i"**** unpickled quote\n$tree")
282298

Diff for: compiler/src/dotty/tools/dotc/quoted/QuotesCache.scala

+29-10
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,43 @@
11
package dotty.tools.dotc.quoted
22

33
import dotty.tools.dotc.core.Contexts.*
4+
import dotty.tools.dotc.core.Symbols.Symbol
45
import dotty.tools.dotc.util.Property
56
import dotty.tools.dotc.ast.tpd
67

78

89
object QuotesCache {
910
import tpd.*
1011

11-
/** A key to be used in a context property that caches the unpickled trees */
12-
private val QuotesCacheKey = new Property.Key[collection.mutable.Map[String | List[String], Tree]]
13-
12+
/** Only used when the cached tree includes symbol definition.
13+
* Represents a mapping from the symbol owning the context of the quote to the unpickled tree. */
14+
private type OwnerCache = collection.mutable.Map[Symbol, Tree]
1415

15-
/** Get the cached tree of the quote */
16-
def getTree(pickled: String | List[String])(using Context): Option[Tree] =
17-
ctx.property(QuotesCacheKey).get.get(pickled)
18-
19-
/** Update the cached tree of the quote */
20-
def update(pickled: String | List[String], tree: Tree)(using Context): Unit =
21-
ctx.property(QuotesCacheKey).get.update(pickled, tree)
16+
/** A key to be used in a context property that caches the unpickled trees */
17+
private val QuotesCacheKey = new Property.Key[collection.mutable.Map[String | List[String], Either[Tree, OwnerCache]]]
18+
19+
20+
/** Get the cached tree of the quote.
21+
* quoteOwner is taken into account only if the unpickled quote includes a symbol definition */
22+
def getTree(pickled: String | List[String], quoteOwner: Symbol)(using Context): Option[Tree] =
23+
ctx.property(QuotesCacheKey).get.get(pickled).flatMap {
24+
case Left(tree: Tree) => Some(tree)
25+
case Right(map) => map.get(quoteOwner)
26+
}
27+
28+
/** Update the cached tree of the quote.
29+
* quoteOwner is applicable only if the quote includes a symbol definition, otherwise should be None */
30+
def update(pickled: String | List[String], quoteOwner: Option[Symbol], tree: Tree)(using Context): Unit =
31+
val previousValueMaybe = ctx.property(QuotesCacheKey).get.get(pickled)
32+
val updatedValue: Either[Tree, OwnerCache] =
33+
(previousValueMaybe, quoteOwner) match
34+
case (None, Some(owner)) =>
35+
Right(collection.mutable.Map((owner, tree)))
36+
case (Some(map: OwnerCache), Some(owner)) =>
37+
map.update(owner, tree)
38+
Right(map)
39+
case _ => Left(tree)
40+
ctx.property(QuotesCacheKey).get.update(pickled, updatedValue)
2241

2342
/** Context with a cache for quote trees and tasty bytes */
2443
def init(ctx: FreshContext): ctx.type =

Diff for: tests/pos-macros/i20471/Macro_1.scala

+63
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import scala.annotation.experimental
2+
import scala.quoted.*
3+
import scala.annotation.tailrec
4+
5+
object FlatMap {
6+
@experimental inline def derived[F[_]]: FlatMap[F] = MacroFlatMap.derive
7+
}
8+
trait FlatMap[F[_]]{
9+
def tailRecM[A, B](a: A)(f: A => F[Either[A, B]]): F[B]
10+
}
11+
12+
@experimental
13+
object MacroFlatMap:
14+
15+
inline def derive[F[_]]: FlatMap[F] = ${ flatMap }
16+
17+
def flatMap[F[_]: Type](using Quotes): Expr[FlatMap[F]] = '{
18+
new FlatMap[F]:
19+
def tailRecM[A, B](a: A)(f: A => F[Either[A, B]]): F[B] =
20+
${ deriveTailRecM('{ a }, '{ f }) }
21+
}
22+
23+
def deriveTailRecM[F[_]: Type, A: Type, B: Type](
24+
a: Expr[A],
25+
f: Expr[A => F[Either[A, B]]]
26+
)(using q: Quotes): Expr[F[B]] =
27+
import quotes.reflect.*
28+
29+
val body: PartialFunction[(Symbol, TypeRepr), Term] = {
30+
case (method, tpe) => {
31+
given q2: Quotes = method.asQuotes
32+
'{
33+
def step(x: A): B = ???
34+
???
35+
}.asTerm
36+
}
37+
}
38+
39+
val term = '{ $f($a) }.asTerm
40+
val name = Symbol.freshName("$anon")
41+
val parents = List(TypeTree.of[Object], TypeTree.of[F[B]])
42+
43+
extension (sym: Symbol) def overridableMembers: List[Symbol] =
44+
val member1 = sym.methodMember("abstractEffect")(0)
45+
val member2 = sym.methodMember("concreteEffect")(0)
46+
def meth(member: Symbol) = Symbol.newMethod(sym, member.name, This(sym).tpe.memberType(member), Flags.Override, Symbol.noSymbol)
47+
List(meth(member1), meth(member2))
48+
49+
val cls = Symbol.newClass(Symbol.spliceOwner, name, parents.map(_.tpe), _.overridableMembers, None)
50+
51+
def transformDef(method: DefDef)(argss: List[List[Tree]]): Option[Term] =
52+
val sym = method.symbol
53+
Some(body.apply((sym, method.returnTpt.tpe)))
54+
55+
val members = cls.declarations
56+
.filterNot(_.isClassConstructor)
57+
.map: sym =>
58+
sym.tree match
59+
case method: DefDef => DefDef(sym, transformDef(method))
60+
case _ => report.errorAndAbort(s"Not supported: $sym in ${sym.owner}")
61+
62+
val newCls = New(TypeIdent(cls)).select(cls.primaryConstructor).appliedToNone
63+
Block(ClassDef(cls, parents, members) :: Nil, newCls).asExprOf[F[B]]

Diff for: tests/pos-macros/i20471/Main_2.scala

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import scala.annotation.experimental
2+
3+
@experimental
4+
object autoFlatMapTests:
5+
trait TestAlgebra[T] derives FlatMap:
6+
def abstractEffect(a: String): T
7+
def concreteEffect(a: String): T = abstractEffect(a + " concreteEffect")

0 commit comments

Comments
 (0)