Compile-Time Operations

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) and every example was compiled and run with Scala 3.10.0 through Scala CLI on JDK 26 (checked on 2026-10-16). The package scala.compiletime is part of the Scala 3 standard library, and nothing on this page is experimental or in preview. The examples were also run on 3.3.8 (the 3.3 LTS) with the same output, apart from syntax: on 3.3 the given in the second program must be written given Show[Int] with instead of given Show[Int]:. Everything also applies to the 3.9 LTS. The examples use the trailhead scenario, a toolkit that plans hikes on a network of mountain trails.

The members of scala.compiletime are the second layer of metaprogramming, between the plain inline methods of Inline and the macros of Macros: Quotes & Splices. They give an inline method access to information that exists only in the compiler: the value behind a singleton type, the type of an argument, the source text of an expression, the set of givens in scope, and the ability to turn a wrong call into a compile error. None of them need a macro, a Quotes or the quotes.reflect API. They are also what the derived methods of Type Class Derivation are written with. Most of the members below work only inside an inline method and fail to compile elsewhere.

Values and types: constValue, constValueOpt, erasedValue

A singleton type is a type that has exactly one value: the literal types 42, "summit" and true, and Mirror label types such as the "name" in a tuple of field names. constValue[T] returns the value of such a type. It makes the type information available as a run-time value, which is the reverse of what erasedValue does.

Member Meaning

constValue[T]

The value of the singleton type T. A compile error if T is not a singleton.

constValueOpt[T]

Some(value) when T is a singleton, and None otherwise, decided at compile time.

constValueTuple[T <: Tuple]

A tuple of the values of a tuple of singleton types.

erasedValue[T]

A placeholder value of type T, for pattern matching on a type in an inline match. It is erased: it must never run.

The program below uses all four, together with error and codeOf from the next sections:

//> using scala 3.10.0

package trailhead

import scala.compiletime.{codeOf, constValue, constValueOpt, constValueTuple, error, erasedValue}

// constValue: turn a singleton type into a value
inline def sizeOf[N <: Int]: Int = constValue[N]

// constValueOpt: Some(value) when the type is a singleton, None otherwise
inline def describe[T]: String =
  constValueOpt[T] match
    case Some(v) => s"constant $v"
    case None    => "not a constant type"

// constValueTuple: a tuple type of singletons to a tuple of values
type Labels = ("name", "lengthKm", "ascentM")

// erasedValue: a dummy value of type T, only usable for matching on its type
inline def kindOf[T]: String =
  inline erasedValue[T] match
    case _: Int    => "int"
    case _: String => "string"
    case _: (a, b) => "pair"
    case _         => "other"

// error: a custom compile-time error
inline def alpine(inline grade: Int): Int =
  inline if grade < 1 || grade > 6 then error("Alpine grade must be between 1 and 6")
  else grade

// codeOf: the source text of an argument
inline def show(inline expr: Any): String = codeOf(expr)

@main def run(): Unit =
  println(sizeOf[42])
  println(describe["summit"])
  println(describe[String])
  println(constValueTuple[Labels])
  println(kindOf[Int])
  println(kindOf[(Int, String)])
  println(kindOf[List[Int]])
  println(alpine(4))
  println(show(1 + 2 * 3))
  println(show(List(1, 2).map(_ + 1)))
42
constant summit
not a constant type
(name,lengthKm,ascentM)
int
pair
other
4
7
List.apply[Int]([1,2 : Int]*).map[Int]((_$1: Int) => _$1 + 1)

Things to take from that run:

  • kindOf[List[Int]] falls to the last case. erasedValue matching is a type test on the static type, evaluated by the compiler, so there is no erasure problem: case _: (a, b) matches the tuple type and binds the type variables a and b.

  • erasedValue[T] is declared as an erased definition. Using the value, for example erasedValue[T].toString, is rejected by the compiler with the message "method erasedValue is declared as erased, but is in fact used" (verified on 3.10.0).

  • constValue needs a type that the compiler knows when the inline method is expanded. A type that is only an upper bound does not work, and the compiler says so, with the call site as the position (verified on 3.10.0):

