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

scala.meta.internal.quasiquotes.ReificationMacros.scala Maven / Gradle / Ivy

package scala.meta
package internal
package quasiquotes

import scala.{Seq => _}
import scala.collection.immutable.Seq
import scala.runtime.ScalaRunTime
import scala.language.implicitConversions
import scala.language.experimental.macros
import scala.reflect.macros.whitebox.Context
import scala.collection.{immutable, mutable}
import org.scalameta.data._
import org.scalameta.adt.{Liftables => AdtLiftables, Reflection => AdtReflection}
import scala.meta.internal.ast.{Liftables => AstLiftables, Reflection => AstReflection}
import org.scalameta._
import org.scalameta.invariants._
import scala.meta.dialects // no underscore import
import scala.meta.classifiers._
import scala.meta.parsers._
import scala.meta.tokenizers._
import scala.meta.prettyprinters._
import scala.meta.internal.dialects.InstantiateDialect
import scala.meta.internal.{semantic => s}
import scala.meta.internal.ast.Quasi
import scala.meta.internal.ast.Helpers._
import scala.meta.internal.parsers.Absolutize._
import scala.meta.internal.tokens._
import scala.compat.Platform.EOL

// TODO: ideally, we would like to bootstrap these macros on top of scala.meta
// so that quasiquotes can be interpreted by any host, not just scalac
class ReificationMacros(val c: Context) extends AstReflection with AdtLiftables with AstLiftables with InstantiateDialect {
  lazy val u: c.universe.type = c.universe
  lazy val mirror: u.Mirror = c.mirror
  import c.internal._
  import decorators._
  import c.universe.{Tree => _, Symbol => _, Type => _, Position => _, _}
  import c.universe.{Tree => ReflectTree, Symbol => ReflectSymbol, Type => ReflectType, Position => ReflectPosition}
  import scala.meta.{Tree => MetaTree, Type => MetaType, Dialect => Dialect}
  import scala.meta.inputs.{Position => MetaPosition, _}
  type MetaParser = (Input, Dialect) => MetaTree
  val XtensionQuasiquoteTerm = "shadow scala.meta quasiquotes"
  val XtensionParsersDialectApply = "shadow extension method conflict"

  // NOTE: only Mode.Pattern really needs holes, and that's only because of Scala's limitations
  // read a comment in liftUnquote for more information on that
  case class Hole(name: TermName, arg: ReflectTree, var reifier: ReflectTree)
  sealed trait Mode {
    def isTerm: Boolean = this.isInstanceOf[Mode.Term]
    def isPattern: Boolean = this.isInstanceOf[Mode.Pattern]
    def multiline: Boolean
    def holes: List[Hole]
  }
  object Mode {
    case class Term(multiline: Boolean, holes: List[Hole]) extends Mode
    case class Pattern(multiline: Boolean, holes: List[Hole], unapplySelector: ReflectTree) extends Mode
  }

  import definitions._
  val SeqModule = c.mirror.staticModule("scala.collection.Seq")
  val ScalaPackageObjectClass = c.mirror.staticModule("scala.package")
  val ScalaList = ScalaPackageObjectClass.info.decl(TermName("List"))
  val ScalaNil = ScalaPackageObjectClass.info.decl(TermName("Nil"))
  val ScalaSeq = ScalaPackageObjectClass.info.decl(TermName("Seq"))
  val ImmutableSeq = mirror.staticClass("scala.collection.immutable.Seq")
  val InternalLift = c.mirror.staticModule("scala.meta.internal.quasiquotes.Lift")
  val InternalUnlift = c.mirror.staticModule("scala.meta.internal.quasiquotes.Unlift")
  val QuasiquotePrefix = c.freshName("quasiquote")

  def apply(args: ReflectTree*)(dialect: ReflectTree): ReflectTree = expand(dialect)
  def unapply(scrutinee: ReflectTree)(dialect: ReflectTree): ReflectTree = expand(dialect)
  def expand(dialectTree: ReflectTree): ReflectTree = {
    val (input, mode) = extractQuasiquotee()
    val dialect = instantiateDialect(dialectTree, mode)
    val parser = instantiateParser(c.macroApplication.symbol)
    val skeleton = parseSkeleton(parser, input, dialect)
    val hygienicSkeleton = hygienifySkeleton(skeleton)
    reifySkeleton(hygienicSkeleton, mode)
  }

  private def extractQuasiquotee(): (Input, Mode) = {
    val reflectInput = c.macroApplication.pos.source
    val (parts, mode) = {
      def isMultiline(firstPart: ReflectTree) = {
        reflectInput.content(firstPart.pos.start - 2) == '"'
      }
      def mkHole(argi: (ReflectTree, Int)) = {
        val (arg, i) = argi
        val name = TermName(QuasiquotePrefix + "$hole$" + i)
        Hole(name, arg, reifier = EmptyTree) // TODO: make reifier immutable somehow
      }
      try {
        c.macroApplication match {
          case q"$_($_.apply(..$parts)).$_.apply[..$_](..$args)($_)" =>
            require(parts.length == args.length + 1)
            val holes = args.zipWithIndex.map(mkHole)
            (parts, Mode.Term(isMultiline(parts.head), holes))
          case q"$_($_.apply(..$parts)).$_.unapply[..$_](${unapplySelector: Ident})($_)" =>
            require(unapplySelector.name == TermName(""))
            val args = c.internal.subpatterns(unapplySelector).get
            require(parts.length == args.length + 1)
            val holes = args.zipWithIndex.map(mkHole)
            (parts, Mode.Pattern(isMultiline(parts.head), holes, unapplySelector))
        }
      } catch {
        case ex: Exception =>
          c.abort(c.macroApplication.pos, s"fatal error initializing quasiquote macro: ${showRaw(c.macroApplication)}")
      }
    }
    val metaInput = {
      val (firstPart, lastPart @ Literal(Constant(s_lastpart: String))) = (parts.head, parts.last)
      val start = firstPart.pos.start // looks like we can trust this position to point to the character right after the opening quotes
      val end = { // we have to infer this for ourselves, because there's no guarantee we run under -Yrangepos
        var remaining = s_lastpart.length
        var curr = lastPart.pos.start - 1
        while (remaining > 0) {
          curr += 1
          if (reflectInput.content(curr) == '$') {
            curr += 1
            require(reflectInput.content(curr) == '$')
          }
          remaining -= 1
        }
        curr + 1
      }
      val result = {
        if (reflectInput.file.file != null) Input.File(reflectInput.file.file)
        else Input.String(new String(reflectInput.content)) // NOTE: can happen in REPL or in custom Global
      }
      Input.Slice(result, start, end)
    }
    (metaInput, mode)
  }

  private implicit def metaPositionToReflectPosition(pos: MetaPosition): ReflectPosition = {
    // TODO: this is another instance of #383
    c.macroApplication.pos.focus.withPoint(pos.start.absolutize.offset)
  }

  private def instantiateDialect(dialectTree: ReflectTree, mode: Mode): Dialect = {
    val underlyingDialect = instantiateDialect(dialectTree)
    if (mode.isTerm) dialects.QuasiquoteTerm(underlyingDialect, mode.multiline)
    else dialects.QuasiquotePat(underlyingDialect, mode.multiline)
  }

  private def instantiateParser(interpolator: ReflectSymbol): MetaParser = {
    val parserModule = interpolator.owner.owner.companion
    val parsersModuleClass = Class.forName("scala.meta.quasiquotes.package$", true, this.getClass.getClassLoader)
    val parsersModule = parsersModuleClass.getField("MODULE$").get(null)
    val parserModuleGetter = parsersModule.getClass.getDeclaredMethod(parserModule.name.toString)
    val parserModuleInstance = parserModuleGetter.invoke(parsersModule)
    val parserMethod = parserModuleInstance.getClass.getDeclaredMethods().find(_.getName == "parse").head
    (input: Input, dialect: Dialect) => {
      try parserMethod.invoke(parserModuleInstance, input, dialect).asInstanceOf[MetaTree]
      catch { case ex: java.lang.reflect.InvocationTargetException => throw ex.getTargetException }
    }
  }

  private def parseSkeleton(parser: MetaParser, input: Input, dialect: Dialect): MetaTree = {
    try {
      parser(input, dialect)
    } catch {
      case TokenizeException(pos, message) => c.abort(pos, message)
      case ParseException(pos, message) => c.abort(pos, message)
    }
  }

  private def hygienifySkeleton(meta: MetaTree): MetaTree = {
    // TODO: Implement this (https://github.com/scalameta/scalameta/issues/156)
    // by setting Tree.env of appropriate trees (names, apply-like nodes) to appropriate values.
    // So far, we don't have to set anything to anything,
    // because Environment.None (the only possible value for environments at the moment) is the default.
    meta
  }

  private def reifySkeleton(meta: MetaTree, mode: Mode): ReflectTree = {
    val pendingQuasis = mutable.Stack[Quasi]()
    implicit class XtensionRankedClazz(clazz: Class[_]) {
      def unwrap: Class[_] = {
        if (clazz.isArray) clazz.getComponentType.unwrap
        else clazz
      }
      def wrap(rank: Int): Class[_] = {
        if (rank == 0) clazz
        else ScalaRunTime.arrayClass(clazz).wrap(rank - 1)
      }
      def toTpe: u.Type = {
        if (clazz.isArray) {
          appliedType(ImmutableSeq, clazz.getComponentType.toTpe)
        } else {
          def loop(owner: u.Symbol, parts: List[String]): u.Symbol = parts match {
            case part :: Nil =>
              if (clazz.getName.endsWith("$")) owner.info.decl(TermName(part))
              else owner.info.decl(TypeName(part))
            case part :: rest =>
              loop(owner.info.decl(TermName(part)), rest)
            case Nil =>
              unreachable(debug(clazz))
          }
          val name = scala.reflect.NameTransformer.decode(clazz.getName)
          val result = loop(mirror.RootPackage, name.stripSuffix("$").split(Array('.', '$')).toList)
          if (result.isModule) result.asModule.info else result.asClass.toType
        }
      }
    }
    implicit class XtensionRankedTree(tree: u.Tree) {
      def wrap(rank: Int): u.Tree = {
        if (rank == 0) tree
        else AppliedTypeTree(tq"$ImmutableSeq", List(tree.wrap(rank - 1)))
      }
    }
    implicit class XtensionQuasiHole(quasi: Quasi) {
      def hole: Hole = {
        val pos = quasi.pos.absolutize
        val maybeHole = mode.holes.find(h => pos.start.offset <= h.arg.pos.point && h.arg.pos.point <= pos.end.offset)
        maybeHole.getOrElse(unreachable(debug(quasi, quasi.pos.absolutize, mode.holes, mode.holes.map(_.arg.pos))))
      }
    }
    object Lifts {
      def liftTree(tree: MetaTree): ReflectTree = {
        Liftables.liftableSubTree(tree)
      }
      def liftOptionTree(maybeTree: Option[MetaTree]): ReflectTree = {
        maybeTree match {
          case Some(tree: Quasi) => liftQuasi(tree, optional = true)
          case Some(otherTree) => q"_root_.scala.Some(${liftTree(otherTree)})"
          case None => q"_root_.scala.None"
        }
      }
      def liftTrees(trees: Seq[MetaTree]): ReflectTree = {
        def loop(trees: List[MetaTree], acc: ReflectTree, prefix: List[MetaTree]): ReflectTree = trees match {
          // TODO: instead of checking against 1, we should also take pendingQuasis into account
          case (quasi: Quasi) +: rest if quasi.rank == 1 =>
            if (acc.isEmpty) {
              if (prefix.isEmpty) loop(rest, liftQuasi(quasi), Nil)
              else loop(rest, prefix.foldRight(acc)((curr, acc) => {
                // NOTE: We cannot do just q"${liftTree(curr)} +: ${liftQuasi(quasi)}"
                // because that creates a synthetic temp variable that doesn't make any sense in a pattern.
                // Neither can we do q"${liftQuasi(quasi)}.+:(${liftTree(curr)})",
                // because that still wouldn't work in pattern mode.
                // Finally, we can't do something like q"+:(${liftQuasi(quasi)}, (${liftTree(curr)}))",
                // because would violate evaluation order guarantees that we must keep.
                if (mode.isTerm) q"${liftTree(curr)} +: ${liftQuasi(quasi)}"
                else pq"${liftTree(curr)} +: ${liftQuasi(quasi)}"
              }), Nil)
            } else {
              require(prefix.isEmpty && debug(trees, acc, prefix))
              if (mode.isTerm) loop(rest, q"$acc ++ ${liftQuasi(quasi)}", Nil)
              else {
                val hint = {
                  "Note that you can extract a sequence into an unquote when pattern matching," + EOL+
                  "it just cannot follow another sequence either directly or indirectly."
                }
                val errorMessage = s"rank mismatch when unquoting;$EOL found   : ${"." * (quasi.rank + 1)}$EOL required: no dots$EOL$hint"
                c.abort(quasi.pos, errorMessage)
              }
            }
          case other +: rest =>
            if (acc.isEmpty) loop(rest, acc, prefix :+ other)
            else {
              require(prefix.isEmpty && debug(trees, acc, prefix))
              if (mode.isTerm) loop(rest, q"$acc :+ ${liftTree(other)}", Nil)
              else loop(rest, pq"$acc :+ ${liftTree(other)}", Nil)
            }
          case Nil =>
            // NOTE: Luckily, at least this quasiquote works fine both in term and pattern modes
            if (acc.isEmpty) q"_root_.scala.collection.immutable.Seq(..${prefix.map(liftTree)})"
            else acc
        }
        loop(trees.toList, EmptyTree, Nil)
      }
      def liftOptionTrees(maybeTrees: Option[Seq[MetaTree]]): ReflectTree = {
        maybeTrees match {
          case Some(Seq(quasi: Quasi)) if quasi.rank == 0 =>
            q"_root_.scala.Some(${liftTrees(Seq(quasi))})"
          case Some(Seq(quasi: Quasi)) if quasi.rank > 0 =>
            liftQuasi(quasi, optional = true)
          case Some(otherTrees) =>
            q"_root_.scala.Some(${liftTrees(otherTrees)})"
          case None =>
            q"_root_.scala.None"
        }
      }
      def liftTreess(treess: Seq[Seq[MetaTree]]): ReflectTree = {
        // TODO: implement support for ... mixed with .., $ and normal code
        treess match {
          case Seq(Seq(quasi: Quasi)) if quasi.rank == 2 =>
            liftQuasi(quasi)
          case _ =>
            Liftable.liftList[Seq[MetaTree]](Liftables.liftableSubTrees).apply(treess.toList)
        }
      }
      def liftQuasi(quasi: Quasi, optional: Boolean = false): ReflectTree = {
        try {
          pendingQuasis.push(quasi)
          if (quasi.rank == 0) {
            var inferredPt = quasi.pt.wrap(pendingQuasis.map(_.rank).sum).toTpe
            if (optional) inferredPt = appliedType(typeOf[Option[_]], inferredPt)
            val lifted = mode match {
              case Mode.Term(_, _) =>
                q"$InternalLift[$inferredPt](${quasi.hole.arg})"
              case Mode.Pattern(_, _, _) =>
                // TODO: Here, we would like to say q"$InternalUnlift[$inferredPt](${quasi.hole.arg})".
                // Unfortunately, the way how patterns work prevents us from having it this easy:
                // 1) unapplications can't have explicitly specified type arguments
                // 2) pattern language is very limited and can't express what we want to express in Unlift
                // Therefore, we're forced to take a two-step unquoting scheme: a) match everything in the corresponding hole,
                // b) call Unlift.unapply as a normal method in the right-hand side part of the pattern matching clause.
                val hole = quasi.hole
                val inferredPt = hole.arg match {
                  case pq"_: $pt" => pt
                  case pq"$_: $pt" => pt
                  case _ =>
                    var inferredPt = tq"_root_.scala.meta.Tree".wrap(pendingQuasis.map(_.rank).sum)
                    if (optional) inferredPt = tq"_root_.scala.Option[$inferredPt]"
                    inferredPt
                }
                hole.reifier = atPos(quasi.pos)(q"$InternalUnlift.unapply[$inferredPt](${hole.name})") // TODO: make `reifier`a immutable somehow
                pq"${hole.name}"
            }
            atPos(quasi.pos)(lifted)
          } else {
            quasi.tree match {
              case quasi: Quasi if quasi.rank == 0 =>
                // NOTE: Option[Seq[T]] exists only in one instance in our tree API: Template.stats.
                // Unfortunately, there is a semantic difference between an empty template and a template
                // without any stats, so we can't just replace it with Seq[T] and rely on tokens
                // (because tokens may be lost, e.g. a TASTY roundtrip).
                //
                // Strictly speaking, we shouldn't be allowing ..$stats in Template.stats,
                // because the pt is Option[Seq[T]], not a Seq[T], but given the fact that Template.stats
                // is very common, I'm willing to bend the rules here and flatten Option[Seq[T]],
                // capturing 0 trees if we have None and working as usual if we have Some.
                //
                // This is definitely loss of information, but if someone wants to discern Some and None,
                // they are welcome to write $stats, which will correctly capture an Option.
                // TODO: Well, they aren't welcome, because $stats will insert or capture just one stat.
                // This is the same problem as with `q"new $x"`, where x is ambiguous between a template and a ctorcall.
                if (optional) {
                  mode match {
                    case Mode.Term(_, _) =>
                      q"_root_.scala.Some(${liftQuasi(quasi)})"
                    case Mode.Pattern(_, holes, _) =>
                      val reified = liftQuasi(quasi)
                      val hole = holes.find(hole => reified.equalsStructure(pq"${hole.name}")).get
                      require(hole.reifier.nonEmpty)
                      val flattened = q"_root_.scala.meta.internal.quasiquotes.Flatten.unapply(${hole.name})"
                      val unlifted = (new Transformer {
                        override def transform(tree: ReflectTree): ReflectTree = tree match {
                          case Ident(name) if name == hole.name => Ident(TermName("tree"))
                          case tree => super.transform(tree)
                        }
                      }).transform(hole.reifier)
                      hole.reifier = atPos(quasi.pos)(q"$flattened.flatMap(tree => $unlifted)")
                      reified
                  }
                } else {
                  liftQuasi(quasi)
                }
              case _ =>
                c.abort(quasi.pos, "complex ellipses are not supported yet")
            }
          }
        } finally {
          pendingQuasis.pop()
        }
      }
    }
    object Liftables extends s.DenotationLiftables
                        with s.TypingLiftables
                        with s.EnvironmentLiftables {
      // NOTE: we could write just `implicitly[Liftable[MetaTree]].apply(meta)`
      // but that would bloat the code significantly with duplicated instances for denotations and sigmas
      override lazy val u: c.universe.type = c.universe
      implicit def liftableSubTree[T <: MetaTree]: Liftable[T] = Liftable((tree: T) => materializeAst[MetaTree].apply(tree))
      implicit def liftableSubTrees[T <:MetaTree]: Liftable[Seq[T]] = Liftable((trees: Seq[T]) => Lifts.liftTrees(trees))
      implicit def liftableSubTreess[T <: MetaTree]: Liftable[Seq[Seq[T]]] = Liftable((treess: Seq[Seq[T]]) => Lifts.liftTreess(treess))
      implicit def liftableOptionSubTree[T <: MetaTree]: Liftable[Option[T]] = Liftable((x: Option[T]) => Lifts.liftOptionTree(x))
      implicit def liftableOptionSubTrees[T <: MetaTree]: Liftable[Option[Seq[T]]] = Liftable((x: Option[Seq[T]]) => Lifts.liftOptionTrees(x))
    }
    mode match {
      case Mode.Term(_, _) =>
        val internalResult = Lifts.liftTree(meta)
        if (sys.props("quasiquote.debug") != null) {
          println(internalResult)
          // println(showRaw(internalResult))
        }
        q"$InternalUnlift[${c.macroApplication.tpe}]($internalResult)"
      case Mode.Pattern(_, holes, unapplySelector) =>
        // inspired by https://github.com/densh/joyquote/blob/master/src/main/scala/JoyQuote.scala
        val pattern = Lifts.liftTree(meta)
        val (thenp, elsep) = {
          if (holes.isEmpty) (q"true", q"false")
          else {
            val resultNames = holes.zipWithIndex.map({ case (_, i) => TermName(QuasiquotePrefix + "$result$" + i) })
            val resultPatterns = resultNames.map(name => pq"_root_.scala.Some($name)")
            val resultTerms = resultNames.map(name => q"$name")
            val thenp = q"""
              (..${holes.map(_.reifier)}) match {
                case (..$resultPatterns) => _root_.scala.Some((..$resultTerms))
                case _ => _root_.scala.None
              }
            """
            (thenp, q"_root_.scala.None")
          }
        }
        val matchp = pattern match {
          case Bind(_, Ident(termNames.WILDCARD)) => q"input match { case $pattern => $thenp }"
          case _ => q"input match { case $pattern => $thenp; case _ => $elsep }"
        }
        val internalResult = q"new { def unapply(input: _root_.scala.meta.Tree) = $matchp }.unapply($unapplySelector)"
        if (sys.props("quasiquote.debug") != null) {
          println(internalResult)
          // println(showRaw(internalResult))
        }
        internalResult // TODO: q"InternalLift[${unapplySelector.tpe}]($internalResult)"
    }
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy