Skip to content

Commit fd5ec93

Browse files
committed
Fix #7110: Pickle types of quote holes
1 parent d84d597 commit fd5ec93

26 files changed

+376
-82
lines changed

compiler/src/dotty/tools/dotc/ast/tpd.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ object tpd extends Trees.Instance[Type] with TypedTreeInfo {
4242
Super(qual, if (mixName.isEmpty) untpd.EmptyTypeIdent else untpd.Ident(mixName), mixinClass)
4343

4444
def Apply(fn: Tree, args: List[Tree])(implicit ctx: Context): Apply = {
45-
assert(fn.isInstanceOf[RefTree] || fn.isInstanceOf[GenericApply[_]] || fn.isInstanceOf[Inlined])
45+
assert(fn.isInstanceOf[RefTree] || fn.isInstanceOf[GenericApply[_]] || fn.isInstanceOf[Inlined] || fn.isInstanceOf[tasty.TreePickler.Hole])
4646
ta.assignType(untpd.Apply(fn, args), fn, args)
4747
}
4848

compiler/src/dotty/tools/dotc/config/Printers.scala

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,3 +46,6 @@ object Printers {
4646
val unapp = noPrinter
4747
val variances = noPrinter
4848
}
49+
50+
// tests/run-macros/i7519c failed
51+
// tests/pos-macros/i7853 failed

compiler/src/dotty/tools/dotc/core/quoted/PickledQuotes.scala

Lines changed: 86 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import dotty.tools.dotc.core.tasty.{ PositionPickler, TastyPickler, TastyPrinter
1616
import dotty.tools.dotc.core.tasty.TreeUnpickler.UnpickleMode
1717
import dotty.tools.dotc.quoted.QuoteContext
1818
import dotty.tools.dotc.tastyreflect.{ReflectionImpl, TastyTreeExpr, TreeType}
19+
import dotty.tools.dotc.util.Spans.NoSpan
1920

2021
import dotty.tools.tasty.TastyString
2122

@@ -61,44 +62,107 @@ object PickledQuotes {
6162
}.apply(tp)
6263

6364
/** Unpickle the tree contained in the TastyExpr */
64-
def unpickleExpr(tasty: PickledQuote, args: PickledArgs)(implicit ctx: Context): Tree = {
65+
def unpickleExpr(tasty: PickledQuote, splices: PickledArgs)(implicit ctx: Context): Tree = {
6566
val tastyBytes = TastyString.unpickle(tasty)
66-
val unpickled = unpickle(tastyBytes, args, isType = false)(ctx.addMode(Mode.ReadPositions))
67+
val unpickled = unpickle(tastyBytes, splices, isType = false)(ctx.addMode(Mode.ReadPositions))
6768
/** Force unpickling of the tree, removes the spliced type `@quotedTypeTag type` definitions and dealiases references to `@quotedTypeTag type` */
68-
val forceAndCleanArtefacts = new TreeMap {
69+
70+
val Inlined(call, Nil, expnasion) = unpickled
71+
72+
val (expansion1, spliceTypes) = extractTypeTags(expnasion, splices)
73+
74+
val expnasion2 =
75+
spliceTypes match
76+
case Some(typeMap) =>
77+
val expansion2 = new TreeTypeMap(typeMap).transform(expansion1)
78+
quotePickling.println(i"**** typed quote\n${expansion2.show}")
79+
expansion2
80+
case _ => expansion1
81+
82+
val evaluateHoles = new TreeMap {
6983
override def transform(tree: tpd.Tree)(implicit ctx: Context): tpd.Tree = tree match {
70-
case Block(stat :: rest, expr1) if stat.symbol.hasAnnotation(defn.InternalQuoted_QuoteTypeTagAnnot) =>
71-
assert(rest.forall { case tdef: TypeDef => tdef.symbol.hasAnnotation(defn.InternalQuoted_QuoteTypeTagAnnot) })
72-
transform(expr1)
73-
case tree => super.transform(tree).withType(dealiasTypeTags(tree.tpe))
84+
case Hole(isTerm, idx, args) =>
85+
assert(isTerm)
86+
val reifiedArgs = args.map { arg =>
87+
if (arg.isTerm) (qctx: scala.quoted.QuoteContext) ?=> new TastyTreeExpr(arg, QuoteContext.scopeId)
88+
else new TreeType(arg, QuoteContext.scopeId)
89+
}
90+
val splice1 = splices(idx).asInstanceOf[Seq[Any] => scala.quoted.QuoteContext ?=> quoted.Expr[?]]
91+
val quotedExpr = splice1(reifiedArgs)(using dotty.tools.dotc.quoted.QuoteContext())
92+
val filled = PickledQuotes.quotedExprToTree(quotedExpr)
93+
94+
// We need to make sure a hole is created with the source file of the surrounding context, even if
95+
// it filled with contents a different source file.
96+
if filled.source == ctx.source then filled
97+
else filled.cloneIn(ctx.source).withSpan(tree.span)
98+
99+
case tree: Select =>
100+
// Retain selected members
101+
val qual = transform(tree.qualifier)
102+
qual.select(tree.symbol).withSpan(tree.span)
103+
104+
case tree =>
105+
if tree.isDef then
106+
tree.symbol.annotations = tree.symbol.annotations.map {
107+
annot => annot.derivedAnnotation(transform(annot.tree))
108+
}
109+
end if
110+
super.transform(tree)
74111
}
75112
}
76-
forceAndCleanArtefacts.transform(unpickled)
113+
val evaluatedExpansion = evaluateHoles.transform(expnasion2)(inlineContext(call))
114+
quotePickling.println(i"**** evaluated quote\n${evaluatedExpansion.show}")
115+
cpy.Inlined(unpickled)(call, Nil, evaluatedExpansion)
77116
}
78117

79118
/** Unpickle the tree contained in the TastyType */
80119
def unpickleType(tasty: PickledQuote, args: PickledArgs)(implicit ctx: Context): Tree = {
81120
val tastyBytes = TastyString.unpickle(tasty)
82121
val unpickled = unpickle(tastyBytes, args, isType = true)(ctx.addMode(Mode.ReadPositions))
83-
val tpt = unpickled match {
84-
case Block(aliases, tpt) =>
85-
// `@quoteTypeTag type` aliases are not required after unpickling.
86-
// Type definitions are placeholders for type holes in the pickled quote, at this point
87-
// those holes have been filled. As we already dealias al references to them in `dealiasTypeTags`
88-
// there is no need to keep their definitions in the tree. As artifacts of quote reification
89-
// they also do not have a meaningful position in the source.
90-
val aliases1 = aliases.filter(!_.symbol.hasAnnotation(defn.InternalQuoted_QuoteTypeTagAnnot))
91-
seq(aliases1, tpt)
92-
case tpt => tpt
122+
val (expnasion1, spliceTypes) = extractTypeTags(unpickled, args)
123+
val evaluatedExpansion = spliceTypes match
124+
case Some(typeMap) => new TreeTypeMap(typeMap).transform(expnasion1)
125+
case None => expnasion1
126+
quotePickling.println(i"**** typed quote\n${evaluatedExpansion.show}")
127+
evaluatedExpansion
128+
}
129+
130+
def extractTypeTags(expnasion: Tree, splices: PickledArgs)(using Context): (Tree, Option[Type => Type]) = {
131+
class ReplaceSplicedTyped(typeSpliceMap: Map[Symbol, Type]) extends TypeMap() {
132+
override def apply(tp: Type): Type = {
133+
val tp1 = tp match {
134+
case tp: TypeRef =>
135+
typeSpliceMap.get(tp.symbol) match
136+
case Some(t) if tp.typeSymbol.hasAnnotation(defn.InternalQuoted_QuoteTypeTagAnnot) => t
137+
case None => tp
138+
case _ => tp
139+
}
140+
mapOver(tp1)
141+
}
93142
}
94-
tpt.withType(dealiasTypeTags(tpt.tpe))
143+
144+
expnasion match
145+
case Block(stat :: rest, expr1) if stat.symbol.hasAnnotation(defn.InternalQuoted_QuoteTypeTagAnnot) =>
146+
val map = (stat :: rest).iterator.map {
147+
case tdef: TypeDef =>
148+
assert(tdef.symbol.hasAnnotation(defn.InternalQuoted_QuoteTypeTagAnnot))
149+
val tree = tdef.rhs match
150+
case TypeBoundsTree(_, Hole(_, idx, args), _) =>
151+
val quotedType = splices(idx).asInstanceOf[Seq[Any] => quoted.Type[?]](args)
152+
PickledQuotes.quotedTypeToTree(quotedType)
153+
case TypeBoundsTree(_, tpt, _) =>
154+
tpt
155+
(tdef.symbol, tree.tpe)
156+
}.toMap
157+
(expr1, Some(new ReplaceSplicedTyped(map)))
158+
case _ => (expnasion, None)
95159
}
96160

97161
// TASTY picklingtests/pos/quoteTest.scala
98162

99163
/** Pickle tree into it's TASTY bytes s*/
100164
private def pickle(tree: Tree)(implicit ctx: Context): Array[Byte] = {
101-
quotePickling.println(i"**** pickling quote of \n${tree.show}")
165+
quotePickling.println(i"**** pickling quote of\n${tree.show}")
102166
val pickler = new TastyPickler(defn.RootClass)
103167
val treePkl = pickler.treePkl
104168
treePkl.pickle(tree :: Nil)
@@ -109,7 +173,7 @@ object PickledQuotes {
109173
new PositionPickler(pickler, treePkl.buf.addrOfTree).picklePositions(tree :: Nil)
110174

111175
val pickled = pickler.assembleParts()
112-
quotePickling.println(s"**** pickled quote\n${new TastyPrinter(pickled).printContents()}")
176+
quotePickling.println(s"**** pickledd quote\n${new TastyPrinter(pickled).printContents()}")
113177
pickled
114178
}
115179

@@ -122,7 +186,7 @@ object PickledQuotes {
122186
unpickler.enter(Set.empty)
123187

124188
val tree = unpickler.tree
125-
quotePickling.println(i"**** unpickle quote ${tree.show}")
189+
quotePickling.println(i"**** unpickled quote\n${tree.show}")
126190
tree
127191
}
128192

compiler/src/dotty/tools/dotc/core/tasty/TreePickler.scala

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,8 @@ object TreePickler {
2626
override def isTerm: Boolean = isTermHole
2727
override def isType: Boolean = !isTermHole
2828
override def fallbackToText(printer: Printer): Text =
29-
s"[[$idx|" ~~ printer.toTextGlobal(tpe) ~~ "|" ~~ printer.toTextGlobal(args, ", ") ~~ "]]"
29+
if isTermHole then s"{{{ $idx |" ~~ printer.toTextGlobal(tpe) ~~ "|" ~~ printer.toTextGlobal(args, ", ") ~~ "}}}"
30+
else s"[[[ $idx |" ~~ printer.toTextGlobal(tpe) ~~ "|" ~~ printer.toTextGlobal(args, ", ") ~~ "]]]"
3031
}
3132
}
3233

@@ -600,6 +601,15 @@ class TreePickler(pickler: TastyPickler) {
600601
pickleTree(alias)
601602
}
602603
case Hole(_, idx, args) =>
604+
lazy val erasedSplicesType = new TypeMap() {
605+
override def apply(tp: Type): Type = tp match {
606+
case tp: TypeRef if tp.typeSymbol.isSplice || tp.typeSymbol.hasAnnotation(defn.InternalQuoted_QuoteTypeTagAnnot) => tp.dealias.typeSymbol.info.hiBound
607+
case tp =>
608+
mapOver(tp)
609+
}
610+
}
611+
val tpe = erasedSplicesType(tree.tpe)
612+
603613
writeByte(HOLE)
604614
withLength {
605615
writeNat(idx)

compiler/src/dotty/tools/dotc/core/tasty/TreeUnpickler.scala

Lines changed: 8 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1224,7 +1224,10 @@ class TreeUnpickler(reader: TastyReader,
12241224
val alias = if currentAddr == end then EmptyTree else readTpt()
12251225
TypeBoundsTree(lo, hi, alias)
12261226
case HOLE =>
1227-
readHole(end, isType = false)
1227+
val idx = readNat()
1228+
val tpe = readType()
1229+
val args = until(end)(readTerm())
1230+
TreePickler.Hole(true, idx, args).withType(tpe)
12281231
case _ =>
12291232
readPathTerm()
12301233
}
@@ -1257,7 +1260,10 @@ class TreeUnpickler(reader: TastyReader,
12571260
case HOLE =>
12581261
readByte()
12591262
val end = readEnd()
1260-
readHole(end, isType = true)
1263+
val idx = readNat()
1264+
val tpe = readType()
1265+
val args = until(end)(readTerm())
1266+
TreePickler.Hole(false, idx, args).withType(tpe)
12611267
case _ =>
12621268
if (isTypeTreeTag(nextByte)) readTerm()
12631269
else {
@@ -1299,36 +1305,6 @@ class TreeUnpickler(reader: TastyReader,
12991305
owner => new LazyReader(localReader, owner, ctx.mode, ctx.source, op)
13001306
}
13011307

1302-
def readHole(end: Addr, isType: Boolean)(implicit ctx: Context): Tree = {
1303-
val idx = readNat()
1304-
val tpe = readType()
1305-
val args = until(end)(readTerm())
1306-
val splice = splices(idx)
1307-
def wrap(arg: Tree) =
1308-
if (arg.isTerm) (qctx: scala.quoted.QuoteContext) ?=> new TastyTreeExpr(arg, QuoteContext.scopeId)
1309-
else new TreeType(arg, QuoteContext.scopeId)
1310-
val reifiedArgs = args.map(wrap)
1311-
val filled = if (isType) {
1312-
val quotedType = splice.asInstanceOf[Seq[Any] => quoted.Type[?]](reifiedArgs)
1313-
PickledQuotes.quotedTypeToTree(quotedType)
1314-
}
1315-
else {
1316-
val splice1 = splice.asInstanceOf[Seq[Any] => scala.quoted.QuoteContext ?=> quoted.Expr[?]]
1317-
val quotedExpr = splice1(reifiedArgs)(using dotty.tools.dotc.quoted.QuoteContext())
1318-
PickledQuotes.quotedExprToTree(quotedExpr)
1319-
}
1320-
// We need to make sure a hole is created with the source file of the surrounding context, even if
1321-
// it filled with contents a different source file. Otherwise nodes containing holes might end
1322-
// up without a position. PositionPickler makes sure that holes always get spans assigned,
1323-
// so we can just return the filler tree with the new source and no span here.
1324-
if (filled.source == ctx.source) filled
1325-
else {
1326-
val filled1 = filled.cloneIn(ctx.source)
1327-
filled1.span = NoSpan
1328-
filled1
1329-
}
1330-
}
1331-
13321308
// ------ Setting positions ------------------------------------------------
13331309

13341310
/** Pickled span for `addr`. */

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

Lines changed: 28 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -331,23 +331,35 @@ class ReifyQuotes extends MacroTransform {
331331
*/
332332
private def makeHole(isTermHole: Boolean, body: Tree, splices: List[Tree], tpe: Type)(implicit ctx: Context): Hole = {
333333
val idx = embedded.addTree(body, NoSymbol)
334-
val holeType = getHoleType(tpe)
335-
Hole(isTermHole, idx, splices).withType(holeType).asInstanceOf[Hole]
336-
}
337334

338-
private def getHoleType(using ctx: Context) = new TypeMap() {
339-
override def apply(tp: Type): Type = tp match
340-
case tp: TypeRef if tp.typeSymbol.isSplice =>
341-
apply(tp.dealias)
342-
case tp @ TypeRef(pre, _) if pre == NoPrefix || pre.termSymbol.isLocal =>
343-
val hiBound = tp.typeSymbol.info match
344-
case info @ ClassInfo(_, _, classParents, _, _) => classParents.reduce(_ & _)
345-
case info => info.hiBound
346-
apply(hiBound)
347-
case tp @ TermRef(NoPrefix, _) =>
348-
apply(tp.widenTermRefExpr)
349-
case tp =>
350-
mapOver(tp)
335+
def getTypeHoleType(using ctx: Context) = new TypeMap() {
336+
override def apply(tp: Type): Type = tp match
337+
case tp: TypeRef if tp.typeSymbol.isSplice =>
338+
apply(tp.dealias)
339+
case tp @ TypeRef(pre, _) if pre == NoPrefix || pre.termSymbol.isLocal =>
340+
val hiBound = tp.typeSymbol.info match
341+
case info @ ClassInfo(_, _, classParents, _, _) => classParents.reduce(_ & _)
342+
case info => info.hiBound
343+
apply(hiBound)
344+
case tp =>
345+
mapOver(tp)
346+
}
347+
348+
def getTermHoleType(using ctx: Context) = new TypeMap() {
349+
override def apply(tp: Type): Type = tp match
350+
case tp @ TypeRef(NoPrefix, _) if capturers.contains(tp.symbol) =>
351+
// reference to term with a type defined in outer quote
352+
getTypeHoleType(tp)
353+
case tp @ TermRef(NoPrefix, _) if capturers.contains(tp.symbol) =>
354+
// widen term refs to terms defined in outer quote
355+
apply(tp.widenTermRefExpr)
356+
case tp =>
357+
mapOver(tp)
358+
}
359+
360+
val holeType = if isTermHole then getTermHoleType(tpe) else getTypeHoleType(tpe)
361+
362+
Hole(isTermHole, idx, splices).withType(holeType).asInstanceOf[Hole]
351363
}
352364

353365
override def transform(tree: Tree)(implicit ctx: Context): Tree =

compiler/test/dotty/tools/dotc/BootstrappedOnlyCompilationTests.scala

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,7 @@ class BootstrappedOnlyCompilationTests extends ParallelTesting {
100100

101101
@Test def negMacros: Unit = {
102102
implicit val testGroup: TestGroup = TestGroup("compileNegWithCompiler")
103-
compileFilesInDir("tests/neg-macros", defaultOptions).checkExpectedErrors()
103+
compileFilesInDir("tests/neg-macros", defaultOptions and "-Yprint-debug").checkExpectedErrors()
104104
}
105105

106106
@Test def negWithCompiler: Unit = {
@@ -116,17 +116,17 @@ class BootstrappedOnlyCompilationTests extends ParallelTesting {
116116
@Test def runMacros: Unit = {
117117
implicit val testGroup: TestGroup = TestGroup("runMacros")
118118
aggregateTests(
119-
compileFilesInDir("tests/run-macros", defaultOptions),
120-
compileFilesInDir("tests/run-custom-args/Yretain-trees", defaultOptions and "-Yretain-trees"),
121-
compileFilesInDir("tests/run-custom-args/run-macros-erased", defaultOptions and "-Yerased-terms"),
119+
compileFilesInDir("tests/run-macros", defaultOptions and "-Yprint-debug"),
120+
compileFilesInDir("tests/run-custom-args/Yretain-trees", defaultOptions and "-Yretain-trees" and "-Yprint-debug"),
121+
compileFilesInDir("tests/run-custom-args/run-macros-erased", defaultOptions and "-Yerased-terms" and "-Yprint-debug"),
122122
)
123123
}.checkRuns()
124124

125125
@Test def runWithCompiler: Unit = {
126126
implicit val testGroup: TestGroup = TestGroup("runWithCompiler")
127127
aggregateTests(
128128
compileFilesInDir("tests/run-with-compiler", withCompilerOptions),
129-
compileFilesInDir("tests/run-staging", withStagingOptions),
129+
compileFilesInDir("tests/run-staging", withStagingOptions and "-Yprint-debug"),
130130
compileFilesInDir("tests/run-custom-args/tasty-inspector", withTastyInspectorOptions),
131131
compileDir("tests/run-custom-args/tasty-interpreter", withTastyInspectorOptions),
132132
).checkRuns()

tests/neg-macros/beta-reduce-inline-result.check

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,5 @@
22
-- [E007] Type Mismatch Error: tests/neg-macros/beta-reduce-inline-result/Test_2.scala:11:41 ---------------------------
33
11 | val x2: 4 = Macros.betaReduce(dummy1)(3) // error
44
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
5-
| Found: Int
6-
| Required: (4 : Int)
5+
| Found: <root>.this.scala.Int
6+
| Required: (4 : scala.this.Int)

tests/neg-macros/delegate-match-1.check

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,5 @@
33
6 | f // error
44
| ^
55
| AmbiguousImplicits
6-
| both value a1 in class Test1 and value a2 in class Test1 match type A
6+
| both value a1 in class Test1 and value a2 in class Test1 match type <empty>.this.A
77
| This location contains code that was inlined from Test_2.scala:6

tests/neg-macros/delegate-match-2.check

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,5 @@
33
5 | f // error
44
| ^
55
| DivergingImplicit
6-
| method a1 in class Test produces a diverging implicit search when trying to match type A
6+
| method a1 in class Test produces a diverging implicit search when trying to match type <empty>.this.A
77
| This location contains code that was inlined from Test_2.scala:5

tests/neg-macros/delegate-match-3.check

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,5 @@
33
3 | f // error
44
| ^
55
| NoMatchingImplicits
6-
| no implicit values were found that match type A
6+
| no implicit values were found that match type <empty>.this.A
77
| This location contains code that was inlined from Test_2.scala:3

tests/pos-macros/i7110a/Macro_1.scala

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import scala.quoted._
2+
3+
object Macros {
4+
5+
inline def m[R](sym: Symantics[R]) : R = ${ mImpl[R]('{sym}) }
6+
7+
def mImpl[R: Type](using qctx: QuoteContext)(sym: Expr[Symantics[R]]): Expr[R] = '{
8+
$sym.Meth(42)
9+
}
10+
}
11+
12+
trait Symantics[R] {
13+
def Meth(exp: Int): R
14+
def Meth(): R
15+
}

tests/pos-macros/i7110a/Test_2.scala

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import scala.quoted._
2+
import Macros._
3+
4+
object Test {
5+
def main(args: Array[String]): Unit = {
6+
7+
val sym = new Symantics[Int] {
8+
def Meth(exp: Int): Int = exp
9+
def Meth(): Int = 42
10+
}
11+
12+
val test = m[Int](sym)
13+
}
14+
}

0 commit comments

Comments
 (0)