Reflection with quotes.reflect
|
This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production. The reference material consulted while preparing these pages is listed in the section bibliography. |
This page is written against Scala 3.10 (3.10.0, released 2026-10-12). Every example was compiled and run with Scala 3.10.0
through Scala CLI 1.17.1 on JDK 26 (checked on 2026-10-15), with each macro in a file of its own (see
Macros: Quotes & Splices). The quotes.reflect API is
the same on the 3.3 LTS, the 3.9 LTS and 3.10 for the members used here, as far as the reference shows. The examples were
run on 3.10.0 only.
Quotes and quoted patterns (see Quoted Pattern Matching)
work with typed expressions and cover most macros. Some jobs need more: finding the fields of a case class, asking
whether one type is a subtype of another, reading the position of an argument, creating a definition with a name chosen
at compile time. quotes.reflect is the API for those jobs. It is a view of the compiler’s typed abstract syntax trees
with their types, symbols, positions and flags, and it is the same API that the TASTy inspector uses on compiled code
(see Staging & TASTy Inspection). The examples use
the trailhead scenario, a toolkit that plans hikes on a network of mountain trails.
Getting the API
The API is a member of the Quotes instance of the current scope. The usual way to reach all of it is an import inside the
macro implementation:
import scala.quoted.*
def impl(x: Expr[Int])(using Quotes): Expr[Int] =
import quotes.reflect.* // Tree, Term, TypeRepr, Symbol, Position, Flags, report, ...
x
quotes is a method that returns the Quotes in scope, so the import brings in every type and every companion-like
module with its extension methods. Only import it where you need it. The code in the rest of the macro keeps the nicer
Expr and Type view.
Source: Scala 3 Macros guide: Reflection
Expr and Term, Type and TypeRepr
The two views convert into each other.
-
Expr[T]wraps aTerm.x.asTermgives the tree andterm.asExprOf[T]turns it back into anExpr[T], which fails if the term does not have typeTor is not a valid expression.term.asExprreturns anExpr[Any]-like value when the type is not known statically. -
Type[T]wraps aTypeRepr.TypeRepr.of[T]gives it, andtpe.asTypereturns aType[?]that you use by naming it with a type pattern:tpe.asType match { case '[t] => ... }.
//> using scala 3.10.0
package trailhead
import scala.quoted.*
inline def fieldNames[A]: List[String] = ${ fieldNamesImpl[A] }
def fieldNamesImpl[A: Type](using Quotes): Expr[List[String]] =
import quotes.reflect.*
val sym = TypeRepr.of[A].typeSymbol
if !sym.flags.is(Flags.Case) then
report.errorAndAbort(s"${sym.name} is not a case class")
Expr(sym.caseFields.map(_.name))
package trailhead
case class Trail(name: String, lengthKm: Double, open: Boolean)
@main def run(): Unit =
println(fieldNames[Trail])
List(name, lengthKm, open)
TypeRepr.of[A].typeSymbol is the shortcut for the symbol that defines A. flags.is(Flags.Case) tests a flag, and
caseFields lists the case class parameters. The macro turns the compile-time list into a List[String] literal with
Expr. For a class that is not a case class the macro reports a normal compile error:
-- Error: Main.scala:3:42
3 |@main def run(): Unit = println(fieldNames[Hut])
| ^^^^^^^^^^^^^^^
| Hut is not a case class
1 error found
Compilation failed
The type Term and its subtypes are described as the tree hierarchy of the API page of
scala.quoted.Quotes.reflectModule. For each type Foo, FooMethods holds the methods available on Foo and
FooModule holds the constructors (apply, copy) and the unapply that makes the type usable in patterns. A subtype
inherits the methods of its supertypes, so a TypeBounds value has the methods of TypeRepr.
Looking at trees
A Term is a case-class-like tree. Pattern matching on it with the module extractors (Apply, Select, Ident,
Literal, Inlined, Block, ValDef, …) is the main way to navigate the API. The macro below prints what kind of tree
it received, its widened type and its position.
inline def inspect[A](inline x: A): String = ${ inspectImpl('x) }
def inspectImpl[A: Type](x: Expr[A])(using Quotes): Expr[String] =
import quotes.reflect.*
val term = x.asTerm
val tpe = term.tpe.widen
val line = term.pos.startLine + 1
val col = term.pos.startColumn + 1
val kind = term match
case Inlined(_, _, t) => "Inlined(" + t.getClass.getSimpleName + ")"
case Apply(fn, args) => s"Apply(${fn.show}, ${args.size} args)"
case Select(q, n) => s"Select(${q.show}, $n)"
case Ident(n) => s"Ident($n)"
case Literal(c) => s"Literal(${c.value})"
case other => other.getClass.getSimpleName
Expr(s"$kind : ${tpe.show} @ $line:$col")
val km = 3.5
println(inspect(km))
println(inspect(km * 2))
println(inspect(Trail("Pico", 12.5, true)))
println(inspect("abc"))
Inlined(Ident) : scala.Double @ 11:19
Inlined(Apply) : scala.Double @ 12:19
Inlined(Apply) : trailhead.Trail @ 13:19
Inlined(Literal) : java.lang.String @ 14:19
Two lessons are in this output. First, an argument to an inline parameter arrives wrapped in Inlined, so a match
that expects Apply directly would miss. The first case unwraps it. Second, positions are 0-based in the API
(startLine, startColumn), so the macro adds 1 to get the numbers that the editor shows. term.pos is a Position that
also has sourceFile, start, end, endLine and sourceCode.
For anything the pattern cases do not cover, print the tree structure:
inline def structure[A](inline x: A): String = ${ structureImpl('x) }
def structureImpl[A](x: Expr[A])(using Quotes): Expr[String] =
import quotes.reflect.*
Expr(Printer.TreeStructure.show(x.asTerm))
Inlined(None, Nil, Apply(Select(Ident("km"), "+"), List(Literal(IntConstant(1)))))
That was structure(km + 1). The line is the actual extractor tree: the + is a Select on km applied to an Int
literal. Printer.TreeStructure shows the constructor form, Printer.TreeCode shows source-like text (what .show
does), and there are matching Printer.TypeReprStructure and Printer.TypeReprCode for types. When a pattern that you
wrote does not match, this is the first thing to print.
Source: Scala 3 Macros guide: Reflection
TypeRepr
A TypeRepr is the compiler’s representation of a type. The common operations are:
-
TypeRepr.of[T], andtpe.widen,tpe.dealias,tpe.simplifiedto normalise a type. -
a <:< bfor subtyping anda =:= bfor type equality. -
tpe.typeSymboland, for a singleton type,tpe.termSymbol. -
tpe.memberType(symbol)for the type of a member as seen fromtpe. -
tpe.appliedTo(args)and theAppliedType(tycon, args)extractor to build and take apart type applications.
inline def subtypeOf[A, B]: Boolean = ${ subtypeImpl[A, B] }
def subtypeImpl[A: Type, B: Type](using Quotes): Expr[Boolean] =
import quotes.reflect.*
Expr(TypeRepr.of[A] <:< TypeRepr.of[B])
inline def dealiased[A]: String = ${ dealiasedImpl[A] }
def dealiasedImpl[A: Type](using Quotes): Expr[String] =
import quotes.reflect.*
val t = TypeRepr.of[A]
val shown = t.dealias match
case AppliedType(tycon, args) => s"${tycon.show} applied to ${args.map(_.show).mkString("[", ", ", "]")}"
case other => other.show
Expr(shown)
type Km = Double
type Routes = Map[String, List[Km]]
println(subtypeOf[List[Int], Seq[Any]])
println(subtypeOf[Seq[Any], List[Int]])
println(dealiased[Routes])
true
false
scala.collection.immutable.Map applied to [scala.Predef.String, scala.collection.immutable.List[trailhead.Main$package.Km]]
dealias only removes the top-level alias. Routes became the Map, but the alias Km inside the type arguments is still
there. A macro that must compare types structurally should use <:< and =:= and not compare TypeRepr values with ==.
The guide makes the same point for members: the two ways of getting the type of Leaf below are the same type but they are
not ==.
val boxTpe = TypeRepr.of[Box.type]
val baseSym = TypeRepr.of[Box.Base].typeSymbol
val leafSym = TypeRepr.of[Box.Leaf].typeSymbol
val viaMember = boxTpe.memberType(leafSym)
symbols equal: true
memberType == of: false
memberType =:= of: true
Here Box is an object that holds sealed trait Base and case class Leaf(x: Int) extends Base. leafSym ==
baseSym.children.head is true: symbols can be compared with ==. Types cannot.
Symbols and Flags
A Symbol identifies a definition: a class, method, val or type. Terms and types both have an associated symbol, and the
symbol is how you compare definitions (==) and how you reach their members. The most used members are:
-
name,fullName,ownerandpos, the position of the definition. -
flags, a set ofFlagsthat you test withis, for exampleFlags.Case,Flags.Sealed,Flags.Private,Flags.Trait. -
declaredMethods,declaredFields,caseFields,children(of a sealed type),companionClassandcompanionModule. -
tree, which returns the definition tree, but see below.
inline def symInfo[A]: String = ${ symInfoImpl[A] }
def symInfoImpl[A: Type](using Quotes): Expr[String] =
import quotes.reflect.*
val s = TypeRepr.of[A].typeSymbol
Expr(s"${s.fullName} owner=${s.owner.name} flags=${s.flags.show} " +
s"pos=${s.pos.map(p => p.sourceFile.name + ":" + (p.startLine + 1)).getOrElse("none")} " +
s"cases=${s.caseFields.size} members=${s.declaredMethods.size}")
trailhead.Trail owner=trailhead flags=Flags.Case | Flags.NoInits pos=Main.scala:3 cases=3 members=15
trailhead.Hut owner=trailhead flags=Flags.NoInits pos=Main.scala:4 cases=0 members=0
The case class Trail(name: String, lengthKm: Double, open: Boolean) reports 3 case fields and 15 declared methods (including
the synthetic ones that the compiler adds), and the plain class class Hut(val beds: Int)
declares no methods, because beds is a field.
Do not rely on Symbol.tree. The tree of a symbol is available only when the symbol was compiled in the same run, or the
compiler was started with -Yretain-trees, and symbols that come from Java never have one. To get the type of a symbol use
tpe.memberType(symbol) as shown above.
Source: Scala 3 Macros guide: Symbols
Building trees
Reflection can also construct definitions, which quotes cannot do when the names or shapes are only known at compile time.
Every new definition needs a symbol with an owner, a type and flags. The owner of code that appears inside the macro’s
splice is Symbol.spliceOwner.
inline def timesTwo(inline x: Int): Int = ${ timesTwoImpl('x) }
def timesTwoImpl(x: Expr[Int])(using Quotes): Expr[Int] =
import quotes.reflect.*
val sym = Symbol.newVal(Symbol.spliceOwner, "doubled", TypeRepr.of[Int], Flags.EmptyFlags, Symbol.noSymbol)
val definition = ValDef(sym, Some(Select.overloaded(x.asTerm, "*", Nil, List(Literal(IntConstant(2))))))
Block(List(definition), Ref(sym)).asExprOf[Int]
println(timesTwo(21))
42
The macro creates a symbol for a val doubled: Int, builds its ValDef with a right-hand side x * 2, and returns a
block { val doubled = x * 2; doubled }. Select.overloaded picks the right overload of * for the argument list.
A second example looks up a method by name at compile time and calls it:
inline def callByName[A](inline target: A, inline method: String): Any = ${ callImpl('target, 'method) }
def callImpl[A: Type](target: Expr[A], method: Expr[String])(using Quotes): Expr[Any] =
import quotes.reflect.*
val name = method.valueOrAbort
val tpe = TypeRepr.of[A]
tpe.typeSymbol.declaredMethods.find(_.name == name) match
case Some(m) if m.paramSymss.flatten.isEmpty => Select(target.asTerm, m).asExpr
case Some(m) => report.errorAndAbort(s"$name takes parameters")
case None => report.errorAndAbort(s"${tpe.show} has no method $name")
Source: Scala 3 Macros guide: Symbols
With class Hut(val beds: Int) that has def capacity: Int = beds and def book(n: Int): Boolean:
val hut = Hut(8)
println(callByName(hut, "capacity"))
println(callByName(hut, "book"))
println(callByName(hut, "missing"))
8
-- Error: Main.scala:17:20
17 | println(callByName(hut, "book"))
| ^^^^^^^^^^^^^^^^^^^^^^^
| book takes parameters
-- Error: Main.scala:18:20
18 | println(callByName(hut, "missing"))
| ^^^^^^^^^^^^^^^^^^^^^^^^^^
| trailhead.Hut has no method missing
The first call ran and printed 8. The two other calls were compiled separately as bad input to show the errors; they
cannot appear in the same program as the first, because an error stops the compile. A first version of the macro applied the
method to an empty argument list too, and failed on the parameterless capacity with method capacity in class Hut does not
take parameters. A def without a parameter list is selected, not applied. Applying is only needed for a method with
parameters, with Apply or appliedToArgs.
Source: Scala 3 Macros guide: Reflection
Reporting and positions
report is part of quotes.reflect. Its operations take an optional Position or an Expr/Tree that supplies one, so the
message points at the code you choose: report.error(msg, expr), report.errorAndAbort(msg, expr), report.warning(msg,
expr) and report.info(msg).
inline def warnMe(inline x: Int): Int = ${ warnImpl('x) }
def warnImpl(x: Expr[Int])(using Quotes): Expr[Int] =
import quotes.reflect.*
report.warning("this call is deprecated by policy", x)
x
-- Warning: Main.scala:16:17
16 | println(warnMe(1))
| ^
| this call is deprecated by policy
1 warning found
The caret is under the argument 1 because the macro passed x as the position. Without it the message points at the
whole macro call.
Searching for givens
A macro often has to ask: is there an instance of a type class in the caller’s scope? From the Expr API,
Expr.summon[T] answers it (see Macros: Quotes & Splices).
From reflection, Implicits.search(tpe) returns an ImplicitSearchSuccess with the tree, or an ImplicitSearchFailure
with an explanation:
inline def summonShow[T]: String = ${ summonShowImpl[T] }
def summonShowImpl[T: Type](using Quotes): Expr[String] =
import quotes.reflect.*
Implicits.search(TypeRepr.of[Ordering[T]]) match
case ok: ImplicitSearchSuccess => Expr("found: " + ok.tree.show)
case bad: ImplicitSearchFailure => Expr("failed: " + bad.explanation.take(60))
println(summonShow[Int])
println(summonShow[Box.Leaf])
found: scala.math.Ordering.Int
failed: no implicit values were found that match type java.util.Comp
The result is the tree of the instance that the compiler would pick, here scala.math.Ordering.Int. Use Expr.summon when
you already hold a Type, and Implicits.search when you start from a TypeRepr, as the guide recommends.
Debugging macros
-
Print the expansion: compile with
-Vprint:inlining(older:-Xprint:inlining) and read the code that replaced the call. -
Print the trees:
x.showfor source-like text andPrinter.TreeStructure.show(x.asTerm)for the constructor form. -
Print types:
Type.show[T],tpe.showandPrinter.TypeReprStructure.show(tpe). -
Add
-Xcheck-macrosto the compiler options while developing; it checks the trees that a macro returns (see Macros: Quotes & Splices). -
The positions and the symbol of a failing tree usually explain a macro that "does nothing": the pattern expected a different tree than the one the compiler built (an
Inlinedwrapper, a typed or block-wrapped tree).
Best practices
The guide’s best-practice page lists these rules for reflection code.
-
Prefer quotes and quoted patterns, and use reflection for what they cannot say. Reflection code is longer and is not checked by the type system the way
Expr[T]is. -
Do not build quotes from large inline splices of arbitrary code,
${ ... }. Compute theExprfirst, give it a name and splice the name with$partsExpr, so the quote stays readable. The guide’s example computesVarargs(parts.map(Expr(_)))in avalfirst. -
Do not nest the
Quotescontext: pass the needed expressions to a function that takes a normal(using Quotes). -
Avoid
Symbol.tree. Usetpe.memberType(symbol)for a member’s type. -
Compare definitions with
Symbols (==), and types with<:<and=:=. -
Pattern match on the API; look at the
unapplyof each*Moduleobject. -
Prefer
Expr.summonwhen you work withExprs,Implicits.searchotherwise.
FAQ
Should I write Expr(...) or '{ ... }? When the value is known at compile time, Expr(value) evaluates more of the work in
the compiler. Quote only code that must run later, because it depends on runtime values. A quote of a primitive constant is
optimised to the same result as Expr.apply.
How do I get the value out of an Expr? x.value returns an Option, x.valueOrAbort aborts with an error, and
Expr.unapply works in patterns.
How do I get the precise type of an Expr? Match with a type variable, x match { case '{ $x: t } => ... }, which binds
t to the precise type of x (see Quoted Pattern Matching).
How do I summon an instance for every element of a tuple type? Match the type with '[head *: tail] and '[EmptyTuple],
summoning each head with Expr.summon. The FAQ page has two skeletons, one that returns List[Expr[Any]] and one that
returns Option[Expr[Tuple]].
How do I summon an instance for a type I only know as a Type[?]? Name it with a pattern, case '[a] => Expr.summon[a], which
also brings a given Type[a] into scope.
Source: Scala 3 Macros guide: FAQ
Scala 2 and Java comparison
Scala 2 macros worked on the compiler’s scala.reflect.api trees through c.universe, which was a different API from the
runtime reflection scala.reflect.runtime.universe but of a similar shape. In Scala 3 quotes.reflect is the compiler-time API.
Runtime reflection also exists in Scala 3, through the JVM, as java.lang.reflect and scala.reflect.ClassTag/TypeTest. It
sees classes after erasure, so it cannot tell List[Int] from List[String]. Java’s java.lang.reflect and annotation
processing (javax.annotation.processing) are the Java counterparts; neither has access to typed trees. Kotlin offers KSP
and compiler plugins, also outside the language.
Scala 3.9 LTS versus 3.10
-
The members shown (
asTerm,asExprOf,TypeRepr.of,typeSymbol,caseFields,Flags,Printer,report,Implicits.search,Symbol.newVal) are part of the API of the 3.x series. The examples were run on 3.10.0 only. -
-Vprint:inliningis the current option name. The old-Xprint:inliningis a deprecated alias that works.
Where the books are outdated
-
Scala 2 macro books use
c.universe,reifyand quasiquotes. None of them exists in Scala 3. -
Early Scala 3 tutorials call
Symbol.treefreely. The API isquotes.reflect, andSymbol.treeis discouraged. -
Examples that compare
TypeReprwith==should use=:=. -
-Xprint:inliningis now-Vprint:inlining.