All Downloads are FREE. Search and download functionalities are using the official Maven repository.

dotty.tools.dotc.transform.ReifyQuotes.scala Maven / Gradle / Ivy

The newest version!
package dotty.tools.dotc
package transform

import core._
import Decorators._
import Flags._
import Types._
import Contexts._
import Symbols._
import Constants._
import ast.Trees._
import ast.{TreeTypeMap, untpd}
import util.Spans._
import util.SourcePosition
import tasty.TreePickler.Hole
import SymUtils._
import NameKinds._
import dotty.tools.dotc.ast.tpd
import typer.Implicits.SearchFailureType

import scala.collection.mutable
import dotty.tools.dotc.core.Annotations.Annotation
import dotty.tools.dotc.core.Names._
import dotty.tools.dotc.core.StdNames._
import dotty.tools.dotc.core.quoted._
import dotty.tools.dotc.transform.TreeMapWithStages._
import dotty.tools.dotc.typer.Inliner
import dotty.tools.dotc.util.SourcePosition

import scala.annotation.constructorOnly


/** Translates quoted terms and types to `unpickle` method calls.
 *
 *  Transforms top level quote
 *   ```
 *   '{ ...
 *      val x1 = ???
 *      val x2 = ???
 *      ...
 *      ${ ... '{ ... x1 ... x2 ...} ... }
 *      ...
 *    }
 *    ```
 *  to
 *    ```
 *     unpickle(
 *       [[ // PICKLED TASTY
 *         ...
 *         val x1 = ???
 *         val x2 = ???
 *         ...
 *         Hole(0 | x1, x2)
 *         ...
 *       ]],
 *       List(
 *         (args: Seq[Any]) => {
 *           val x1$1 = args(0).asInstanceOf[Expr[T]]
 *           val x2$1 = args(1).asInstanceOf[Expr[T]] // can be asInstanceOf[Type[T]]
 *           ...
 *           { ... '{ ... ${x1$1} ... ${x2$1} ...} ... }
 *         }
 *       )
 *     )
 *    ```
 *  and then performs the same transformation on `'{ ... ${x1$1} ... ${x2$1} ...}`.
 *
 */
class ReifyQuotes extends MacroTransform {
  import ReifyQuotes._
  import tpd._

  override def phaseName: String = ReifyQuotes.name

  override def allowsImplicitSearch: Boolean = true

  override def checkPostCondition(tree: Tree)(implicit ctx: Context): Unit = {
    tree match {
      case tree: RefTree if !ctx.inInlineMethod =>
        assert(!tree.symbol.isQuote)
        assert(!tree.symbol.isSplice)
      case _ : TypeDef =>
        assert(!tree.symbol.hasAnnotation(defn.InternalQuoted_QuoteTypeTagAnnot),
          s"${tree.symbol} should have been removed by PickledQuotes because it has a @quoteTypeTag")
      case _ =>
    }
  }

  override def run(implicit ctx: Context): Unit =
    if (ctx.compilationUnit.needsStaging) super.run(freshStagingContext)

  protected def newTransformer(implicit ctx: Context): Transformer = new Transformer {
    override def transform(tree: tpd.Tree)(implicit ctx: Context): tpd.Tree =
      new QuoteReifier(null, new mutable.HashMap[Symbol, Tree => Tree], new Embedded, ctx.owner)(ctx).transform(tree)
  }

