Skip to content

Commit b89405c

Browse files
committed
Fix #7110: Pickle types of quote holes
1 parent 12bbf53 commit b89405c

25 files changed

+373
-85
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: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,3 +46,19 @@ object Printers {
4646
val unapp = noPrinter
4747
val variances = noPrinter
4848
}
49+
50+
// tests/run-macros/i7519c failed
51+
// tests/run-macros/quote-matching-optimize-2 failed
52+
// tests/run-macros/quote-matcher-symantics-3 failed
53+
// tests/run-macros/i5941 failed
54+
// tests/run-macros/quote-matching-optimize-1 failed
55+
// tests/run-macros/i8007 failed
56+
// tests/run-macros/quote-matching-optimize-3 failed
57+
// tests/pos-macros/i7853 failed
58+
59+
// tests/run-staging/i6281.scala failed
60+
// tests/run-staging/shonan-hmm failed
61+
// tests/run-staging/staged-tuples failed
62+
// tests/run-staging/staged-streams_1.scala failed
63+
// tests/run-staging/quote-lib.scala failed
64+
// tests/run-staging/shonan-hmm-simple.scala failed

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

Lines changed: 86 additions & 25 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

@@ -50,55 +51,115 @@ object PickledQuotes {
5051
}
5152

5253
private def dealiasTypeTags(tp: Type)(implicit ctx: Context): Type = new TypeMap() {
53-
override def apply(tp: Type): Type = {
54-
val tp1 = tp match {
54+
override def apply(tp: Type): Type =
55+
val tp1 = tp match
5556
case tp: TypeRef if tp.typeSymbol.hasAnnotation(defn.InternalQuoted_QuoteTypeTagAnnot) =>
5657
tp.symbol.info.hiBound
5758
case _ => tp
58-
}
5959
mapOver(tp1)
60-
}
6160
}.apply(tp)
6261

6362
/** Unpickle the tree contained in the TastyExpr */
64-
def unpickleExpr(tasty: PickledQuote, args: PickledArgs)(implicit ctx: Context): Tree = {
63+
def unpickleExpr(tasty: PickledQuote, splices: PickledArgs)(implicit ctx: Context): Tree = {
6564
val tastyBytes = TastyString.unpickle(tasty)
66-
val unpickled = unpickle(tastyBytes, args, isType = false)(ctx.addMode(Mode.ReadPositions))
65+
val unpickled = unpickle(tastyBytes, splices, isType = false)(ctx.addMode(Mode.ReadPositions))
6766
/** Force unpickling of the tree, removes the spliced type `@quotedTypeTag type` definitions and dealiases references to `@quotedTypeTag type` */
68-
val forceAndCleanArtefacts = new TreeMap {
67+
68+
val Inlined(call, Nil, expnasion) = unpickled
69+
70+
val (expnasion1, spliceTypes) = extractTypeTags(expnasion, splices)
71+
72+
val evaluateHoles = new TreeMap {
6973
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))
74+
case Hole(isTerm, idx, args) =>
75+
val reifiedArgs = args.map { arg =>
76+
if (arg.isTerm) (qctx: scala.quoted.QuoteContext) ?=> new TastyTreeExpr(arg, QuoteContext.scopeId)
77+
else new TreeType(arg, QuoteContext.scopeId)
78+
}
79+
val splice = splices(idx)
80+
81+
if isTerm then
82+
val splice1 = splice.asInstanceOf[Seq[Any] => scala.quoted.QuoteContext ?=> quoted.Expr[?]]
83+
val quotedExpr = splice1(reifiedArgs)(using dotty.tools.dotc.quoted.QuoteContext())
84+
val filled = PickledQuotes.quotedExprToTree(quotedExpr)
85+
86+
// We need to make sure a hole is created with the source file of the surrounding context, even if
87+
// it filled with contents a different source file.
88+
if filled.source == ctx.source then filled
89+
else filled.cloneIn(ctx.source).withSpan(tree.span)
90+
else
91+
val quotedType = splice.asInstanceOf[Seq[Any] => quoted.Type[?]](reifiedArgs)
92+
PickledQuotes.quotedTypeToTree(quotedType)
93+
94+
case tree =>
95+
if tree.isDef then
96+
tree.symbol.annotations = tree.symbol.annotations.map {
97+
annot => annot.derivedAnnotation(transform(annot.tree))
98+
}
99+
end if
100+
val tree1 = super.transform(tree)
101+
spliceTypes match
102+
case Some(f) => tree1.withType(f(tree1.tpe))
103+
case None => tree1
74104
}
75105
}
76-
forceAndCleanArtefacts.transform(unpickled)
106+
val evaluatedExpansion = evaluateHoles.transform(expnasion1)(inlineContext(call))
107+
quotePickling.println(i"**** evaluated quote\n${evaluatedExpansion.show}")
108+
cpy.Inlined(unpickled)(call, Nil, evaluatedExpansion)
77109
}
78110

79111
/** Unpickle the tree contained in the TastyType */
80112
def unpickleType(tasty: PickledQuote, args: PickledArgs)(implicit ctx: Context): Tree = {
81113
val tastyBytes = TastyString.unpickle(tasty)
82114
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
115+
val (expnasion1, spliceTypes) = extractTypeTags(unpickled, args)
116+
val evaluatedExpansion = spliceTypes match
117+
case Some(f) =>
118+
new TreeMap {
119+
override def transform(tree: tpd.Tree)(implicit ctx: Context): tpd.Tree =
120+
super.transform(tree).withType(f(tree.tpe))
121+
}.transform(expnasion1)
122+
case None => expnasion1
123+
quotePickling.println(i"**** evaluated quote\n${evaluatedExpansion.show}")
124+
evaluatedExpansion
125+
}
126+
127+
def extractTypeTags(expnasion: Tree, splices: PickledArgs)(using Context): (Tree, Option[Type => Type]) = {
128+
class ReplaceSplicedTyped(typeSpliceMap: Map[Symbol, Type]) extends TypeMap() {
129+
override def apply(tp: Type): Type = {
130+
val tp1 = tp match {
131+
case tp: TypeRef =>
132+
typeSpliceMap.get(tp.symbol) match
133+
case Some(t) if tp.typeSymbol.hasAnnotation(defn.InternalQuoted_QuoteTypeTagAnnot) => t
134+
case None => tp
135+
case _ => tp
136+
}
137+
mapOver(tp1)
138+
}
93139
}
94-
tpt.withType(dealiasTypeTags(tpt.tpe))
140+
141+
expnasion match
142+
case Block(stat :: rest, expr1) if stat.symbol.hasAnnotation(defn.InternalQuoted_QuoteTypeTagAnnot) =>
143+
val map = (stat :: rest).iterator.map {
144+
case tdef: TypeDef =>
145+
assert(tdef.symbol.hasAnnotation(defn.InternalQuoted_QuoteTypeTagAnnot))
146+
val tree = tdef.rhs match
147+
case TypeBoundsTree(_, Hole(_, idx, args), _) =>
148+
val quotedType = splices(idx).asInstanceOf[Seq[Any] => quoted.Type[?]](args)
149+
PickledQuotes.quotedTypeToTree(quotedType)
150+
case TypeBoundsTree(_, tpt, _) =>
151+
tpt
152+
(tdef.symbol, tree.tpe)
153+
}.toMap
154+
(expr1, Some(new ReplaceSplicedTyped(map)))
155+
case _ => (expnasion, None)
95156
}
96157

97158
// TASTY picklingtests/pos/quoteTest.scala
98159

99160
/** Pickle tree into it's TASTY bytes s*/
100161
private def pickle(tree: Tree)(implicit ctx: Context): Array[Byte] = {
101-
quotePickling.println(i"**** pickling quote of \n${tree.show}")
162+
quotePickling.println(i"**** pickling quote of\n${tree.show}")
102163
val pickler = new TastyPickler(defn.RootClass)
103164
val treePkl = pickler.treePkl
104165
treePkl.pickle(tree :: Nil)
@@ -122,7 +183,7 @@ object PickledQuotes {
122183
unpickler.enter(Set.empty)
123184

124185
val tree = unpickler.tree
125-
quotePickling.println(i"**** unpickle quote ${tree.show}")
186+
quotePickling.println(i"**** unpickle quote\n${tree.show}")
126187
tree
127188
}
128189

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

@@ -599,6 +600,15 @@ class TreePickler(pickler: TastyPickler) {
599600
pickleTree(alias)
600601
}
601602
case Hole(_, idx, args) =>
603+
lazy val erasedSplicesType = new TypeMap() {
604+
override def apply(tp: Type): Type = tp match {
605+
case tp: TypeRef if tp.typeSymbol.isSplice || tp.typeSymbol.hasAnnotation(defn.InternalQuoted_QuoteTypeTagAnnot) => tp.dealias.typeSymbol.info.hiBound
606+
case tp =>
607+
mapOver(tp)
608+
}
609+
}
610+
val tpe = erasedSplicesType(tree.tpe)
611+
602612
writeByte(HOLE)
603613
withLength {
604614
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
@@ -1213,7 +1213,10 @@ class TreeUnpickler(reader: TastyReader,
12131213
val alias = if currentAddr == end then EmptyTree else readTpt()
12141214
TypeBoundsTree(lo, hi, alias)
12151215
case HOLE =>
1216-
readHole(end, isType = false)
1216+
val idx = readNat()
1217+
val tpe = readType()
1218+
val args = until(end)(readTerm())
1219+
TreePickler.Hole(true, idx, args).withType(tpe)
12171220
case _ =>
12181221
readPathTerm()
12191222
}
@@ -1246,7 +1249,10 @@ class TreeUnpickler(reader: TastyReader,
12461249
case HOLE =>
12471250
readByte()
12481251
val end = readEnd()
1249-
readHole(end, isType = true)
1252+
val idx = readNat()
1253+
val tpe = readType()
1254+
val args = until(end)(readTerm())
1255+
TreePickler.Hole(false, idx, args).withType(tpe)
12501256
case _ =>
12511257
if (isTypeTreeTag(nextByte)) readTerm()
12521258
else {
@@ -1288,36 +1294,6 @@ class TreeUnpickler(reader: TastyReader,
12881294
owner => new LazyReader(localReader, owner, ctx.mode, ctx.source, op)
12891295
}
12901296

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

13231299
/** 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

0 commit comments

Comments
 (0)