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.

Expr and Term, Type and TypeRepr

The two views convert into each other.

  • Expr[T] wraps a Term. x.asTerm gives the tree and term.asExprOf[T] turns it back into an Expr[T], which fails if the term does not have type T or is not a valid expression. term.asExpr returns an Expr[Any]-like value when the type is not known statically.

  • Type[T] wraps a TypeRepr. TypeRepr.of[T] gives it, and tpe.asType returns a Type[?] 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.

TypeRepr

A TypeRepr is the compiler’s representation of a type. The common operations are:

  • TypeRepr.of[T], and tpe.widen, tpe.dealias, tpe.simplified to normalise a type.

  • a <:< b for subtyping and a =:= b for type equality.

  • tpe.typeSymbol and, for a singleton type, tpe.termSymbol.

  • tpe.memberType(symbol) for the type of a member as seen from tpe.

  • tpe.appliedTo(args) and the AppliedType(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, owner and pos, the position of the definition.

  • flags, a set of Flags that you test with is, for example Flags.Case, Flags.Sealed, Flags.Private, Flags.Trait.

  • declaredMethods, declaredFields, caseFields, children (of a sealed type), companionClass and companionModule.

  • 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.

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")

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.

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.show for source-like text and Printer.TreeStructure.show(x.asTerm) for the constructor form.

  • Print types: Type.show[T], tpe.show and Printer.TypeReprStructure.show(tpe).

  • Add -Xcheck-macros to 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 Inlined wrapper, 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 the Expr first, give it a name and splice the name with $partsExpr, so the quote stays readable. The guide’s example computes Varargs(parts.map(Expr(_))) in a val first.

  • Do not nest the Quotes context: pass the needed expressions to a function that takes a normal (using Quotes).

  • Avoid Symbol.tree. Use tpe.memberType(symbol) for a member’s type.

  • Compare definitions with Symbols (==), and types with <:< and =:=.

  • Pattern match on the API; look at the unapply of each *Module object.

  • Prefer Expr.summon when you work with Exprs, Implicits.search otherwise.

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.

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:inlining is the current option name. The old -Xprint:inlining is a deprecated alias that works.

Where the books are outdated

  • Scala 2 macro books use c.universe, reify and quasiquotes. None of them exists in Scala 3.

  • Early Scala 3 tutorials call Symbol.tree freely. The API is quotes.reflect, and Symbol.tree is discouraged.

  • Examples that compare TypeRepr with == should use =:=.

  • -Xprint:inlining is now -Vprint:inlining.