Skip to content

Fix #7110: Fix splice overload #8561

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Apr 1, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion compiler/src/dotty/tools/dotc/ast/tpd.scala
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ object tpd extends Trees.Instance[Type] with TypedTreeInfo {
Super(qual, if (mixName.isEmpty) untpd.EmptyTypeIdent else untpd.Ident(mixName), mixinClass)

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

Expand Down
143 changes: 106 additions & 37 deletions compiler/src/dotty/tools/dotc/core/quoted/PickledQuotes.scala
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import dotty.tools.dotc.core.Symbols._
import dotty.tools.dotc.core.Types._
import dotty.tools.dotc.core.tasty.TreePickler.Hole
import dotty.tools.dotc.core.tasty.{ PositionPickler, TastyPickler, TastyPrinter }
import dotty.tools.dotc.core.tasty.DottyUnpickler
import dotty.tools.dotc.core.tasty.TreeUnpickler.UnpickleMode
import dotty.tools.dotc.quoted.QuoteContext
import dotty.tools.dotc.tastyreflect.{ReflectionImpl, TastyTreeExpr, TreeType}
Expand Down Expand Up @@ -49,56 +50,118 @@ object PickledQuotes {
healOwner(tpe1.typeTree)
}

private def dealiasTypeTags(tp: Type)(implicit ctx: Context): Type = new TypeMap() {
override def apply(tp: Type): Type = {
val tp1 = tp match {
case tp: TypeRef if tp.typeSymbol.hasAnnotation(defn.InternalQuoted_QuoteTypeTagAnnot) =>
tp.symbol.info.hiBound
case _ => tp
}
mapOver(tp1)
}
}.apply(tp)

/** Unpickle the tree contained in the TastyExpr */
def unpickleExpr(tasty: PickledQuote, args: PickledArgs)(implicit ctx: Context): Tree = {
def unpickleExpr(tasty: PickledQuote, splices: PickledArgs)(implicit ctx: Context): Tree = {
val tastyBytes = TastyString.unpickle(tasty)
val unpickled = unpickle(tastyBytes, args, isType = false)(ctx.addMode(Mode.ReadPositions))
/** Force unpickling of the tree, removes the spliced type `@quotedTypeTag type` definitions and dealiases references to `@quotedTypeTag type` */
val forceAndCleanArtefacts = new TreeMap {
override def transform(tree: tpd.Tree)(implicit ctx: Context): tpd.Tree = tree match {
case Block(stat :: rest, expr1) if stat.symbol.hasAnnotation(defn.InternalQuoted_QuoteTypeTagAnnot) =>
assert(rest.forall { case tdef: TypeDef => tdef.symbol.hasAnnotation(defn.InternalQuoted_QuoteTypeTagAnnot) })
transform(expr1)
case tree => super.transform(tree).withType(dealiasTypeTags(tree.tpe))
}
}
forceAndCleanArtefacts.transform(unpickled)
val unpickled = unpickle(tastyBytes, splices, isType = false)(ctx.addMode(Mode.ReadPositions))
val Inlined(call, Nil, expnasion) = unpickled
val inlineCtx = inlineContext(call)
val expansion1 = spliceTypes(expnasion, splices)(using inlineCtx)
val expansion2 = spliceTerms(expansion1, splices)(using inlineCtx)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Small opt: can we just do one pass for splicing?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, that was the first approach I tried. We need the types to be known when we expand the term holes as some of them may take some type parameters that the user inspects. Note that spliceTypes is a no-op if the quote does not have type splices.

cpy.Inlined(unpickled)(call, Nil, expansion2)
}

/** Unpickle the tree contained in the TastyType */
def unpickleType(tasty: PickledQuote, args: PickledArgs)(implicit ctx: Context): Tree = {
val tastyBytes = TastyString.unpickle(tasty)
val unpickled = unpickle(tastyBytes, args, isType = true)(ctx.addMode(Mode.ReadPositions))
val tpt = unpickled match {
case Block(aliases, tpt) =>
// `@quoteTypeTag type` aliases are not required after unpickling.
// Type definitions are placeholders for type holes in the pickled quote, at this point
// those holes have been filled. As we already dealias al references to them in `dealiasTypeTags`
// there is no need to keep their definitions in the tree. As artifacts of quote reification
// they also do not have a meaningful position in the source.
val aliases1 = aliases.filter(!_.symbol.hasAnnotation(defn.InternalQuoted_QuoteTypeTagAnnot))
seq(aliases1, tpt)
case tpt => tpt
spliceTypes(unpickled, args)
}

/** Replace all term holes with the spliced terms */
private def spliceTerms(tree: Tree, splices: PickledArgs)(using Context): Tree = {
val evaluateHoles = new TreeMap {
override def transform(tree: tpd.Tree)(implicit ctx: Context): tpd.Tree = tree match {
case Hole(isTerm, idx, args) =>
val reifiedArgs = args.map { arg =>
if (arg.isTerm) (using qctx: scala.quoted.QuoteContext) => new TastyTreeExpr(arg, QuoteContext.scopeId)
else new TreeType(arg, QuoteContext.scopeId)
}
if isTerm then
val splice1 = splices(idx).asInstanceOf[Seq[Any] => scala.quoted.QuoteContext ?=> quoted.Expr[?]]
val quotedExpr = splice1(reifiedArgs)(using dotty.tools.dotc.quoted.QuoteContext())
val filled = PickledQuotes.quotedExprToTree(quotedExpr)

// We need to make sure a hole is created with the source file of the surrounding context, even if
// it filled with contents a different source file.
if filled.source == ctx.source then filled
else filled.cloneIn(ctx.source).withSpan(tree.span)
else
// Replaces type holes generated by ReifyQuotes (non-spliced types).
// These are types defined in a quote and used at the same level in a nested quote.
val quotedType = splices(idx).asInstanceOf[Seq[Any] => quoted.Type[?]](reifiedArgs)
PickledQuotes.quotedTypeToTree(quotedType)
case tree: Select =>
// Retain selected members
val qual = transform(tree.qualifier)
qual.select(tree.symbol).withSpan(tree.span)

case tree =>
if tree.isDef then
tree.symbol.annotations = tree.symbol.annotations.map {
annot => annot.derivedAnnotation(transform(annot.tree))
}
end if

val tree1 = super.transform(tree)
tree1.withType(mapAnnots(tree1.tpe))
}

// Evaluate holes in type annotations
private val mapAnnots = new TypeMap {
override def apply(tp: Type): Type = {
tp match
case tp @ AnnotatedType(underlying, annot) =>
val underlying1 = this(underlying)
derivedAnnotatedType(tp, underlying1, annot.derivedAnnotation(transform(annot.tree)))
case _ => mapOver(tp)
}
}
}
tpt.withType(dealiasTypeTags(tpt.tpe))
val tree1 = evaluateHoles.transform(tree)
quotePickling.println(i"**** evaluated quote\n$tree1")
tree1
}

/** Replace all type holes generated with the spliced types */
private def spliceTypes(tree: Tree, splices: PickledArgs)(using Context): Tree = {
tree match
case Block(stat :: rest, expr1) if stat.symbol.hasAnnotation(defn.InternalQuoted_QuoteTypeTagAnnot) =>
val typeSpliceMap = (stat :: rest).iterator.map {
case tdef: TypeDef =>
assert(tdef.symbol.hasAnnotation(defn.InternalQuoted_QuoteTypeTagAnnot))
val tree = tdef.rhs match
case TypeBoundsTree(_, Hole(_, idx, args), _) =>
val quotedType = splices(idx).asInstanceOf[Seq[Any] => quoted.Type[?]](args)
PickledQuotes.quotedTypeToTree(quotedType)
case TypeBoundsTree(_, tpt, _) =>
tpt
(tdef.symbol, tree.tpe)
}.toMap
class ReplaceSplicedTyped extends TypeMap() {
override def apply(tp: Type): Type = {
val tp1 = tp match {
case tp: TypeRef =>
typeSpliceMap.get(tp.symbol) match
case Some(t) if tp.typeSymbol.hasAnnotation(defn.InternalQuoted_QuoteTypeTagAnnot) => t
case None => tp
case _ => tp
}
mapOver(tp1)
}
}
val expansion2 = new TreeTypeMap(new ReplaceSplicedTyped).transform(expr1)
quotePickling.println(i"**** typed quote\n${expansion2.show}")
expansion2
case _ =>
tree
}

// TASTY picklingtests/pos/quoteTest.scala

/** Pickle tree into it's TASTY bytes s*/
private def pickle(tree: Tree)(implicit ctx: Context): Array[Byte] = {
quotePickling.println(i"**** pickling quote of \n${tree.show}")
quotePickling.println(i"**** pickling quote of\n$tree")
val pickler = new TastyPickler(defn.RootClass)
val treePkl = pickler.treePkl
treePkl.pickle(tree :: Nil)
Expand All @@ -118,11 +181,17 @@ object PickledQuotes {
quotePickling.println(s"**** unpickling quote from TASTY\n${new TastyPrinter(bytes).printContents()}")

val mode = if (isType) UnpickleMode.TypeTree else UnpickleMode.Term
val unpickler = new QuoteUnpickler(bytes, splices, mode)
val unpickler = new DottyUnpickler(bytes, mode)
unpickler.enter(Set.empty)

val tree = unpickler.tree
quotePickling.println(i"**** unpickle quote ${tree.show}")

// Make sure trees and positions are fully loaded
new TreeTraverser {
def traverse(tree: Tree)(implicit ctx: Context): Unit = traverseChildren(tree)
}.traverse(tree)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What will happen if we remove the code above? Maybe add a doc for consequences if the action is not taken.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some symbols and positions are not loaded early enough. Not sure why though. But it has been like that since the start. Dimitry mentioned the need for this for loading trees from tasty in general and we have keep this mechanism so far.


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

Expand Down
27 changes: 0 additions & 27 deletions compiler/src/dotty/tools/dotc/core/quoted/QuoteUnpickler.scala

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ object DottyUnpickler {
class TreeSectionUnpickler(posUnpickler: Option[PositionUnpickler], commentUnpickler: Option[CommentUnpickler])
extends SectionUnpickler[TreeUnpickler](TreePickler.sectionName) {
def unpickle(reader: TastyReader, nameAtRef: NameTable): TreeUnpickler =
new TreeUnpickler(reader, nameAtRef, posUnpickler, commentUnpickler, Seq.empty)
new TreeUnpickler(reader, nameAtRef, posUnpickler, commentUnpickler)
}

class PositionsSectionUnpickler extends SectionUnpickler[PositionUnpickler]("Positions") {
Expand Down
4 changes: 3 additions & 1 deletion compiler/src/dotty/tools/dotc/core/tasty/TreePickler.scala
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@ object TreePickler {
override def isTerm: Boolean = isTermHole
override def isType: Boolean = !isTermHole
override def fallbackToText(printer: Printer): Text =
s"[[$idx|" ~~ printer.toTextGlobal(args, ", ") ~~ "]]"
if isTermHole then s"{{{ $idx |" ~~ printer.toTextGlobal(tpe) ~~ "|" ~~ printer.toTextGlobal(args, ", ") ~~ "}}}"
else s"[[[ $idx |" ~~ printer.toTextGlobal(tpe) ~~ "|" ~~ printer.toTextGlobal(args, ", ") ~~ "]]]"
}
}

Expand Down Expand Up @@ -603,6 +604,7 @@ class TreePickler(pickler: TastyPickler) {
writeByte(HOLE)
withLength {
writeNat(idx)
pickleType(tree.tpe, richTypes = true)
args.foreach(pickleTree)
}
}
Expand Down
43 changes: 9 additions & 34 deletions compiler/src/dotty/tools/dotc/core/tasty/TreeUnpickler.scala
Original file line number Diff line number Diff line change
Expand Up @@ -47,13 +47,11 @@ import scala.annotation.internal.sharable
* @param reader the reader from which to unpickle
* @param posUnpicklerOpt the unpickler for positions, if it exists
* @param commentUnpicklerOpt the unpickler for comments, if it exists
* @param splices
*/
class TreeUnpickler(reader: TastyReader,
nameAtRef: NameRef => TermName,
posUnpicklerOpt: Option[PositionUnpickler],
commentUnpicklerOpt: Option[CommentUnpickler],
splices: Seq[Any]) {
commentUnpicklerOpt: Option[CommentUnpickler]) {
import TreeUnpickler._
import tpd._

Expand Down Expand Up @@ -1224,7 +1222,10 @@ class TreeUnpickler(reader: TastyReader,
val alias = if currentAddr == end then EmptyTree else readTpt()
TypeBoundsTree(lo, hi, alias)
case HOLE =>
readHole(end, isType = false)
val idx = readNat()
val tpe = readType()
val args = until(end)(readTerm())
TreePickler.Hole(true, idx, args).withType(tpe)
case _ =>
readPathTerm()
}
Expand Down Expand Up @@ -1257,7 +1258,10 @@ class TreeUnpickler(reader: TastyReader,
case HOLE =>
readByte()
val end = readEnd()
readHole(end, isType = true)
val idx = readNat()
val tpe = readType()
val args = until(end)(readTerm())
TreePickler.Hole(false, idx, args).withType(tpe)
case _ =>
if (isTypeTreeTag(nextByte)) readTerm()
else {
Expand Down Expand Up @@ -1299,35 +1303,6 @@ class TreeUnpickler(reader: TastyReader,
owner => new LazyReader(localReader, owner, ctx.mode, ctx.source, op)
}

def readHole(end: Addr, isType: Boolean)(implicit ctx: Context): Tree = {
val idx = readNat()
val args = until(end)(readTerm())
val splice = splices(idx)
def wrap(arg: Tree) =
if (arg.isTerm) (qctx: scala.quoted.QuoteContext) ?=> new TastyTreeExpr(arg, QuoteContext.scopeId)
else new TreeType(arg, QuoteContext.scopeId)
val reifiedArgs = args.map(wrap)
val filled = if (isType) {
val quotedType = splice.asInstanceOf[Seq[Any] => quoted.Type[?]](reifiedArgs)
PickledQuotes.quotedTypeToTree(quotedType)
}
else {
val splice1 = splice.asInstanceOf[Seq[Any] => scala.quoted.QuoteContext ?=> quoted.Expr[?]]
val quotedExpr = splice1(reifiedArgs)(using dotty.tools.dotc.quoted.QuoteContext())
PickledQuotes.quotedExprToTree(quotedExpr)
}
// We need to make sure a hole is created with the source file of the surrounding context, even if
// it filled with contents a different source file. Otherwise nodes containing holes might end
// up without a position. PositionPickler makes sure that holes always get spans assigned,
// so we can just return the filler tree with the new source and no span here.
if (filled.source == ctx.source) filled
else {
val filled1 = filled.cloneIn(ctx.source)
filled1.span = NoSpan
filled1
}
}

// ------ Setting positions ------------------------------------------------

/** Pickled span for `addr`. */
Expand Down
32 changes: 31 additions & 1 deletion compiler/src/dotty/tools/dotc/transform/ReifyQuotes.scala
Original file line number Diff line number Diff line change
Expand Up @@ -331,7 +331,37 @@ class ReifyQuotes extends MacroTransform {
*/
private def makeHole(isTermHole: Boolean, body: Tree, splices: List[Tree], tpe: Type)(implicit ctx: Context): Hole = {
val idx = embedded.addTree(body, NoSymbol)
Hole(isTermHole, idx, splices).withType(tpe).asInstanceOf[Hole]

/** Remove references to local types that will not be defined in this quote */
def getTypeHoleType(using ctx: Context) = new TypeMap() {
override def apply(tp: Type): Type = tp match
case tp: TypeRef if tp.typeSymbol.isSplice =>
apply(tp.dealias)
case tp @ TypeRef(pre, _) if pre == NoPrefix || pre.termSymbol.isLocal =>
val hiBound = tp.typeSymbol.info match
case info @ ClassInfo(_, _, classParents, _, _) => classParents.reduce(_ & _)
case info => info.hiBound
apply(hiBound)
case tp =>
mapOver(tp)
}

/** Remove references to local types that will not be defined in this quote */
def getTermHoleType(using ctx: Context) = new TypeMap() {
override def apply(tp: Type): Type = tp match
case tp @ TypeRef(NoPrefix, _) if capturers.contains(tp.symbol) =>
// reference to term with a type defined in outer quote
getTypeHoleType(tp)
case tp @ TermRef(NoPrefix, _) if capturers.contains(tp.symbol) =>
// widen term refs to terms defined in outer quote
apply(tp.widenTermRefExpr)
case tp =>
mapOver(tp)
}
Copy link
Contributor

@liufengyun liufengyun Apr 1, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will approximation of the hole type have consequences on member resolution in unpickling?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, the consequence is that when unpickling we use the same known bounds of the types to resolve members selection as was done in typed. This is the key to make sure they do not change.


val holeType = if isTermHole then getTermHoleType(tpe) else getTypeHoleType(tpe)

Hole(isTermHole, idx, splices).withType(holeType).asInstanceOf[Hole]
}

override def transform(tree: Tree)(implicit ctx: Context): Tree =
Expand Down
2 changes: 1 addition & 1 deletion tasty/src/dotty/tools/tasty/TastyFormat.scala
Original file line number Diff line number Diff line change
Expand Up @@ -249,7 +249,7 @@ Standard Section: "Comments" Comment*
object TastyFormat {

final val header: Array[Int] = Array(0x5C, 0xA1, 0xAB, 0x1F)
val MajorVersion: Int = 21
val MajorVersion: Int = 22
val MinorVersion: Int = 0

/** Tags used to serialize names, should update [[nameTagToString]] if a new constant is added */
Expand Down
15 changes: 15 additions & 0 deletions tests/pos-macros/i7110a/Macro_1.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import scala.quoted._

object Macros {

inline def m[R](sym: Symantics[R]) : R = ${ mImpl[R]('{sym}) }

def mImpl[R: Type](using qctx: QuoteContext)(sym: Expr[Symantics[R]]): Expr[R] = '{
$sym.Meth(42)
}
}

trait Symantics[R] {
def Meth(exp: Int): R
def Meth(): R
}
14 changes: 14 additions & 0 deletions tests/pos-macros/i7110a/Test_2.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import scala.quoted._
import Macros._

object Test {
def main(args: Array[String]): Unit = {

val sym = new Symantics[Int] {
def Meth(exp: Int): Int = exp
def Meth(): Int = 42
}

val test = m[Int](sym)
}
}
Loading