import scala.compiletime.constValue
inline def sizeOf[N <: Int]: Int = constValue[N]
@main def run(): Unit = println(sizeOf[Int])
-- [E182] Type Error: d2.scala:4:39
4 |@main def run(): Unit = println(sizeOf[Int])
  |                                       ^^^
  |                        Int is not a constant type; cannot take constValue

The same error appears when the type argument is itself an abstract type parameter. def generic[N <: Int]: Int = sizeOf[N] fails with N is not a constant type; cannot take constValue, because generic is compiled once for all N. An inline method keeps the question open until its own callers supply the type.

codeOf printed 7 for 1 + 2 * 3 because the compiler folds constants before codeOf sees the argument. The second line shows the form of a typed tree, with the type arguments (List.apply[Int]) and the synthetic lambda parameter _$1. Use codeOf for diagnostics and assertion messages, not for round-tripping source text.

Reporting: error and requireConst

error(msg) is a compile-time error with a custom message. The compiler reports it only if the expansion actually contains the call, so it goes in the branch of an inline if or inline match that is the invalid case. With alpine from the program above, a call with an out-of-range grade fails like this:

//> using scala 3.10.0
import scala.compiletime.error
inline def alpine(inline grade: Int): Int =
  inline if grade < 1 || grade > 6 then error("Alpine grade must be between 1 and 6")
  else grade
@main def run(): Unit = println(alpine(9))
-- Error: d1.scala:6:38
6 |@main def run(): Unit = println(alpine(9))
  |                                ^^^^^^^^^
  |                                Alpine grade must be between 1 and 6

The error points at the call site, not at the inline method. alpine(4) compiles and alpine(k) for a runtime k fails with the inline if error shown on Inline, because the condition is not a constant.

requireConst(x) is the shorter check for the case where the argument must be a constant and nothing else is required of it. It is a compile error with the message expected a constant value but found: ... when the argument is not a literal:

//> using scala 3.10.0
import scala.compiletime.requireConst
inline def beds(inline n: Int): Int =
  requireConst(n)
  n
@main def run(args: String*): Unit =
  println(beds(4))
  val k = args.size
  println(beds(k))
-- Error: r1.scala:9:14
9 |  println(beds(k))
  |          ^^^^^^^
  |          expected a constant value but found: k
  |-----------------------------------------------------------------------------
  |Inline stack trace
  |- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
  |This location contains code that was inlined from r1.scala:4
4 |  requireConst(n)
  |               ^

Use requireConst to document that an argument is a literal, for example a buffer size or a port. Use error when the message should explain a business rule such as the valid range of an Alpine grade.

Summoning givens: summonInline, summonFrom, summonAll

An inline method often needs a given for a type that is known only at its call sites. summon[T] is resolved when the inline method is type-checked, where A is abstract, so it cannot work:

trait Show[A]:
  extension (a: A) def show: String
inline def display[A](a: A): String = summon[Show[A]].show(a)
-- [E172] Type Error: d5.scala:4:53
4 |inline def display[A](a: A): String = summon[Show[A]].show(a)
  |                                                     ^
  |No given instance of type Show[A] was found for parameter x of method summon in object Predef

summonInline[T] delays the lookup until the method is expanded. The method compiles, and each caller is checked against the types it uses. Compare with the ordinary alternative, a using Show[A] parameter, which reports the error at the call site, with the name of the parameter. summonInline reports it at the call site too, but adds the inline stack trace:

-- [E172] Type Error: d3.scala:6:39
6 |@main def run(): Unit = println(display(3))
  |                                ^^^^^^^^^^
  |                             No given instance of type Show[Int] was found
  |-----------------------------------------------------------------------------
  |Inline stack trace
  |- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
  |This location contains code that was inlined from d3.scala:5