  /** The main transformer class
   *  @param  outer      the next outer reifier, null is this is the topmost transformer
   *  @param  embedded   a list of embedded quotes (if in a splice) or splices (if in a quote)
   *  @param  owner      the owner in the destination lifted lambda
   *  @param  capturers  register a reference defined in a quote but used in another quote nested in a splice.
   *                     Returns a version of the reference that needs to be used in its place.
   *                     '{
   *                       val x = ???
   *                       ${ ... '{ ... x ... } ... }
   *                     }
   *                     Eta expanding the `x` in `${ ... '{ ... x ... } ... }` will return a `${x$1}` for which the `x$1`
   *                     be created by some outer reifier.
   *                     This transformation is only applied to definitions at staging level 1.
   *                     See `isCaptured`.
   */
  private class QuoteReifier(outer: QuoteReifier, capturers: mutable.HashMap[Symbol, Tree => Tree],
                             val embedded: Embedded, val owner: Symbol)(@constructorOnly ictx: Context) extends TreeMapWithStages(ictx) { self =>

    import StagingContext._

    /** A nested reifier for a quote (if `isQuote = true`) or a splice (if not) */
    def nested(isQuote: Boolean)(implicit ctx: Context): QuoteReifier = {
      val nestedEmbedded = if (level > 1 || (level == 1 && isQuote)) embedded else new Embedded
      new QuoteReifier(this, capturers, nestedEmbedded, ctx.owner)(ctx)
    }

    /** Assuming  contains types `${}, ..., ${}`, the expression
     *
     *      { @quoteTypeTag type  = ${}
     *        ...
     *        @quoteTypeTag type  = ${}
     *        
     *      }
     *
     *  references to `TypeI` in `expr` are rewired to point to the locally
     *  defined versions. As a side effect, prepend the expressions `tag1, ..., `tagN`
     *  as splices.
     */
    private def addTags(expr: Tree)(implicit ctx: Context): Tree = {

      def mkTagSymbolAndAssignType(spliced: TermRef): TypeDef = {
        val splicedTree = tpd.ref(spliced)
        val rhs = transform(splicedTree.select(tpnme.splice))
        val alias = ctx.typeAssigner.assignType(untpd.TypeBoundsTree(rhs, rhs), rhs, rhs)
        val local = ctx.newSymbol(
          owner = ctx.owner,
          name = UniqueName.fresh((splicedTree.symbol.name.toString + "$_").toTermName).toTypeName,
          flags = Synthetic,
          info = TypeAlias(splicedTree.tpe.select(tpnme.splice)),
          coord = spliced.termSymbol.coord).asType
        local.addAnnotation(Annotation(defn.InternalQuoted_QuoteTypeTagAnnot))
        ctx.typeAssigner.assignType(untpd.TypeDef(local.name, alias), local)
      }

      val tagDefCache = new mutable.LinkedHashMap[Symbol, TypeDef]()

      def typeTagMap = new TypeMap() {
        def apply(tp: Type): Type = tp match {
          case tp: TypeRef if tp.symbol.isSplice =>
            tp.prefix match {
              case prefix: TermRef =>
                val tagDef = tagDefCache.getOrElseUpdate(prefix.symbol, mkTagSymbolAndAssignType(prefix))
                tagDef.symbol.typeRef
            }
          case _ =>
            mapOver(tp)
        }
      }

      val tagedTree = new TreeTypeMap(typeMap = typeTagMap).apply(expr)

      if (tagDefCache.isEmpty) expr
      else Block(tagDefCache.valuesIterator.toList, tagedTree)
    }

    /** Split `body` into a core and a list of embedded splices.
     *  Then if inside a splice, make a hole from these parts.
     *  If outside a splice, generate a call tp `scala.quoted.Unpickler.unpickleType` or
     *  `scala.quoted.Unpickler.unpickleExpr` that matches `tpe` with
     *  core and splices as arguments.
     */
    override protected def transformQuotation(body: Tree, quote: Tree)(implicit ctx: Context): Tree = {
      val isType = quote.symbol eq defn.InternalQuoted_typeQuote
      assert(!(body.symbol.isSplice && (body.isInstanceOf[GenericApply[_]] || body.isInstanceOf[Select])))
      if (level > 0) {
        val body1 = nested(isQuote = true).transform(body)(quoteContext)
        super.transformQuotation(body1, quote)
      }
      else body match {
        case body: RefTree if isCaptured(body.symbol, level + 1) =>
          // Optimization: avoid the full conversion when capturing `x`
          // in '{ x } to '{ ${x$1} } and go directly to `x$1`
          capturers(body.symbol)(body)
        case _=>
          val (body1, splices) = nested(isQuote = true).splitQuote(body)(quoteContext)
          if (level == 0) {
            val body2 =
              if (body1.isType) body1
              else Inlined(Inliner.inlineCallTrace(ctx.owner, quote.sourcePos), Nil, body1)
            pickledQuote(body2, splices, body.tpe, isType).withSpan(quote.span)
          }
          else {
            body
          }
      }
    }

    private def pickledQuote(body: Tree, splices: List[Tree], originalTp: Type, isType: Boolean)(implicit ctx: Context) = {

      def liftedValue[T](value: T, name: TermName, qctx: Tree) =
        ref(defn.LiftableModule).select(name).select("toExpr".toTermName).appliedTo(Literal(Constant(value))).appliedTo(qctx)

      def pickleAsValue[T](value: T) = {
        val qctx = ctx.typer.inferImplicitArg(defn.QuoteContextType, body.span)
        if (qctx.tpe.isInstanceOf[SearchFailureType])
          ctx.error(ctx.typer.missingArgMsg(qctx, defn.QuoteContextType, ""), ctx.source.atSpan(body.span))
        value match {
          case null => ref(defn.QuotedExprModule).select("nullExpr".toTermName).appliedTo(qctx)
          case _: Unit => ref(defn.QuotedExprModule).select("unitExpr".toTermName).appliedTo(qctx)
          case _: Boolean => liftedValue(value, "Liftable_Boolean_delegate".toTermName, qctx)
          case _: Byte => liftedValue(value, "Liftable_Byte_delegate".toTermName, qctx)
          case _: Short => liftedValue(value, "Liftable_Short_delegate".toTermName, qctx)
          case _: Int => liftedValue(value, "Liftable_Int_delegate".toTermName, qctx)
          case _: Long => liftedValue(value, "Liftable_Long_delegate".toTermName, qctx)
          case _: Float => liftedValue(value, "Liftable_Float_delegate".toTermName, qctx)
          case _: Double => liftedValue(value, "Liftable_Double_delegate".toTermName, qctx)
          case _: Char => liftedValue(value, "Liftable_Char_delegate".toTermName, qctx)
          case _: String => liftedValue(value, "Liftable_String_delegate".toTermName, qctx)
        }
      }

      def pickleAsTasty() = {
        val meth =
          if (isType) ref(defn.Unpickler_unpickleType).appliedToType(originalTp)
          else ref(defn.Unpickler_unpickleExpr).appliedToType(originalTp.widen)
        val spliceResType =
          if(isType) defn.QuotedTypeType.appliedTo(WildcardType)
          else defn.QuotedExprType.appliedTo(defn.AnyType) | defn.QuotedTypeType.appliedTo(WildcardType)
        meth.appliedTo(
          liftList(PickledQuotes.pickleQuote(body).map(x => Literal(Constant(x))), defn.StringType),
          liftList(splices, defn.FunctionType(1).appliedTo(defn.SeqType.appliedTo(defn.AnyType), spliceResType)))
      }
      if (splices.nonEmpty) pickleAsTasty()
      else if (isType) {
        def tag(tagName: String) = ref(defn.QuotedTypeModule).select(tagName.toTermName)
        if (body.symbol.isPrimitiveValueClass) tag(s"${body.symbol.name}Tag")
        else pickleAsTasty()
      }
      else toValue(body) match {
        case Some(value) => pickleAsValue(value)
        case _ => pickleAsTasty()
      }
    }

    /** If inside a quote, split the body of the splice into a core and a list of embedded quotes
     *  and make a hole from these parts. Otherwise issue an error, unless we
     *  are in the body of an inline method.
     */
    protected def transformSplice(body: Tree, splice: Tree)(implicit ctx: Context): Tree = {
      if (level > 1) {
        val body1 = nested(isQuote = false).transform(body)(spliceContext)
        splice match {
          case splice: Apply => cpy.Apply(splice)(splice.fun, body1 :: Nil)
          case splice: Select => cpy.Select(splice)(body1, splice.name)
        }
      }
      else {
        assert(level == 1, "unexpected top splice outside quote")
        val (body1, quotes) = nested(isQuote = false).splitSplice(body)(spliceContext)
        val tpe = outer.embedded.getHoleType(body, splice)
        val hole = makeHole(body1, quotes, tpe).withSpan(splice.span)
        // We do not place add the inline marker for trees that where lifted as they come from the same file as their
        // enclosing quote. Any intemediate splice will add it's own Inlined node and cancel it before splicig the lifted tree.
        // Note that lifted trees are not necessarily expressions and that Inlined nodes are expected to be expressions.
        // For example we can have a lifted tree containing the LHS of an assignment (see tests/run-with-compiler/quote-var.scala).
        if (splice.isType || outer.embedded.isLiftedSymbol(body.symbol)) hole
        else Inlined(EmptyTree, Nil, hole).withSpan(splice.span)
      }
    }

    /** Transforms the contents of a nested splice
     *  Assuming
     *     '{
     *        val x = ???
     *        val y = ???
     *        ${ ... '{ ... x .. y ... } ... }
     *      }
     *  then the spliced subexpression
     *     { ... '{ ... x ... y ... } ... }
     *  will be transformed to
     *     (args: Seq[Any]) => {
     *       val x$1 = args(0).asInstanceOf[Expr[Any]] // or .asInstanceOf[Type[Any]]
     *       val y$1 = args(1).asInstanceOf[Expr[Any]] // or .asInstanceOf[Type[Any]]
     *       { ... '{ ... ${x$1} ... ${y$1} ... } ... }
     *     }
     *
     *  See: `capture`
     *
     *  At the same time register embedded trees `x` and `y` to place as arguments of the hole
     *  placed in the original code.
     *     '{
     *        val x = ???
     *        val y = ???
     *        Hole(0 | x, y)
     *      }
     */
    private def makeLambda(tree: Tree)(implicit ctx: Context): Tree = {
      def body(arg: Tree)(implicit ctx: Context): Tree = {
        var i = 0
        transformWithCapturer(tree)(
          (captured: mutable.Map[Symbol, Tree]) => {
            (tree: Tree) => {
              def newCapture = {
                val tpw = tree.tpe.widen match {
                  case tpw: MethodicType => tpw.toFunctionType()
                  case tpw => tpw
                }
                assert(tpw.isInstanceOf[ValueType])
                val argTpe =
                  if (tree.isType) defn.QuotedTypeType.appliedTo(tpw)
                  else defn.QuotedExprType.appliedTo(tpw)
                val selectArg = arg.select(nme.apply).appliedTo(Literal(Constant(i))).cast(argTpe)
                val capturedArg = SyntheticValDef(UniqueName.fresh(tree.symbol.name.toTermName).toTermName, selectArg)
                i += 1
                embedded.addTree(tree, capturedArg.symbol)
                captured.put(tree.symbol, capturedArg)
                capturedArg
              }
              val refSym = captured.getOrElseUpdate(tree.symbol, newCapture).symbol
              ref(refSym).withSpan(tree.span)
            }
          }
        )
      }
      /* Lambdas are generated outside the quote that is beeing reified (i.e. in outer.owner).
       * In case the case that level == -1 the code is not in a quote, it is in an inline method,
       * hence we should take that as owner directly.
       */
      val lambdaOwner = if (level == -1) ctx.owner else outer.owner

      val tpe = MethodType(defn.SeqType.appliedTo(defn.AnyType) :: Nil, tree.tpe.widen)
      val meth = ctx.newSymbol(lambdaOwner, UniqueName.fresh(nme.ANON_FUN), Synthetic | Method, tpe)
      val closure = Closure(meth, tss => body(tss.head.head)(ctx.withOwner(meth)).changeOwner(ctx.owner, meth)).withSpan(tree.span)

      enclosingInlineds match {
        case enclosingInline :: _ =>
         // In case a tree was inlined inside of the quote and we this closure corresponds to code within it we need to keep the inlined node.
         Inlined(enclosingInline, Nil, closure)(ctx.withSource(lambdaOwner.topLevelClass.source))
        case Nil => closure
      }
    }

    private def transformWithCapturer(tree: Tree)(capturer: mutable.Map[Symbol, Tree] => Tree => Tree)(implicit ctx: Context): Tree = {
      val captured = mutable.LinkedHashMap.empty[Symbol, Tree]
      val captured2 = capturer(captured)

      outer.localSymbols.foreach(sym => if (!sym.isInlineMethod) capturers.put(sym, captured2))

      val tree2 = transform(tree)
      capturers --= outer.localSymbols

      seq(captured.result().valuesIterator.toList, tree2)
    }

    /** Returns true if this tree will be captured by `makeLambda`. Checks phase consistency and presence of capturer. */
    private def isCaptured(sym: Symbol, level: Int)(implicit ctx: Context): Boolean =
      level == 1 && levelOf(sym).contains(1) && capturers.contains(sym)

    /** Transform `tree` and return the resulting tree and all `embedded` quotes
     *  or splices as a pair, after performing the `addTags` transform.
     */
    private def splitQuote(tree: Tree)(implicit ctx: Context): (Tree, List[Tree]) = {
      val tree1 = addTags(transform(tree))
      (tree1, embedded.getTrees)
    }

    private def splitSplice(tree: Tree)(implicit ctx: Context): (Tree, List[Tree]) = {
      val tree1 = makeLambda(tree)
      (tree1, embedded.getTrees)
    }

    /** Register `body` as an `embedded` quote or splice
     *  and return a hole with `splices` as arguments and the given type `tpe`.
     */
    private def makeHole(body: Tree, splices: List[Tree], tpe: Type)(implicit ctx: Context): Hole = {
      val idx = embedded.addTree(body, NoSymbol)
      Hole(idx, splices).withType(tpe).asInstanceOf[Hole]
    }

    override def transform(tree: Tree)(implicit ctx: Context): Tree = {
      if (tree.source != ctx.source && tree.source.exists)
        transform(tree)(ctx.withSource(tree.source))
      else reporting.trace(i"Reifier.transform $tree at $level", show = true) {
        tree match {
          case tree: RefTree if isCaptured(tree.symbol, level) =>
            val body = capturers(tree.symbol).apply(tree)
            val splice: Tree =
              if (tree.isType) body.select(tpnme.splice)
              else ref(defn.InternalQuoted_exprSplice).appliedToType(tree.tpe).appliedTo(body)

            transformSplice(body, splice)

          case tree: DefDef if tree.symbol.is(Macro) && level == 0 =>
            // Shrink size of the tree. The methods have already been inlined.
            // TODO move to FirstTransform to trigger even without quotes
            cpy.DefDef(tree)(rhs = defaultValue(tree.rhs.tpe))

          case _ =>
            super.transform(tree)
        }
      }
    }

    private def liftList(list: List[Tree], tpe: Type)(implicit ctx: Context): Tree = {
      list.foldRight[Tree](ref(defn.NilModule)) { (x, acc) =>
        acc.select("::".toTermName).appliedToType(tpe).appliedTo(x)
      }
    }
  }
}


