TASTy Reflect

TASTy Reflect enables inspection and construction of Typed Abstract Syntax Trees (Typed-AST). It may be used on quoted expressions (quoted.Expr) and quoted types (quoted.Type) from Macros or on full TASTy files.

If you are writing macros, please first read Macros. You may find all you need without using TASTy Reflect.

API: From quotes and splices to TASTy reflect trees and back

With quoted.Expr and quoted.Type we can compute code but also analyze code by inspecting the ASTs. Macros provides the guarantee that the generation of code will be type-correct. Using TASTy Reflect will break these guarantees and may fail at macro expansion time, hence additional explicit checks must be done.

To provide reflection capabilities in macros we need to add an implicit parameter of type scala.quoted.QuoteContext and import tasty._ from it in the scope where it is used.

import scala.quoted._

inline def natConst(x: => Int): Int = ${natConstImpl('{x})}

def natConstImpl(x: Expr[Int])(using qctx: QuoteContext): Expr[Int] = {
  import qctx.tasty._
  ...
}

Extractors

import qctx.tasty._ will provide all extractors and methods on TASTy Reflect trees. For example the Literal(_) extractor used below.

def natConstImpl(x: Expr[Int])(using qctx: QuoteContext): Expr[Int] = {
  import qctx.tasty._
  val xTree: Term = x.unseal
  xTree match {
    case Inlined(_, _, Literal(Constant(n: Int))) =>
      if (n <= 0) {
        qctx.error("Parameter must be natural number")
        '{0}
      } else {
        xTree.seal.cast[Int]
      }
    case _ =>
      qctx.error("Parameter must be a known constant")
      '{0}
  }
}

To easily know which extractors are needed, the showExtractors method on a qctx.tasty.Term returns the string representation of the extractors.

The method qctx.tasty.Term.seal provides a way to go back to a quoted.Expr[Any]. Note that the type is Expr[Any]. Consequently, the type must be set explicitly with a checked cast call. If the type does not conform to it an exception will be thrown. In the code above, we could have replaced Expr(n) by xTree.seal.cast[Int].

Obtaining the underlying argument

A macro can access the tree of the actual argument passed on the call-site. The underlyingArgument method on a Term object will give access to the tree defining the expression passed. For example the code below matches a selection operation expression passed while calling the macro below.

inline def macro(param: => Boolean): Unit = ${ macroImpl('param) }

def macroImpl(param: Expr[Boolean])(using qctx: QuoteContext): Expr[Unit] = {
  import qctx.tasty._
  import util._

  param.unseal.underlyingArgument match {
    case t @ Apply(Select(lhs, op), rhs :: Nil) => ..
  }
}

// example
macro(this.checkCondition())

Positions

The tasty context provides a rootPosition value. For macros it corresponds to the expansion site. The macro authors can obtain various information about that expansion site. The example below shows how we can obtain position information such as the start line, the end line or even the source code at the expansion point.

def macroImpl()(qctx: QuoteContext): Expr[Unit] = {
  import qctx.tasty._
  val pos = rootPosition

  val path = pos.sourceFile.jpath.toString
  val start = pos.start
  val end = pos.end
  val startLine = pos.startLine
  val endLine = pos.endLine
  val startColumn = pos.startColumn
  val endColumn = pos.endColumn
  val sourceCode = pos.sourceCode
  ...

Tree Utilities

scala.tasty.reflect contains three facilities for tree traversal and transformations.

TreeAccumulator ties the knot of a traversal. By calling foldOver(x, tree)) we can dive in the tree node and start accumulating values of type X (e.g., of type List[Symbol] if we want to collect symbols). The code below, for example, collects the pattern variables of a tree.

def collectPatternVariables(tree: Tree)(implicit ctx: Context): List[Symbol] = {
  val acc = new TreeAccumulator[List[Symbol]] {
    def apply(syms: List[Symbol], tree: Tree)(implicit ctx: Context) = tree match {
      case Bind(_, body) => apply(tree.symbol :: syms, body)
      case _ => foldOver(syms, tree)
    }
  }
  acc(Nil, tree)
}

A TreeTraverser extends a TreeAccumulator and performs the same traversal but without returning any value. Finally a TreeMap performs a transformation.

Let

scala.tasty.Reflection also offers a method let that allows us to bind the rhs to a val and use it in body. Additionally, lets binds the given terms to names and use them in the body. Their type definitions are shown below:

def let(rhs: Term)(body: Ident => Term): Term = ...

def lets(terms: List[Term])(body: List[Term] => Term): Term = ...

More Examples