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 type T. It is not the value itself. Expr[Int] can be the literal 2, the call list.size or a block of statements.

  • Type[T] is a typed type in the same sense. A macro that is generic in T often needs a Type[T] to write '{ ... } code that mentions T. It is usually passed as a context bound, def impl[T: Type].

  • Quotes is the context of one macro expansion. It carries the compiler’s state, and the quotes.reflect API (see Reflection with quotes.reflect) hangs off it. A macro implementation always has a (using Quotes) parameter. Every Expr and Type belongs to the Quotes that created it, which is why a stray Expr cannot be reused in another expansion.

Two operators build and take apart Exprs.

  • A quote, '{ ... }, turns a piece of code into an Expr without running it. '{ 1 + 2 } is an Expr[Int]. The quote 'x is the short form for an identifier, '{ x }. The type form is Type.of[T], or '[T] in a pattern.

  • A splice, ${ ... }, is the reverse. Inside a quote it inserts an Expr into the code being built. $x is the short form for ${ x } when x is 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.

flowchart TB subgraph L0["Level 0: the macro implementation runs inside the compiler"] A["def dbgImpl(x: Expr[A]) = ..."] --> B["builds an Expr with a quote"] subgraph L1["Level 1: inside a quote, this is the code that will be generated"] C["'{ val v = ..."] --> D["${ text } splice returns to level 0"] C --> E["$x splice inserts the argument expression"] end B --> C end L1 -.->|"expansion replaces the call"| R["Level 0 of the user program: ordinary code"]

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.

  1. An inline method whose body is one top-level splice. This is the API that users call. It can have inline parameters, type parameters and several parameter lists. The splice may contain only a call to a method that receives quoted arguments, 'x for a parameter, '{ ... }, Type.of or Expr(...), no other computation.

  2. A method with a (using Quotes) parameter that takes Expr (and Type) arguments and returns an Expr. 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 inline method is the splice. The documentation also shows an inline match or if whose 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.

  1. A macro cannot be used in the same source file it is defined in. This is checked and reported.

  2. 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] has def apply(x: T)(using Quotes): Expr[T]. Expr(x) summons a ToExpr[T] and applies it. There are given instances for the primitive types, String, Option, Either, tuples and the common collections.

  • FromExpr[T] has def unapply(x: Expr[T])(using Quotes): Option[T]. x.value returns Some if the expression is a constant that the instance recognises, and x.valueOrAbort aborts 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.

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) and report.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

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

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.

Testing macros

Two techniques cover most needs.

  1. 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.scala file.

  2. Test that bad input is rejected with scala.compiletime.testing. typeChecks(code) returns whether a code string compiles, and typeCheckErrors(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

def f(x: Int): Int = macro impl with a blackbox.Context or whitebox.Context argument

inline def f(inline x: Int): Int = ${ impl('x) } and an implementation with (using Quotes)

Input

Untyped and typed trees, plus c.universe

Typed Expr[T] and Type[T] only. The tree level is behind quotes.reflect

Code construction

Quasiquotes q"..."

Quotes '{ ... } and splices ${ ... }, checked at the level of types

Annotations that generate code

Macro annotations (paradise plugin)

Experimental in Scala 3, not covered here

Whitebox macros

whitebox.Context

transparent inline plus a macro

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.quoted API (Expr, Type, Quotes, ToExpr, FromExpr, Varargs) and the quotes.reflect API 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:inlining is the current spelling of the option. -Xprint:inlining is a deprecated alias that works and warns (3.10.0 printed "Option -Xprint is a deprecated alias: use -Vprint instead").

  • publicInBinary was verified on 3.10.0 only.

Where the books are outdated

  • Scala 2 macros, scala.reflect.macros, quasiquotes, c.Expr and 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 the Quotes as an implicit parameter. The current form is a (using Quotes) clause.

  • -Xprint:inlining is now -Vprint:inlining (the old spelling still works with a deprecation notice).

  • Examples that write x.show for output assume source-like text. show prints a canonical, fully applied form (km.*(2) for km * 2), so do not use it for messages meant for end users without checking what it prints.