object ReifyQuotes {
  import tpd._

  val name: String = "reifyQuotes"

  def toValue(tree: tpd.Tree): Option[Any] = tree match {
    case Literal(Constant(c)) => Some(c)
    case Block(Nil, e) => toValue(e)
    case Inlined(_, Nil, e) => toValue(e)
    case _ => None
  }

  class Embedded(trees: mutable.ListBuffer[tpd.Tree] = mutable.ListBuffer.empty, map: mutable.Map[Symbol, tpd.Tree] = mutable.Map.empty) {
    /** Adds the tree and returns it's index */
    def addTree(tree: tpd.Tree, liftedSym: Symbol): Int = {
      trees += tree
      if (liftedSym ne NoSymbol)
        map.put(liftedSym, tree)
      trees.length - 1
    }

    /** Type used for the hole that will replace this splice */
    def getHoleType(body: tpd.Tree, splice: tpd.Tree)(implicit ctx: Context): Type = {
      // For most expressions the splice.tpe but there are some types that are lost by lifting
      // that can be recoverd from the original tree. Currently the cases are:
      //  * Method types: the splice represents a method reference
      map.get(body.symbol).map(_.tpe.widen).getOrElse(splice.tpe)
    }

    def isLiftedSymbol(sym: Symbol)(implicit ctx: Context): Boolean = map.contains(sym)

    /** Get the list of embedded trees */
    def getTrees: List[tpd.Tree] = trees.toList

    override def toString: String = s"Embedded($trees, $map)"

  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy