This doc page is specific to Scala 3, and may cover new concepts not available in Scala 2. Unless otherwise stated, all the code examples in this page assume you are using Scala 3.
The reflection API provides a more complex and comprehensive view on the structure of the code. It provides a view of Typed Abstract Syntax Trees and their properties such as types, symbols, positions and comments.
The API can be used in macros as well as for inspecting TASTy files.
How to use the APIThe reflection API is defined in the type Quotes
as reflect
. The actual instance depends on the current scope, in which quotes or quoted pattern matching is used. Hence, every macro method receives Quotes as an additional argument. Since Quotes
is contextual, to access its members we either need to name the parameter or summon it. The following definition from the standard library details the canonical way of accessing it:
package scala.quoted
transparent inline def quotes(using inline q: Quotes): q.type = q
We can use scala.quoted.quotes
to import the current Quotes
in scope:
import scala.quoted.* // Import `quotes`, `Quotes`, and `Expr`
def f(x: Expr[Int])(using Quotes): Expr[Int] =
import quotes.reflect.* // Import `Tree`, `TypeRepr`, `Symbol`, `Position`, .....
val tree: Tree = ...
...
This will import all the types and modules (with extension methods) of the API.
How to navigate the APIThe full API can be found in the API documentation for scala.quoted.Quotes.reflectModule
. Unfortunately, at this stage, this automatically-generated documentation is not very easy to navigate.
The most important element on the page is the hierarchy tree which provides a synthetic overview of the subtyping relationships of the types in the API. For each type Foo
in the tree:
FooMethods
contains the methods available on the type Foo
FooModule
contains the static methods available on the object Foo
. Most notably, constructors (apply/copy
) and the unapply
method which provides the extractor(s) required for pattern matching are found hereUpper
such that Foo <: Upper
, the methods defined in UpperMethods
are also available on Foo
For example, TypeBounds
, a subtype of TypeRepr
, represents a type tree of the form T >: L <: U
: a type T
which is a super type of L
and a subtype of U
. In TypeBoundsMethods
, you will find the methods low
and hi
, which allow you to access the representations of L
and U
. In TypeBoundsModule
, you will find the unapply
method, which allows you to write:
def f(tpe: TypeRepr) =
tpe match
case TypeBounds(l, u) =>
Because TypeBounds <: TypeRepr
, all the methods defined in TypeReprMethods
are available on TypeBounds
values:
def f(tpe: TypeRepr) =
tpe match
case tpe: TypeBounds =>
val low = tpe.low
val hi = tpe.hi
Relation with Expr/Type Expr and Term
Expressions (Expr[T]
) can be seen as wrappers around a Term
, where T
is the statically-known type of the term. Below, we use the extension method asTerm
to transform an expression into a term. This extension method is only available after importing quotes.reflect.asTerm
. Then we use asExprOf[Int]
to transform the term back into Expr[Int]
. This operation will fail if the term does not have the provided type (in this case, Int
) or if the term is not a valid expression. For example, an Ident(fn)
is an invalid term if the method fn
takes type parameters, in which case we would need an Apply(Ident(fn), args)
.
def f(x: Expr[Int])(using Quotes): Expr[Int] =
import quotes.reflect.*
val tree: Term = x.asTerm
val expr: Expr[Int] = tree.asExprOf[Int]
expr
Type and TypeRepr
Similarly, we can also see Type[T]
as a wrapper over TypeRepr
, with T
being the statically-known type. To get a TypeRepr
, we use TypeRepr.of[T]
, which expects a given Type[T]
in scope (similar to Type.of[T]
). We can also transform it back into a Type[?]
using the asType
method. As the type of Type[?]
is not statically known, we need to name it with an existential type to use it. This can be achieved using the '[t]
pattern.
def g[T: Type](using Quotes) =
import quotes.reflect.*
val tpe: TypeRepr = TypeRepr.of[T]
tpe.asType match
case '[t] => '{ val x: t = ${...} }
...
Symbols
The APIs of Term
and TypeRepr
are relatively closed in the sense that methods produce and accept values whose types are defined in the API. However, you might notice the presence of Symbol
s which identify definitions.
Both Term
s and TypeRepr
s (and therefore Expr
s and Type
s) have an associated symbol. Symbol
s make it possible to compare two definitions using ==
to know if they are the same. In addition, Symbol
exposes and is used by many useful methods. For example:
declaredFields
and declaredMethods
allow you to iterate on the fields and members defined inside a symbolflags
allows you to check multiple properties of a symbolcompanionClass
and companionModule
provide a way to jump to and from the companion object/classTypeRepr.baseClasses
returns the list of symbols of classes extended by a typeSymbol.pos
gives you access to the position where the symbol is defined, the source code of the definition, and even the filename where the symbol is definedSymbolMethods
Consider an instance of the type TypeRepr
named val tpe: TypeRepr = ...
. Then:
tpe.typeSymbol
returns the symbol of the type represented by TypeRepr
. The recommended way to obtain a Symbol
given a Type[T]
is TypeRepr.of[T].typeSymbol
tpe.termSymbol
returns the symbol of the underlying object or valuetpe.memberType(symbol)
returns the TypeRepr
of the provided symbolt: Tree
, t.symbol
returns the symbol associated with a tree. Given that Term <: Tree
, Expr.asTerm.symbol
is the best way to obtain the symbol associated with an Expr[T]
sym: Symbol
, sym.tree
returns the Tree
associated to the symbol. Be careful when using this method as the tree for a symbol might not be defined. Read more on the best practices pageIt will often be useful to create helper methods or extractors that perform some common logic of your macros.
The simplest methods will be those that only mention Expr
, Type
, and Quotes
in their signature. Internally, they may use reflection, but this will not be seen at the use site of the method.
def f(x: Expr[Int])(using Quotes): Expr[Int] =
import quotes.reflect.*
...
In some cases, it may be inevitable that some methods will expect or return Tree
s or other types in quotes.reflect
. For these cases, the best practice is to follow the following method signature examples:
A method that takes a quotes.reflect.Term
parameter
def f(using Quotes)(term: quotes.reflect.Term): String =
import quotes.reflect.*
...
An extension method for a quotes.reflect.Term
returning a quotes.reflect.Tree
extension (using Quotes)(term: quotes.reflect.Term)
def g: quotes.reflect.Tree = ...
An extractor that matches on quotes.reflect.Term
s
object MyExtractor:
def unapply(using Quotes)(x: quotes.reflect.Term) =
...
Some(y)
Debugging Runtime checksAvoid saving the
Quotes
context in a field.Quotes
in fields inevitably make its use harder by causing errors involvingQuotes
with different paths.Usually, these patterns have been seen in code that uses the Scala 2 ways to define extension methods or contextual unapplies. Now that we have
given
parameters that can be added before other parameters, all these old workarounds are not needed anymore. The new abstractions make it simpler both at the definition site and at the use site.
Expressions (Expr[T]
) can be seen as wrappers around a Term
, where T
is the statically-known type of the term. Hence, these checks will be done at runtime (i.e. compile-time when the macro expands).
It is recommended to enable the -Xcheck-macros
flag while developing macros or on the tests for the macro. This flag will enable extra runtime checks that will try to find ill-formed trees or types as soon as they are created.
There is also the -Ycheck:all
flag that checks all compiler invariants for tree well-formedness. These checks will usually fail with an assertion error.
The toString
methods on types in the quotes.reflect
package are not great for debugging as they show the internal representation rather than the quotes.reflect
representation. In many cases these are similar, but they may sometimes lead the debugging process astray, so they shouldnât be relied on.
Instead, quotes.reflect.Printers
provides a set of useful printers for debugging. Notably the TreeStructure
, TypeReprStructure
, and ConstantStructure
classes can be quite useful. These will print the tree structure following loosely the extractors that would be needed to match it.
val tree: Tree = ...
println(tree.show(using Printer.TreeStructure))
One of the most useful places where this can be added is at the end of a pattern match on a Tree
.
tree match
case Ident(_) =>
case Select(_, _) =>
...
case _ =>
throw new MatchError(tree.show(using Printer.TreeStructure))
This way, if a case is missed the error will report a familiar structure that can be copy-pasted to start fixing the issue.
You can make this printer the default if desired:
import quotes.reflect.*
given Printer[Tree] = Printer.TreeStructure
...
println(tree.show)
More
Coming soon
RetroSearch is an open source project built by @garambo | Open a GitHub Issue
Search and Browse the WWW like it's 1997 | Search results from DuckDuckGo
HTML:
3.2
| Encoding:
UTF-8
| Version:
0.7.4