Macros: Quotes & Splices
|
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). Scala macros have the same design in the 3.3 LTS, the
3.9 LTS and 3.10, because the scala.quoted API has been stable since Scala 3.0. The only thing on this page that is
tied to a recent release is the publicInBinary annotation (see Public in binary and macro APIs). The 3.9 LTS line
was not used to run the examples.
A macro is a function that runs while the compiler runs. It receives the program text that appears at its call site as
typed expressions, and it returns a new expression that replaces the call. Scala 3 macros are built from two ideas:
inline methods decide when a macro expands, and a typed quote-and-splice notation describes what the expansion
looks like. The old Scala 2 macros (scala.reflect.macros, def macro, quasiquotes) were dropped, see
Where the books are outdated. This page covers the quote and splice notation and the structure of a macro. The
inspection of arbitrary code, quoted patterns and runtime code generation have their own pages:
Quoted Pattern Matching,
Reflection with quotes.reflect and
Staging & TASTy Inspection. The big picture is in
Metaprogramming Overview, and the inline machinery that
every macro builds on is in Inline.
The examples use the trailhead scenario, a toolkit that plans hikes on a network of mountain trails.
Expr, Type and Quotes
Three types from the scala.quoted package are the vocabulary of every macro.
-
Expr[T]is a typed piece of program that, when it is eventually run, produces a value of typeT. It is not the value itself.Expr[Int]can be the literal2, the calllist.sizeor a block of statements. -
Type[T]is a typed type in the same sense. A macro that is generic inToften needs aType[T]to write'{ ... }code that mentionsT. It is usually passed as a context bound,def impl[T: Type]. -
Quotesis the context of one macro expansion. It carries the compiler’s state, and thequotes.reflectAPI (see Reflection with quotes.reflect) hangs off it. A macro implementation always has a(using Quotes)parameter. EveryExprandTypebelongs to theQuotesthat created it, which is why a strayExprcannot be reused in another expansion.
Two operators build and take apart Exprs.
-
A quote,
'{ ... }, turns a piece of code into anExprwithout running it.'{ 1 + 2 }is anExpr[Int]. The quote'xis the short form for an identifier,'{ x }. The type form isType.of[T], or'[T]in a pattern. -
A splice,
${ ... }, is the reverse. Inside a quote it inserts anExprinto the code being built.$xis the short form for${ x }whenxis a plain name.
//> using scala 3.10.0
package trailhead
import scala.quoted.*
inline def dbg[A](inline x: A): A = ${ dbgImpl('x) }
def dbgImpl[A: Type](x: Expr[A])(using Quotes): Expr[A] =
val text = Expr(x.show)
'{ val v = $x; println(${ text } + " = " + v); v }
The inline method dbg is what the user calls. Its right-hand side is a single top-level splice, and the splice calls
the macro implementation dbgImpl, passing the argument as the quoted 'x. The implementation builds a new
expression with a quote. It evaluates x once into v, prints the source text of x next to the value, and returns the
value. x.show renders the expression as source text, and Expr(text) is the way to turn that runtime-of-the-compiler
String into an Expr[String] (it is a call to ToExpr, covered below).
The call site is in another file (the next section explains why).
package trailhead
@main def run(): Unit =
val km = 12.5
dbg(km * 2)
Output of scala run . with both files in the directory:
km.*(2) = 25.0
show prints the tree in a canonical form, so the multiplication appears as the method call km.*(2). The call
dbg(km * 2) was replaced by the block built in dbgImpl before the program ran, so no macro code exists at runtime.
Quote and splice levels
Quotes and splices have a level. Code outside any quote or splice is at level 0. Each enclosing '{ ... } raises the
level by one and each enclosing ${ ... } lowers it by one. The rule that keeps macros type safe is that a value must be
used at the level it was defined at, never at a different one. This is called level consistency.
The diagram shows that the quote is where the generated code lives (level 1), the splices punch back to the macro’s own
code (level 0), and the finished Expr replaces the inline call in the user’s program. A variable defined in the macro
at level 0 cannot be named inside the quote, because by the time the generated code runs the variable no longer exists. The
compiler rejects it.
inline def bad(inline xs: List[Int]): Int = ${ badImpl('xs) }
private def badImpl(xs: Expr[List[Int]])(using Quotes): Expr[Int] =
val offset = 10
'{ $xs.sum + offset }
-- Error: Lvl.scala:9:15
9 | '{ $xs.sum + offset }
| ^^^^^^
| access to value offset from wrong staging level:
| - the definition is at level 0,
| - but the access is at level 1.
1 error found
The repair is to lift the value into the quote with a splice, '{ $xs.sum + ${ Expr(offset) } }, which uses the
ToExpr conversion described below. That version compiled and ran. In the reverse direction the variable-in-a-quote
case is allowed: a val y = ... declared inside a quote can be referred to as 'y inside a nested splice, as the next
example does.
//> using scala 3.10.0
package trailhead
import scala.quoted.*
inline def power(x: Double, inline n: Int): Double = ${ powerImpl('x, 'n) }
def powerImpl(x: Expr[Double], n: Expr[Int])(using Quotes): Expr[Double] =
n.value match
case Some(k) => unrolled(x, k)
case None => '{ math.pow($x, $n.toDouble) }
private def unrolled(x: Expr[Double], k: Int)(using Quotes): Expr[Double] =
if k == 0 then '{ 1.0 }
else if k % 2 == 0 then '{ val y = $x * $x; ${ unrolled('y, k / 2) } }
else '{ $x * ${ unrolled(x, k - 1) } }
package trailhead
@main def run(): Unit =
val slope = 1.5
println(power(slope, 5))
println(power(slope, 0))
7.59375
1.0
The recursion on k runs at compile time at level 0 and only the multiplications are emitted. Compiling with
-Vprint:inlining (the older spelling -Xprint:inlining still works but prints a deprecation notice) shows what
replaced the first call:
println(
(slope *
{
val y: Double = (slope * slope)
{
val y: Double = (y * y)
(y * 1.0d)
}
}
):Double
)
println(1.0d:Double)
The two y are different symbols that the printer shows with the same name. power(slope, 0) collapsed to the constant
1.0d. The parameter n is inline, which is what makes n.value succeed: an inline parameter is passed as the
argument expression itself instead of a value, so a literal stays a literal.
The inline definition and the macro implementation
A macro is always two definitions.
-
An
inlinemethod whose body is one top-level splice. This is the API that users call. It can haveinlineparameters, type parameters and several parameter lists. The splice may contain only a call to a method that receives quoted arguments,'xfor a parameter,'{ ... },Type.oforExpr(...), no other computation. -
A method with a
(using Quotes)parameter that takesExpr(andType) arguments and returns anExpr. This is the macro implementation. It is ordinary Scala code that runs inside the compiler, so it can use the standard library, pattern match, recurse and allocate.
Why the split? Only the compiler knows how to turn a call site into Exprs, and it does so for an inline method with a
splice body. The implementation, by contrast, has to be already compiled, because the compiler needs to run it. The
inline method is the signature that survives into other libraries' compilation, and the implementation is loaded from the
class path when a call is expanded.
The expansion happens during inlining, after type checking of the call site. A macro never sees untyped code, which is the biggest difference from the Scala 2 macro system.
Two practical notes on the shape:
-
The body of the
inlinemethod is the splice. The documentation also shows aninlinematchorifwhose branches are splices, which lets one API choose between several implementations. -
Inlined code is copied into other files, so the members it refers to must be reachable from there. Section Public in binary and macro APIs shows what the compiler does about private members.
Macros need a separate compilation unit
The compiler has to run the macro implementation, so the code it runs must already exist as compiled classes. Two things follow.
-
A macro cannot be used in the same source file it is defined in. This is checked and reported.
-
Using a macro defined in another file of the same compilation run works in Scala 3 when the files have no cyclic dependency, because the compiler suspends the compilation of the using file until the macro is compiled. This is the behaviour verified with Scala CLI below.
Verified with Scala CLI 1.17.1 and Scala 3.10.0.
Same file, one program. Both the macro and its use are in One.scala:
//> using scala 3.10.0
import scala.quoted.*
inline def dbg[A](inline x: A): A = ${ dbgImpl('x) }
def dbgImpl[A: Type](x: Expr[A])(using Quotes): Expr[A] =
'{ val v = $x; println(${Expr(x.show)} + " = " + v); v }
@main def run(): Unit =
val km = 12.5
dbg(km * 2)
-- Error: One.scala:8:5
8 | dbg(km * 2)
| ^^^^^^^^^^^
| Cannot call macro method dbgImpl defined in the same source file
1 error found
Compilation failed
Two files in one directory (the layout of the examples above). scala run . compiles Macros.scala and Main.scala
together and the program runs. This is the simplest layout for a small project and for the examples in this guide.
Compile the macro first, use it through the class path. This is the layout that mirrors a library: the macro is compiled to a directory (or a jar), and the application only sees the classes:
scala compile --server=false lib/Macros.scala -d out
scala run --server=false -cp out app/Main.scala --scala 3.10.0
Both commands succeeded and the second printed km.*(2) = 25.0. For a real library, publish the macro as a normal
dependency (//> using dep in Scala CLI, libraryDependencies in sbt) and the users get the inline method with its
macro implementation from the jar.
Test scope. With the Toolkit test dependency, the macro defined in the main scope can be used by a test source in the same project. See Testing macros.
In sbt the same restriction exists, and the convention is the one above: put the macro in a module (or project) that the using module depends on, or rely on the suspension within one module. Library authors who want to be safe use two modules.
ToExpr and FromExpr
To move a value between the compiler’s world (level 0) and the generated code (level 1) the compiler uses two type classes.
-
ToExpr[T]hasdef apply(x: T)(using Quotes): Expr[T].Expr(x)summons aToExpr[T]and applies it. There are given instances for the primitive types,String,Option,Either, tuples and the common collections. -
FromExpr[T]hasdef unapply(x: Expr[T])(using Quotes): Option[T].x.valuereturnsSomeif the expression is a constant that the instance recognises, andx.valueOrAbortaborts the compilation with an error otherwise.
You write your own instance to pass a user-defined type through a macro. The pattern in the FromExpr is the quoted
pattern, which gets its own page: Quoted Pattern Matching.
//> using scala 3.10.0
package trailhead
import scala.quoted.*
case class Trail(name: String, lengthKm: Double)
given ToExpr[Trail] with
def apply(t: Trail)(using Quotes): Expr[Trail] =
'{ Trail(${ Expr(t.name) }, ${ Expr(t.lengthKm) }) }
given FromExpr[Trail] with
def unapply(x: Expr[Trail])(using Quotes): Option[Trail] = x match
case '{ Trail(${ Expr(n) }, ${ Expr(km) }) } => Some(Trail(n, km))
case _ => None
The ToExpr is the quote of a constructor call whose arguments are themselves lifted. The FromExpr is the inverse: it
only succeeds when every argument of the constructor call is a literal.
Source: Scala 3 Macros guide: ToExpr
Reporting errors
A macro that finds invalid input should fail the compilation with a message that points at the offending code. Use the
report object from quotes.reflect.
-
report.errorAndAbort(msg)reports an error at the macro call and stops the expansion.report.errorAndAbort(msg, expr)points at the given expression, which is almost always what a user wants. -
report.error(msg, expr)reports and keeps going, so that several problems can be shown in one pass. -
report.warning(msg, expr)andreport.info(msg)produce a warning or an informational message.
//> using scala 3.10.0
package trailhead
import scala.quoted.*
inline def trail(inline name: String, inline lengthKm: Double): Trail =
${ trailImpl('name, 'lengthKm) }
private def trailImpl(name: Expr[String], km: Expr[Double])(using Quotes): Expr[Trail] =
import quotes.reflect.report
(name.value, km.value) match
case (Some(n), Some(k)) if n.isBlank => report.errorAndAbort("a trail needs a name", name)
case (Some(_), Some(k)) if k <= 0 => report.errorAndAbort(s"length must be positive, got $k", km)
case (Some(n), Some(k)) => Expr(Trail(n, k))
case _ => report.errorAndAbort("name and length must be literals", Expr.ofTuple((name, km)))
A call with a bad argument is rejected at compile time, at the argument:
val bad = trail("Pico", -1.0)
-- Error: Main.scala:4:26
4 | val bad = trail("Pico", -1.0)
| ^^^^
| length must be positive, got -1.0
1 error found
Compilation failed
The macro validated its arguments while the program was compiled, so a bad trail can never reach the runtime. The third case matters in practice: a macro argument that is not a literal (a value read at runtime) cannot be checked, so the macro refuses it instead of silently generating unchecked code. A less strict macro would fall back to a runtime check in the generated code.
Varargs, Type and Expr.summon
A repeated parameter reaches the macro as an Expr[Seq[T]]. The extractor Varargs takes it apart into a
Seq[Expr[T]] when the arguments are written out at the call site, and Expr.ofSeq or Expr.ofList goes the other way.
inline def totalKm(inline trails: Trail*): Double = ${ totalKmImpl('trails) }
private def totalKmImpl(trails: Expr[Seq[Trail]])(using Quotes): Expr[Double] =
trails match
case Varargs(ts) =>
val known = ts.map(_.value)
if known.forall(_.isDefined) then Expr(known.flatten.map(_.lengthKm).sum)
else '{ ${ Expr.ofList(ts.toList) }.map(_.lengthKm).sum }
case _ => '{ $trails.map(_.lengthKm).sum }
If every argument is a constant Trail the sum is computed at compile time and the result is a literal. Otherwise the
macro generates code that sums at runtime. The pattern of trying to simplify at compile time and falling back to
generated code is the common one. The macro has three exits and the third handles a sequence that is not written out, such
as totalKm(ts*).
A type parameter reaches the macro as a Type[T] given. Type.show[T] renders it, and Type.of[T] is the explicit
form. Expr.summon[F[T]] searches for a given at the call site and returns Option[Expr[F[T]]], so a macro can use the
instances available in the user’s scope:
inline def maxOf[A](inline a: A, inline b: A): A = ${ maxImpl('a, 'b) }
private def maxImpl[A: Type](a: Expr[A], b: Expr[A])(using Quotes): Expr[A] =
Expr.summon[Ordering[A]] match
case Some(ord) => '{ if $ord.gteq($a, $b) then $a else $b }
case None =>
quotes.reflect.report.errorAndAbort(s"no Ordering for ${Type.show[A]}")
inline def typeName[A]: String = ${ typeNameImpl[A] }
private def typeNameImpl[A: Type](using Quotes): Expr[String] = Expr(Type.show[A])
println(maxOf(3, 7))
println(maxOf("alpha", "beta"))
println(totalKm(pico, trail("Tejo", 8.0)))
println(typeName[Map[String, List[Trail]]])
7
beta
20.5
scala.collection.immutable.Map[scala.Predef.String, scala.collection.immutable.List[trailhead.Trail]]
Type.show prints fully qualified names, so a macro that produces text for users usually post-processes it.
Checking the expansion: -Xcheck-macros
The compiler normally trusts the trees a macro returns. The compiler option -Xcheck-macros makes it add extra checks
on the macro expansion (well-formedness of the generated trees, such as types and owners of definitions). It costs
compile time, so it is meant for macro development and for the CI of a macro library, not for production builds. In Scala CLI:
//> using option -Xcheck-macros
Source: Scala 3 Reference: Macros
With -Xcheck-macros the trail example above compiled and printed the same output as without it, since that macro
only builds well-formed trees. A macro that builds a tree incorrectly (for instance a definition with the wrong owner, which is easy to do with the
reflection API) can otherwise fail in a later compiler phase with a message that does not mention the macro. The checked
build is the place to look for such problems first.
Public in binary and macro APIs
Inline code is copied into the call site, which may be in another jar. If an inline method refers to a private or
protected member, the compiler generates a public accessor (named inline$helper) in the defining class so that the
copy can still call it. The accessor’s name and presence are not part of the source API but they are part of the binary
API, so changing the inline method can break binary compatibility of a library.
The annotation scala.annotation.publicInBinary lets you choose a stable, public-in-bytecode member instead. It cannot be
placed on a private member. The compiler says so and suggests the alternatives:
object Stable:
@publicInBinary private def helper(x: Int): Int = x + 1
-- Error: Lib.scala:10:30
10 | @publicInBinary private def helper(x: Int): Int = x + 1
| ^
| @publicInBinary cannot be used on private definitions
|
| Consider using `private[Stable]` or `protected` instead
1 error found
Source: Scala API: publicInBinary
With private[Stable] it compiles. This javap -p listing compares the two objects:
object Plain:
private def helper(x: Int): Int = x + 1
inline def api(x: Int): Int = helper(x)
object Stable:
@publicInBinary private[Stable] def helper(x: Int): Int = x + 1
inline def api(x: Int): Int = helper(x)
public final class trailhead.Plain$ {
private int helper(int);
public final int inline$helper(int);
}
public final class trailhead.Stable$ {
public int helper(int);
}
Plain gets a synthetic inline$helper accessor, and Stable makes helper itself public in the bytecode with its
own name. Mark the helpers that a macro library’s inline methods and macro implementations use with @publicInBinary so
that the binary names are the ones you wrote and nothing else is generated.
Source: Scala API: publicInBinary
Testing macros
Two techniques cover most needs.
-
Test the expansion’s behaviour with an ordinary test in a different file or module than the macro. In Scala CLI put the macro in the main scope and the test in a
*.test.scalafile. -
Test that bad input is rejected with
scala.compiletime.testing.typeChecks(code)returns whether a code string compiles, andtypeCheckErrors(code)returns the list of errors with their messages. The code is checked at the call site’s compile time, so macro errors are caught too.
//> using scala 3.10.0
//> using test.toolkit 0.9.2
package trailhead
import scala.quoted.*
inline def triple(inline n: Int): Int = ${ tripleImpl('n) }
def tripleImpl(n: Expr[Int])(using Quotes): Expr[Int] =
n.value match
case Some(k) => Expr(k * 3)
case None => '{ $n * 3 }
package trailhead
import scala.compiletime.testing.{typeCheckErrors, typeChecks}
class PowTests extends munit.FunSuite:
test("folds literals"):
assertEquals(triple(14), 42)
test("handles runtime values"):
val n = scala.util.Random.nextInt(1) + 5
assertEquals(triple(n), 15)
test("typechecks"):
assert(typeChecks("triple(2)"))
assert(!typeChecks("triple(\"a\")"))
trailhead.PowTests:
+ folds literals 0.019s
+ handles runtime values 0.001s
+ typechecks 0.008s
(The JDK printed sun.misc.Unsafe warnings from the Scala runtime before this output. They are unrelated to macros.)
The trail macro’s error cases were tested the same way, by printing the messages of typeCheckErrors("""trail("", 3.0)""")
and the other bad calls:
a trail needs a name
length must be positive, got -1.0
name and length must be literals
To also inspect what a macro produced, print the trees after inlining with -Vprint:inlining (shown above) or print
the expression inside the macro with expr.show (code form) or expr.asTerm.show(using Printer.TreeStructure) (structure,
see Reflection with quotes.reflect).
Macros in Scala 2, Java and Kotlin
| Aspect | Scala 2 | Scala 3 |
|---|---|---|
Definition |
|
|
Input |
Untyped and typed trees, plus |
Typed |
Code construction |
Quasiquotes |
Quotes |
Annotations that generate code |
Macro annotations (paradise plugin) |
Experimental in Scala 3, not covered here |
Whitebox macros |
|
|
Java has no macro system. Annotation processors, bytecode generators and Lombok play part of the role, and they run outside the type system. Kotlin has compiler plugins and KSP, which are separate from the language. Scala is the only one of the three where metaprogramming is a language feature that is part of the type system.
Scala 3.9 LTS versus 3.10
-
The
scala.quotedAPI (Expr,Type,Quotes,ToExpr,FromExpr,Varargs) and thequotes.reflectAPI are the same in 3.3 LTS, 3.9 LTS and 3.10 for everything on this page. The examples ran only on 3.10.0. -
-Vprint:inliningis the current spelling of the option.-Xprint:inliningis a deprecated alias that works and warns (3.10.0 printed "Option -Xprint is a deprecated alias: use -Vprint instead"). -
publicInBinarywas verified on 3.10.0 only.
Where the books are outdated
-
Scala 2 macros,
scala.reflect.macros, quasiquotes,c.Exprand macro paradise do not exist in Scala 3. Code that uses them must be rewritten with quotes and splices. The reference page on dropped macros lists the replacement. -
Tutorials that define the macro and use it in the same file fail with the error shown above. Always use two files.
-
Older examples return
Expr[T]from the implementation and take theQuotesas an implicit parameter. The current form is a(using Quotes)clause. -
-Xprint:inliningis now-Vprint:inlining(the old spelling still works with a deprecation notice). -
Examples that write
x.showfor output assume source-like text.showprints a canonical, fully applied form (km.*(2)forkm * 2), so do not use it for messages meant for end users without checking what it prints.