Skip to content

Commit 36253dc

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

26 files changed

+371
-86
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: 81 additions & 26 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,98 @@ 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-
/** Force unpickling of the tree, removes the spliced type `@quotedTypeTag type` definitions and dealiases references to `@quotedTypeTag type` */
68-
val forceAndCleanArtefacts = new TreeMap {
69-
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-
}
75-
}
76-
forceAndCleanArtefacts.transform(unpickled)
67+
val unpickled = unpickle(tastyBytes, splices, isType = false)(ctx.addMode(Mode.ReadPositions))
68+
val Inlined(call, Nil, expnasion) = unpickled
69+
val inlineCtx = inlineContext(call)
70+
val expansion1 = spliceTypes(expnasion, splices)(using inlineCtx)
71+
val expansion2 = spliceTerms(expansion1, splices)(using inlineCtx)
72+
cpy.Inlined(unpickled)(call, Nil, expansion2)
7773
}
7874

7975
/** Unpickle the tree contained in the TastyType */
8076
def unpickleType(tasty: PickledQuote, args: PickledArgs)(implicit ctx: Context): Tree = {
8177
val tastyBytes = TastyString.unpickle(tasty)
8278
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
79+
spliceTypes(unpickled, args)
80+
}
81+
82+
def spliceTerms(tree: Tree, splices: PickledArgs)(using Context): Tree = {
83+
val evaluateHoles = new TreeMap {
84+
override def transform(tree: tpd.Tree)(implicit ctx: Context): tpd.Tree = tree match {
85+
case Hole(isTerm, idx, args) =>
86+
assert(isTerm)
87+
val reifiedArgs = args.map { arg =>
88+
if (arg.isTerm) (qctx: scala.quoted.QuoteContext) ?=> new TastyTreeExpr(arg, QuoteContext.scopeId)
89+
else new TreeType(arg, QuoteContext.scopeId)
90+
}
91+
val splice1 = splices(idx).asInstanceOf[Seq[Any] => scala.quoted.QuoteContext ?=> quoted.Expr[?]]
92+
val quotedExpr = splice1(reifiedArgs)(using dotty.tools.dotc.quoted.QuoteContext())
93+
val filled = PickledQuotes.quotedExprToTree(quotedExpr)
94+
95+
// We need to make sure a hole is created with the source file of the surrounding context, even if
96+
// it filled with contents a different source file.
97+
if filled.source == ctx.source then filled
98+
else filled.cloneIn(ctx.source).withSpan(tree.span)
99+
100+
case tree: Select =>
101+
// Retain selected members
102+
val qual = transform(tree.qualifier)
103+
qual.select(tree.symbol).withSpan(tree.span)
104+
105+
case tree =>
106+
if tree.isDef then
107+
tree.symbol.annotations = tree.symbol.annotations.map {
108+
annot => annot.derivedAnnotation(transform(annot.tree))
109+
}
110+
end if
111+
super.transform(tree)
112+
}
93113
}
94-
tpt.withType(dealiasTypeTags(tpt.tpe))
114+
val tree1 = evaluateHoles.transform(tree)
115+
quotePickling.println(i"**** evaluated quote\n${tree1.show}")
116+
tree1
117+
}
118+
119+
def spliceTypes(tree: Tree, splices: PickledArgs)(using Context): Tree = {
120+
tree match
121+
case Block(stat :: rest, expr1) if stat.symbol.hasAnnotation(defn.InternalQuoted_QuoteTypeTagAnnot) =>
122+
val typeSpliceMap = (stat :: rest).iterator.map {
123+
case tdef: TypeDef =>
124+
assert(tdef.symbol.hasAnnotation(defn.InternalQuoted_QuoteTypeTagAnnot))
125+
val tree = tdef.rhs match
126+
case TypeBoundsTree(_, Hole(_, idx, args), _) =>
127+
val quotedType = splices(idx).asInstanceOf[Seq[Any] => quoted.Type[?]](args)
128+
PickledQuotes.quotedTypeToTree(quotedType)
129+
case TypeBoundsTree(_, tpt, _) =>
130+
tpt
131+
(tdef.symbol, tree.tpe)
132+
}.toMap
133+
class ReplaceSplicedTyped extends TypeMap() {
134+
override def apply(tp: Type): Type = {
135+
val tp1 = tp match {
136+
case tp: TypeRef =>
137+
typeSpliceMap.get(tp.symbol) match
138+
case Some(t) if tp.typeSymbol.hasAnnotation(defn.InternalQuoted_QuoteTypeTagAnnot) => t
139+
case None => tp
140+
case _ => tp
141+
}
142+
mapOver(tp1)
143+
}
144+
}
145+
val expansion2 = new TreeTypeMap(new ReplaceSplicedTyped).transform(expr1)
146+
quotePickling.println(i"**** typed quote\n${expansion2.show}")
147+
expansion2
148+
case _ =>
149+
tree
95150
}
96151

97152
// TASTY picklingtests/pos/quoteTest.scala
98153

99154
/** Pickle tree into it's TASTY bytes s*/
100155
private def pickle(tree: Tree)(implicit ctx: Context): Array[Byte] = {
101-
quotePickling.println(i"**** pickling quote of \n${tree.show}")
156+
quotePickling.println(i"**** pickling quote of\n${tree.show}")
102157
val pickler = new TastyPickler(defn.RootClass)
103158
val treePkl = pickler.treePkl
104159
treePkl.pickle(tree :: Nil)
@@ -109,7 +164,7 @@ object PickledQuotes {
109164
new PositionPickler(pickler, treePkl.buf.addrOfTree).picklePositions(tree :: Nil)
110165

111166
val pickled = pickler.assembleParts()
112-
quotePickling.println(s"**** pickled quote\n${new TastyPrinter(pickled).printContents()}")
167+
quotePickling.println(s"**** pickledd quote\n${new TastyPrinter(pickled).printContents()}")
113168
pickled
114169
}
115170

@@ -122,7 +177,7 @@ object PickledQuotes {
122177
unpickler.enter(Set.empty)
123178

124179
val tree = unpickler.tree
125-
quotePickling.println(i"**** unpickle quote ${tree.show}")
180+
quotePickling.println(i"**** unpickled quote\n${tree.show}")
126181
tree
127182
}
128183

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)