5 |inline def display[A](a: A): String = summonInline[Show[A]].show(a)
  |                                      ^^^^^^^^^^^^^^^^^^^^^
  • summonInline[T] is for code that must find a given and may be applied to types that are only known later, as when it summons one instance per element of a tuple type (see Type Class Derivation). A plain using parameter is better when the type is known at the definition.

  • summonFrom chooses between alternatives: each case is a pattern over givens, and the first one that finds its given is taken. A final case _ handles the situation with no given at all, so the method never fails to expand.

  • summonAll[T <: Tuple] summons a given for each element of a tuple type and returns a tuple of them.

//> using scala 3.10.0

package trailhead

import scala.compiletime.{constValue, summonAll, summonFrom, summonInline}
import scala.compiletime.ops.int.{+, <, >, S}
import scala.compiletime.ops.int
import scala.compiletime.ops.string
import scala.compiletime.ops.any.==
import scala.compiletime.testing.{typeChecks, typeCheckErrors}

trait Show[A]:
  extension (a: A) def show: String

object Show:
  given Show[Int]:
    extension (a: Int) def show: String = s"int $a"
  given Show[String]:
    extension (a: String) def show: String = s"text $a"

// summonInline: resolved at the call site, where the given exists
inline def display[A](a: A): String = summonInline[Show[A]].show(a)

// summonFrom: branch on whether a given exists
inline def describeIfShowable[A](a: A): String =
  summonFrom:
    case s: Show[A] => s.show(a)
    case _          => s"no Show for ${a.getClass.getSimpleName}"

// summonAll: a tuple of givens
inline def both: (Show[Int], Show[String]) = summonAll[(Show[Int], Show[String])]

// type-level arithmetic on singleton types
type Total = 12 + 30
type Beds = int.*[2, 4]
type Hut = string.+["Refuge ", "Alpina"]
type Big = 5 > 3
type Same = ("a" == "a")
type Next = S[41]

@main def run(): Unit =
  println(display(3))
  println(display("ridge"))
  println(describeIfShowable(4))
  println(describeIfShowable(2.5))
  println(both._1.show(7))
  println(constValue[Total])
  println(constValue[Beds])
  println(constValue[Hut])
  println(constValue[Big])
  println(constValue[Same])
  println(constValue[Next])
  println(typeChecks("val x: Int = \"hi\""))
  println(typeChecks("val x: Int = 1"))
  println(typeCheckErrors("val x: Int = \"hi\"").map(_.message))
int 3
text ridge
int 4
no Show for double
int 7
42
8
Refuge Alpina
true
true
42
false
true
List(Found:    ("hi" : String)
Required: Int)

The first five lines come from the summoning members. describeIfShowable(2.5) has no Show[Double] in scope and so takes the fallback case, while display(2.5) would have been a compile error. The givens are in the companion of Show, so they are found without an import, through the implicit scope.

Prefer using parameters and context bounds (see Givens & Using Clauses) when the type is known where the method is defined. summonFrom is the tool for libraries that adapt to what the user has in scope, and for code that is only generated for the types that support it.

Type-level arithmetic: scala.compiletime.ops

The sub-packages scala.compiletime.ops.int, long, double, string, boolean and any define type-level operations over singleton types. A type such as 12 + 30 is the literal type 42, and constValue turns it into a value. The operations are type aliases that the compiler evaluates when it normalizes a type.

Object Members (as of 3.10.0)

ops.int

Addition, subtraction, multiplication, division, remainder, the comparisons, shifts and bit operations, Abs, Negate, Min, Max, S (successor), ToLong, ToFloat, ToDouble, and ToString (deprecated since 3.2.0, use any.ToString).

ops.long

The same family for Long, plus ToInt.

ops.double

Arithmetic, comparisons, Abs, Negate, Min, Max, ToInt, ToLong, ToFloat.

ops.string

Concatenation, Length, Substring, CharAt, Matches, and the comparisons.

ops.boolean

Negation, and, or and exclusive or.

ops.any

==, !=, IsConst, ToString.

The names in braces conflict with the wildcard import rules: import scala.compiletime.ops.int.{+, *, <} fails with named imports cannot follow wildcard imports, because * in a braced import is the wildcard. The program above avoids this by importing int and writing int.*[2, 4].

More operations, run on 3.10.0:

//> using scala 3.10.0

package trailhead

import scala.compiletime.constValue
import scala.compiletime.ops.{int, string, any}

type Name = "Lake Path"
type NameLength = string.Length[Name]
type Gradient = int.Max[3, 9]
type Described = string.+["Grade ", any.ToString[4]]
type IsLiteral = any.IsConst[Name]
type Valid = string.Matches[Name, "[A-Z][a-z]+ [A-Z][a-z]+"]

@main def run(): Unit =
  println(constValue[NameLength])
  println(constValue[Gradient])
  println(constValue[Described])
  println(constValue[IsLiteral])
  println(constValue[Valid])
9
9
Grade 4
true
true

With int.ToString[4] instead of any.ToString[4] the compiler warns: type ToString in object int is deprecated since 3.2.0: Use compiletime.ops.any.ToString instead.

A practical use of the arithmetic is a type that tracks a size. The Route below records the number of legs in its type, so a mismatch is a type error:

//> using scala 3.10.0

package trailhead

import scala.compiletime.constValue
import scala.compiletime.ops.int.+

// A route with a statically known number of legs
final case class Route[N <: Int](legs: List[String]):
  def ++[M <: Int](other: Route[M]): Route[N + M] = Route(legs ::: other.legs)
  inline def size: Int = constValue[N]

@main def run(): Unit =
  val up = Route[2](List("hut", "col"))
  val down = Route[3](List("col", "lake", "car park"))
  val loop: Route[5] = up ++ down
  println(loop.size)
  println(loop.legs)
5
List(hut, col, col, lake, car park)

Adding the line val wrong: Route[6] = up ++ down fails to compile:

-- [E007] Type Mismatch Error: o1.scala:19:24
19 |  val wrong: Route[6] = up ++ down
   |                        ^^^^^^^^^^
   |                        Found:    trailhead.Route[(5 : Int)]
   |                        Required: trailhead.Route[(6 : Int)]

These operations reduce only on literal types. On an abstract N the expression N + M stays unreduced, and a function that needs the value for such a type needs constValue at a call site where the types are known (the size method above is inline for this reason).

scala.compiletime.testing

scala.compiletime.testing.typeChecks(code: String) and typeCheckErrors(code: String) ask the compiler, at compile time, whether a string of code compiles. They are meant for tests of inline and macro code that must reject bad input: the last three lines of the program above print false, true, and the error message Found: ("hi" : String) / Required: Int. The check runs when the expression is expanded.

Other members

  • scala.compiletime.uninitialized: the replacement of var x: T = _ in classes (the underscore form is deprecated since 3.4). It is not related to inline.

  • deferred: used by deferred givens in traits. See Context Bounds & Deferred Givens.

Comparison with other languages

  • Java has no compile-time operations. Constants are inlined by javac, annotation processors can report errors with a Messager, and generics are erased, so nothing like constValue or erasedValue exists.

  • Kotlin has reified type parameters in inline fun, which make a type argument available at run time, and const val. Compile-time errors from user code need a compiler plugin or KSP. Scala’s error, requireConst and constValue need no plugin.

  • Rust has const evaluation and const fn, and compile_error! as a macro, which is the closest to error.

  • C++ has constexpr, if constexpr (like inline if) and static_assert (like error), plus templates for type-level arithmetic.

  • Scala 2 had no equivalent. Libraries such as shapeless and Singleton-ops encoded literal types and type-level arithmetic with macros and implicits. In Scala 2.13 the ValueOf[T] type class gives the value of a singleton type, and Scala 3 still has ValueOf.

Where the books are outdated

  • shapeless-style implicit-based type-level code (Nat, Witness) is replaced by constValue, ops and match types (see Match Types).

  • Early Scala 3 tutorials use scala.compiletime.ops.int.ToString, which is deprecated since 3.2.0 in favor of scala.compiletime.ops.any.ToString.

  • The macros guide notes that its text was written against 3.0.0-RC3, so some function signatures in older write-ups differ from the current ones. Check the API page for scala.compiletime.