<?xml version="1.0" encoding="UTF-8"?><?xml-stylesheet href="/rss.xsl" type="text/xsl"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>halotukozak</title><description>Scala Software Engineer in an affair with Kotlin</description><link>https://halotukozak.com</link><item><title>Method too large</title><link>https://halotukozak.com/posts/scala-macro-jvm-method-size-limit</link><guid isPermaLink="true">https://halotukozak.com/posts/scala-macro-jvm-method-size-limit</guid><description>How to work around the JVM 64KB method size limit when generating code with Scala 3 macros, using local method chunking to split large generated methods.</description><pubDate>Thu, 30 Oct 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;Scala 3 macro vs JVM method size limit&lt;/h1&gt;
&lt;h2&gt;It&apos;s so big&lt;/h2&gt;
&lt;p&gt;While working on my BSc thesis, I encountered a cryptic Scala compilation error: &quot;method too large&quot;. I use macros to
generate code, and I mean, a lot of code! Think derivations, routing REST API generation or SQL queries.&lt;/p&gt;
&lt;h2&gt;What&apos;s going on?&lt;/h2&gt;
&lt;p&gt;To simplify the example, let’s reduce it to generating a long list. In a real case, though, the list creation can&apos;t be
replaced
with a simple loop, because the elements aren&apos;t predictable.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import scala.quoted.*

inline def someMacro[T](inline n: Int, elem: T): Seq[T] =
  ${ someMacroImpl[T](&apos;{ n }, &apos;{ elem }) }

def someMacroImpl[T: Type](n: Expr[Int], elem: Expr[T])(using Quotes): Expr[Seq[T]] = 
  val longList = Seq.fill(n.valueOrAbort)(elem)
  Expr.ofSeq(longList)

def usage = someMacro(100000, 42) // List(42, 42, 42, ..., 42)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I assume you have some familiarity with macros in
Scala, otherwise &lt;a href=&quot;https://softwaremill.com/scala-3-macros-tips-and-tricks/&quot;&gt;this Software Mill article&lt;/a&gt; can be helpful.
I use them to generate a long list of repeated elements at compile time and then inject it into
the code.&lt;/p&gt;
&lt;p&gt;Quick recap:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;inline&lt;/code&gt; marks the method as an inline one that will be expanded at compile time&lt;/li&gt;
&lt;li&gt;Splice syntax &lt;code&gt;${ ... }&lt;/code&gt; that invokes the macro implementation&lt;/li&gt;
&lt;li&gt;Quote syntax &lt;code&gt;&apos;{ n }&lt;/code&gt; and &lt;code&gt;&apos;{ elem }&lt;/code&gt; that converts values into &lt;code&gt;Expr[T]&lt;/code&gt; representations&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Quotes&lt;/code&gt; parameter provides the context for macro expansion&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Expr.ofSeq&lt;/code&gt; converts a sequence of expressions into an expression representing a sequence&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;When I try to compile this code, I get a not-too-verbose error:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Error while emitting Usage$package$
Method too large: Usage$package$.main ()Lscala/collection/immutable/Seq;
one error found
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This error occurs when a single method exceeds the JVM&apos;s limit of 64KB of bytecode. But how can we bypass this
limitation?&lt;/p&gt;
&lt;h2&gt;Local methods translation&lt;/h2&gt;
&lt;p&gt;JVM doesn&apos;t support local methods directly, so Scala finds a way to compile them. Let&apos;s see how. Consider the following
Scala code:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;def sth() = 
  def local() = ???

  local()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;which is compiled to:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;//decompiled from Usage$package.class

import scala.runtime.Nothing;

public final class Usage$package {
    public static Nothing sth() {
        return Usage$package$.MODULE$.sth();
    }
}

//decompiled from Usage$package$.class
import java.io.Serializable;
import scala.Predef .;
import scala.runtime.ModuleSerializationProxy;
import scala.runtime.Nothing;

public final class Usage$package$ implements Serializable {
    public static final Usage$package$ MODULE$ = new Usage$package$();

    private Usage$package$() {
    }

    private Object writeReplace() {
        return new ModuleSerializationProxy(Usage$package$.class);
    }

    public Nothing sth() {
        return this.local$1();
    }

    private final Nothing local$1() {
        return .MODULE$.$qmark$qmark$qmark();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;As you can see, the local method &lt;code&gt;local&lt;/code&gt; is compiled to a private method &lt;code&gt;local$1&lt;/code&gt; of the enclosing object
&lt;code&gt;Usage$package$&lt;/code&gt;.&lt;/p&gt;
&lt;h2&gt;The chunking solution&lt;/h2&gt;
&lt;p&gt;The story goes that nine women can&apos;t have a baby in a month. In our case, we can&apos;t generate a large method, but we can
split it
into smaller methods.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;inline def someMacro2[T](inline n: Int, elem: T): Seq[T] =
  ${ someMacroImpl2[T](&apos;{ n }, &apos;{ elem }) }

def someMacroImpl2[T: Type](n: Expr[Int], elem: Expr[T])(using quotes: Quotes): Expr[Seq[T]] = 
  import quotes.reflect.*
  import scala.collection.mutable

  val longList = Seq.fill(n.valueOrAbort)(elem)

  val symbol = Symbol.newVal(
    Symbol.spliceOwner,
    Symbol.freshName(&quot;builder&quot;),
    TypeRepr.of[mutable.Builder[T, Seq[T]]],
    Flags.Synthetic,
    Symbol.noSymbol,
  )

  val valDef = ValDef(symbol, Some(&apos;{ Seq.newBuilder[T] }.asTerm))

  val builder = Ref(symbol).asExprOf[mutable.Builder[T, Seq[T]]]

  val additions = longList
    .map(element =&amp;gt;
      &apos;{
        def avoidTooLargerMethod(): Unit = $builder += $element
          avoidTooLargerMethod()
      }.asTerm,
    )
    .toList

  val result = &apos;{ $builder.result() }.asTerm

  Block(valDef :: additions, result).asExprOf[Seq[T]]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The entry point is much the same, but in the implementation, we use&lt;code&gt;quotes.reflect.*&lt;/code&gt; to build the AST manually.&lt;/p&gt;
&lt;p&gt;We create a new Symbol for a mutable builder, then we create a &lt;code&gt;ValDef&lt;/code&gt; to define this builder variable.
Fresh name generation is crucial here to avoid name clashes in the generated code.&lt;/p&gt;
&lt;p&gt;We create a reference to this builder with &lt;code&gt;Ref(symbol)&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;additions&lt;/code&gt; is a list of terms, each representing a local method that adds an element to the builder. We don&apos;t have to
worry about the method names since they are local and will be compiled to unique private methods.&lt;/p&gt;
&lt;p&gt;We then create a &lt;code&gt;Block&lt;/code&gt; that contains the variable definition, all the addition methods, and the final result
retrieval.&lt;/p&gt;
&lt;p&gt;Finally, we convert this block to an expression of type &lt;code&gt;Seq[T]&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;The generated code looks like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;def usage = {
  val builder$macro$1: scala.collection.mutable.Builder[scala.Int, scala.collection.immutable.Seq[scala.Int]] = scala.Seq.newBuilder[scala.Int]

  def avoidTooLargerMethod(): scala.Unit = {
    builder$macro$1.+=(42)
    ()
  }

  avoidTooLargerMethod()

  def `avoidTooLargerMethod₂`(): scala.Unit = {
    builder$macro$1.+=(42)
    ()
  }

  `avoidTooLargerMethod₂`()

  def `avoidTooLargerMethod₃`(): scala.Unit = {
    builder$macro$1.+=(42)
    ()
  }

  `avoidTooLargerMethod₃`()
  builder$macro$1.result()
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And decompiled to Java:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;//
// Source code recreated by IntelliJ IDEA
// (powered by FernFlower decompiler)
//

//decompiled from Usage$package.class

import scala.collection.immutable.Seq;

public final class Usage$package {
    public static Seq&amp;lt;Object&amp;gt; usage() {
        return Usage$package$.MODULE$.usage();
    }
}

//decompiled from Usage$package$.class
import java.io.Serializable;
import scala.collection.immutable.Seq;
import scala.collection.mutable.Builder;
import scala.package.;
import scala.runtime.BoxesRunTime;
import scala.runtime.ModuleSerializationProxy;

public final class Usage$package$ implements Serializable {
    public static final Usage$package$ MODULE$ = new Usage$package$();

    private Usage$package$() {
    }

    private Object writeReplace() {
        return new ModuleSerializationProxy(Usage$package$.class);
    }

    public Seq&amp;lt;Object&amp;gt; usage() {
        Builder var1 = .MODULE$.Seq().newBuilder();
        this.avoidTooLargerMethod$1(var1);
        this.avoidTooLargerMethod$2(var1);
        this.avoidTooLargerMethod$3(var1);
        return (Seq) var1.result();
    }

    private final void avoidTooLargerMethod$1(final Builder builder$macro$1$1) {
        builder$macro$1$1.$plus$eq(BoxesRunTime.boxToInteger(42));
    }

    private final void avoidTooLargerMethod$2(final Builder builder$macro$1$2) {
        builder$macro$1$2.$plus$eq(BoxesRunTime.boxToInteger(42));
    }

    private final void avoidTooLargerMethod$3(final Builder builder$macro$1$3) {
        builder$macro$1$3.$plus$eq(BoxesRunTime.boxToInteger(42));
    }
}

&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Case closed&lt;/h2&gt;
&lt;p&gt;Back to working on my thesis now. We&apos;ll see if I find time to write something here again.&lt;/p&gt;
&lt;p&gt;Written by me[^*], no AI.&lt;/p&gt;
&lt;p&gt;[^*]: The English text and grammar were kindly reviewed and corrected by my friend Mateusz.&lt;/p&gt;
</content:encoded><author>Bartłomiej Kozak</author></item><item><title>Type-Safe Access to Method Parameter Defaults in Scala</title><link>https://halotukozak.com/posts/scala-macro-defaults</link><guid isPermaLink="true">https://halotukozak.com/posts/scala-macro-defaults</guid><description>How to extract method parameter defaults at compile time in Scala 3 using macros, the Selectable trait, and computed field names for full type safety.</description><pubDate>Sat, 10 Jan 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;The Problem: Hidden Defaults&lt;/h2&gt;
&lt;p&gt;Someone &lt;a href=&quot;https://contributors.scala-lang.org/t/how-to-avoid-repeating-method-parameter-defaults/7330&quot;&gt;asked on the Scala contributors forum&lt;/a&gt;
how to access method parameter defaults.&lt;/p&gt;
&lt;p&gt;Default parameters are convenient syntax sugar, but sometimes you need to access them programmatically. Imagine you&apos;re building a configuration builder and want to include the default values in help
text:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;case class Database(host: String = &quot;localhost&quot;, port: Int = 5432, name: String = &quot;mydb&quot;)

// You want to generate documentation:
// &quot;host&quot; (default: localhost)
// &quot;port&quot; (default: 5432)
// &quot;name&quot; (default: mydb)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We&apos;d like to pass a method reference and get back a &lt;code&gt;Map[String, Any]&lt;/code&gt; containing all parameter defaults.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@main
def main(): Unit =
  assert(defaults(method) == Map(&quot;x&quot; -&amp;gt; 10, &quot;y&quot; -&amp;gt; &quot;two&quot;))

def method(x: Int = 10, y: String = &quot;two&quot;) = ???
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Prerequisites&lt;/h2&gt;
&lt;p&gt;With this goal in mind, let&apos;s ensure you have the right background knowledge before diving into the implementation. I assume you&apos;re comfortable with Scala and have some familiarity with Scala 3 macros. If you need a refresher on Scala 3 macros, I recommend &lt;a href=&quot;https://softwaremill.com/scala-3-macros-tips-and-tricks/&quot;&gt;this Software Mill article&lt;/a&gt; first.&lt;/p&gt;
&lt;p&gt;I use Scala 3.8.0-RC4 for this example.&lt;/p&gt;
&lt;h2&gt;How Scala Encodes Defaults&lt;/h2&gt;
&lt;p&gt;Since the JVM doesn&apos;t natively support default parameters, Scala gets creative. When you define a method with defaults, the compiler generates companion methods in the class for each defaulted parameter. These methods are named with a special encoding: &lt;code&gt;$default$N,&lt;/code&gt; where &lt;code&gt;N&lt;/code&gt; is the parameter position (1-indexed).&lt;/p&gt;
&lt;p&gt;So &lt;code&gt;def method(x: Int = 10, y: String = &quot;two&quot;) = ???&lt;/code&gt; actually creates:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;//
// Source code recreated by IntelliJ IDEA
// (powered by FernFlower decompiler)
//
// decompiled from main$package$.class
import java.io.Serializable;
import scala.runtime.Nothing;
import scala.runtime.Scala3RunTime.;

public final class main$package$ implements Serializable {
    // ... other generated code ...

    public Nothing method(final int x, final String y) {
        return scala.Predef..MODULE$.$qmark$qmark$qmark();
    }

    public int method$default$1() {
        return 10;
    }

    public String method$default$2() {
        return &quot;two&quot;;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Scala compiler on call site generates calls to these &lt;code&gt;$default$N&lt;/code&gt; methods when parameters are omitted. Understanding
this encoding is crucial because our macro will hunt for these hidden &lt;code&gt;$default$N&lt;/code&gt; methods. With that foundation, let&apos;s
build the first version.&lt;/p&gt;
&lt;h2&gt;Step 1: Simple Version (Map-Based)&lt;/h2&gt;
&lt;p&gt;Let&apos;s move to the code. We will use the &lt;code&gt;quotes.reflect&lt;/code&gt; to look under the hood of the method definition at compile
time. Our macro does three things:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;extracts the symbol of method we&apos;re analyzing,&lt;/li&gt;
&lt;li&gt;finds all generated &lt;code&gt;$default$N&lt;/code&gt; methods,&lt;/li&gt;
&lt;li&gt;builds a map connecting parameter names to their default values.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Here&apos;s the code with detailed comments:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;inline def defaults[T](inline fun: T): Map[String, Any] = ${ defaultsImpl(&apos;{ fun }) }

def defaultsImpl[T: Type](expr: Expr[T])(using quotes: Quotes): Expr[Map[String, Any]] =
  import quotes.reflect.*
  
  // Extract the method reference from the inline parameter
  // Lambda(...) = the anonymous function wrapper, Apply(...) = the function call
  val Lambda(_, Apply(method, _)) = expr
    .asTerm                                     // convert Expr[?] to the Reflection Term
    .underlying                                 // get the real method behind the inline wrapper
    .runtimeChecked                             // disable the exhaustiveness check (we assume happy path here)
  
  // Get the method&apos;s symbol (compile-time metadata about the method)
  val methodSymbol = method.symbol
  
  
  // Collect all parameter names in order
  val paramNames = methodSymbol.paramSymss // &quot;parameter symbol sequences&quot; (handles grouped parameters)
    .flatten                               // combine all groups into one list
    .map(_.name)                           // extract just the names
    .toVector                              // convert to Vector for indexed access (we need positions later)
  
  // Build the prefix for hidden default methods
  val prefix = methodSymbol.name + &quot;$default$&quot;
  
  // Find all hidden default methods and map them to parameter names
  val defaults = methodSymbol.owner.methodMembers
    .collect:
      case m if m.name.startsWith(prefix) =&amp;gt;            // Only collect methods starting with our prefix (e.g., &quot;greet$default$1&quot;)
        val position = m.name.stripPrefix(prefix).toInt // Extract the position number
        paramNames(position - 1) -&amp;gt; Ref(m)              // Ref(m) creates an expression that can call this method at runtime
    
    // Convert each (paramName, methodRef) pair to an expression tuple
    // This prepares them for splicing into the macro result
    .map: (k, v) =&amp;gt;
      Expr.ofTuple((Expr(k), v.asExpr)) //Expr lifts String into Expr[String], asExpr converts Term to Expr[?]

  // Varargs(...) converts List[Expr[T]] to Expr[List[T]]
  &apos;{ ${ Varargs(defaults) }.toMap }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This macro successfully extracts defaults into a Map. Let&apos;s see it in action:&lt;/p&gt;
&lt;h3&gt;Testing the First Version&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;def greet(name: String = &quot;samsepi0l&quot;, number: Int = 43) = s&quot;$greeting $number&quot;

@main
def main(): Unit =
  val d = defaults(greet)
  println(d) // Map(&quot;name&quot; -&amp;gt; &quot;samsepi0l&quot;, &quot;number&quot; -&amp;gt; &quot;43&quot;)

  // Access via string key (but no type safety)
  val name = d(&quot;name&quot;)
  val number: Int = d(&quot;number&quot;).asInstanceOf[Int] // type is Any, must cast
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Why We Need Better: Type-Safety&lt;/h2&gt;
&lt;p&gt;Our first solution works, but it has drawbacks: we can get a runtime error when key does not exist and the type of value
is always &lt;code&gt;Any&lt;/code&gt;.&lt;/p&gt;
&lt;h2&gt;Step 2: Type-Safe Version with Computed Field Names&lt;/h2&gt;
&lt;p&gt;To solve these issues, we&apos;ll use two powerful Scala 3 features: &lt;code&gt;Selectable&lt;/code&gt; and &lt;code&gt;Computed Field Names&lt;/code&gt;.&lt;/p&gt;
&lt;h3&gt;What is Selectable?&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;Selectable&lt;/code&gt; is a trait that enables dynamic access to the refined fields.
The &lt;code&gt;selectDynamic&lt;/code&gt; method takes a field name and returns the value associated with that name.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class DynamicConfig extends Selectable:
  val x = 10
  val y = &quot;hello&quot;

  def selectDynamic(name: String): Any = name match
    case &quot;x&quot; =&amp;gt; x
    case &quot;y&quot; =&amp;gt; y
    case _ =&amp;gt; throw new NoSuchFieldException(s&quot;No such field: $name&quot;)

import scala.reflect.Selectable.reflectiveSelectable

val d: Selectable { val x: Int; val y: String; def selectDynamic(name: String): Any } = new DynamicConfig
val x: Int = d.x    // works
val y: String = d.y // works
val z = d.z         // compile error: no z field
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This solution is type-safe because the compiler knows the types of &lt;code&gt;x&lt;/code&gt; and &lt;code&gt;y&lt;/code&gt; at compile time, but it requires us to
import implicit conversion that turns a value into a &lt;code&gt;Selectable&lt;/code&gt; such that structural selections are performed on that
value.&lt;/p&gt;
&lt;h3&gt;What are Computed Field Names?&lt;/h3&gt;
&lt;p&gt;This basic Selectable gives us dynamic access, but lacks type safety. That&apos;s where computed field names come in. They
let us encode field types at compile-time.
The &lt;code&gt;Selectable&lt;/code&gt; trait now can have a &lt;code&gt;Fields&lt;/code&gt; type member that can be instantiated to a named tuple.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;trait Selectable:
  type Fields &amp;lt;: NamedTuple.AnyNamedTuple
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If &lt;code&gt;Fields&lt;/code&gt; is instantiated in a subclass of &lt;code&gt;Selectable&lt;/code&gt; to some named tuple type, then the available fields and their
types will be defined by that type. For example, if &lt;code&gt;Fields&lt;/code&gt; is defined as &lt;code&gt;(x: Int, y: String)&lt;/code&gt;, then the &lt;code&gt;Selectable&lt;/code&gt;
instance will have fields &lt;code&gt;x&lt;/code&gt; of
type &lt;code&gt;Int&lt;/code&gt; and &lt;code&gt;y&lt;/code&gt; of type &lt;code&gt;String&lt;/code&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class DynamicConfig extends Selectable:
  type Fields = (x: Int, y: String)
  
  private val data = Map(&quot;x&quot; -&amp;gt; 10, &quot;y&quot; -&amp;gt; &quot;hello&quot;)
  
  def selectDynamic(name: String): Any = data(name)

val d = new DynamicConfig
val x: Int = d.x      // works
val y: String = d.y   // works
// val z = d.z        // compile error: no z field
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;With &lt;code&gt;Selectable&lt;/code&gt; and &lt;code&gt;Computed Field Names&lt;/code&gt; understood, let&apos;s enhance our macro to automatically generate the &lt;code&gt;Fields&lt;/code&gt;
type for any method&apos;s defaults.&lt;/p&gt;
&lt;h2&gt;The Type-Safe Implementation&lt;/h2&gt;
&lt;p&gt;We&apos;re gonna enhance our macro to build a &lt;code&gt;Fields&lt;/code&gt; type member that is a named tuple of parameter names and types.&lt;/p&gt;
&lt;p&gt;Example: for &lt;code&gt;method(x: Int = 10, y: String = &quot;hi&quot;)&lt;/code&gt; names should become: &lt;code&gt;(&quot;x&quot;, &quot;y&quot;)&lt;/code&gt;, types should become:
&lt;code&gt;(Int, String)&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;The result should be &lt;code&gt;(x: Int, y: String)&lt;/code&gt; (which is a syntax sugar for &lt;code&gt;NamedTuple[(&quot;x&quot;, &quot;y&quot;), (Int, String)]&lt;/code&gt;).&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import scala.NamedTuple.{AnyNamedTuple, NamedTuple}
import scala.quoted.*

// &apos;transparent&apos; is key: type information flows through to the caller
// Without it, type refinements would be hidden inside the return type
transparent inline def defaults[T](inline fun: T): DefaultsExtractor = 
  ${ defaultsImpl(&apos;{ fun }) }

def defaultsImpl[T: Type](expr: Expr[T])(using quotes: Quotes): Expr[DefaultsExtractor] =
  import quotes.reflect.*
  
  // Extract method symbol (same as before)
  val Lambda(_, Apply(method, _)) = expr.asTerm.underlying.runtimeChecked
  val methodSymbol = method.symbol
  val prefix = methodSymbol.name + &quot;$default$&quot;
  val paramNames = methodSymbol.paramSymss.flatten.map(_.name).toVector

  // Build runtime map (same as before)
  val defaults = methodSymbol.owner.methodMembers
    .collect:
      case m if m.name.startsWith(prefix) =&amp;gt;
        paramNames(m.name.stripPrefix(prefix).toInt - 1) -&amp;gt; Ref(m)

  // Convert to expression that builds a Map at runtime
  val defaultsExpr =
    val list = Expr.ofSeq:
      defaults.map: (name, term) =&amp;gt;
        Expr.ofTuple((Expr(name), term.asExpr))
        
    &apos;{ $list.toMap }

  val fieldsType = TypeRepr
    .of[NamedTuple]
    .appliedTo:
      defaults
        // Start with empty tuples: () and ()
        .foldLeft((TypeRepr.of[EmptyTuple], TypeRepr.of[EmptyTuple])):
          case ((accNames, accTypes), (paramName, term)) =&amp;gt;
            // For each (paramName, defaultMethod) pair, build up both tuples
            
            // Build the names tuple by cons-ing the parameter name. *: is Scala&apos;s tuple cons operator (like :: for lists at type level)
            val newNames = TypeRepr.of[*:].appliedTo(List(
              ConstantType(StringConstant(paramName)), // convert a string to a type
              accNames                                 // previously accumulated names
            ))
            
            //Similarly for types
            val newTypes = TypeRepr.of[*:].appliedTo(List(
              term.tpe,                               // the type of the default value&apos;s expression
              accTypes                                // previously accumulated types
            ))
            
            (newNames, newTypes)
        // After the fold, convert the (names, types) pair to a List
        // for passing to .appliedTo
        .toList

  // Pattern match to &quot;escape&quot; the type into the macro&apos;s context
  // asType match converts TypeRepr to a compile-time Type
  fieldsType.asType match
      // Pattern introduces a type variable &apos;fields&apos; capturing what we built (with bounds to AnyNamedTuple)
    case &apos;[ type fields &amp;lt;: AnyNamedTuple; fields ] =&amp;gt;
      &apos;{
        new DefaultsExtractor($defaultsExpr):
          type Fields = fields  // Refine the abstract type to our computed one
      }

// The extractor class that makes everything work
sealed class DefaultsExtractor(defaults: Map[String, Any]) extends Selectable:
  // This type member will be refined by each macro invocation
  // to the exact NamedTuple of defaults for that specific method
  type Fields &amp;lt;: AnyNamedTuple

  // The magic method: enables d.x, d.y syntax
  // When you write d.x, the compiler expands it to:
  // selectDynamic(&quot;x&quot;).asInstanceOf[T]
  // where T is the type of &quot;x&quot; from the Fields type member
  final def selectDynamic(name: String): Any = defaults(name)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This code works because of a crucial detail: the &lt;code&gt;transparent&lt;/code&gt; keyword.&lt;/p&gt;
&lt;h3&gt;Why transparent inline def?&lt;/h3&gt;
&lt;p&gt;The &lt;code&gt;transparent&lt;/code&gt; keyword means the inline function doesn&apos;t create a type boundary. Type refinements (the
&lt;code&gt;type Fields = ...&lt;/code&gt; part) flow through to the caller&apos;s context. Without it, the detailed field information would be
hidden inside the &lt;code&gt;DefaultsExtractor&lt;/code&gt;&apos;s type, and callers couldn&apos;t see which fields are available.&lt;/p&gt;
&lt;h2&gt;Edge Cases &amp;amp; Production Considerations&lt;/h2&gt;
&lt;p&gt;This implementation handles the happy path. It may crash on varargs, implicits, nested functions or in combination with
access modifiers.&lt;/p&gt;
&lt;h2&gt;Case closed&lt;/h2&gt;
&lt;p&gt;You can get the code on &lt;a href=&quot;https://gist.github.com/halotukozak/a5e439309b3c2d010d93f38d9fd3eec7&quot;&gt;GitHub&lt;/a&gt;.
Back to sleep now. Would you like me to write something here again?&lt;/p&gt;
&lt;h2&gt;References&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.scala-lang.org/scala3/reference/metaprogramming/macros.html&quot;&gt;Scala 3 Macro Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.scala-lang.org/scala3/reference/other-new-features/named-tuples.html&quot;&gt;Named Tuples&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.scala-lang.org/scala3/reference/other-new-features/named-tuples.html#computed-field-names&quot;&gt;Computed Field Names&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://scala-lang.org/api/3.x/scala/Selectable.html&quot;&gt;Selectable Trait&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://contributors.scala-lang.org/t/how-to-avoid-repeating-method-parameter-defaults/7330&quot;&gt;Original Forum Post&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded><author>Bartłomiej Kozak</author></item><item><title>Scala Type Class Derivation with (almost) no macros</title><link>https://halotukozak.com/posts/scala-type-class-derivation-with-no-macros</link><guid isPermaLink="true">https://halotukozak.com/posts/scala-type-class-derivation-with-no-macros</guid><description>Automating type class derivation in Scala 3 using the Mirror API and compiletime utilities — replacing complex Scala 2 macro code with readable, type-safe compile-time logic.</description><pubDate>Sun, 01 Feb 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;Boilerplate in Type Class Instances&lt;/h2&gt;
&lt;p&gt;Back in the Scala 2 days, type class derivation was a &quot;dark art&quot;. Automating serialization — take AVSystem’s GenCodecs, for instance — required wrestling with the Reflection API or Shapeless. You’d easily end up with 1,200 lines of low-level macro code just to handle the boilerplate of peering into class structures.&lt;/p&gt;
&lt;p&gt;With Scala 3, the approach has changed significantly.
We can achieve the same (and more) using the native &lt;code&gt;Mirror&lt;/code&gt; API and &lt;code&gt;scala.compiletime&lt;/code&gt; utilities.
Most of the process is now &quot;just Scala code&quot; that runs at compile-time.
We still need a &lt;em&gt;tiny&lt;/em&gt; bit of macro magic for advanced features like annotation extraction, but we can get 95% of the way there with standard, readable code.&lt;/p&gt;
&lt;h2&gt;The Foundation: GenCodec&lt;/h2&gt;
&lt;p&gt;We assume a robust serialization API already exists.
Our goal isn&apos;t to write the serializer itself, but to automate instance creation for complex types.
The &lt;code&gt;GenCodec&lt;/code&gt; trait defines how to read and write a type &lt;code&gt;T&lt;/code&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;trait GenCodec[T]:
  def read(input: Input): T
  def write(output: Output, value: T): Unit

object GenCodec:
  // Basic primitives are already defined
  given GenCodec[Boolean] = ???
  given GenCodec[Char] = ???
  given GenCodec[Byte] = ???
  // ... other basic types

  given [C[X] &amp;lt;: Seq[X], T: GenCodec] =&amp;gt; GenCodec[C[T]] = ???
  // ... other collections

  // Internal &quot;low-level&quot; derivation methods that handle the 
  // actual serialization logic for different class shapes:
  private def deriveSingleton[T &amp;lt;: Singleton](typeName: String, value: T): GenCodec[T] = ???

  private def deriveSum[T](
    typeName: String,
    instances: Array[GenCodec[?]],
    fieldNames: Array[String],
    classes: Array[Class[?]],
  ): GenCodec[T] = ???

  private def deriveProduct[T &amp;lt;: Product](
    typeName: String,
    instances: Array[GenCodec[?]],
    fieldNames: Array[String],
  ): GenCodec[T] = ???
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;The Power of Mirrors&lt;/h2&gt;
&lt;p&gt;Scala 3 introduces &lt;code&gt;Mirror&lt;/code&gt;, a type class synthesized by the compiler that provides a compile-time representation of a class&apos;s structure.
It lets us decompose types into their components without runtime reflection.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Mirror.ProductOf[T]&lt;/strong&gt;: For case classes (product types).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Mirror.SumOf[T]&lt;/strong&gt;: For enums and sealed traits (sum types).&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Our first attempt at a &lt;code&gt;derived&lt;/code&gt; method uses these mirrors to extract type information:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;inline private def derived[T]: GenCodec[T] = compiletime.summonFrom:
  case m: Mirror.Of[T] =&amp;gt;
    val typeName = compiletime.constValue[m.MirroredLabel]
    val instances =
      compiletime.constValueTuple[Tuple.Map[m.MirroredElemTypes, GenCodec]].toArray.map(_.asInstanceOf[GenCodec[?]])
    val fieldNames = compiletime.constValueTuple[m.MirroredElemLabels].toArray.map(_.asInstanceOf[String])

    m match
      case m: Mirror.ProductOf[T] =&amp;gt;
        deriveProduct(typeName, instances, fieldNames)
      case m: Mirror.SumOf[T] =&amp;gt;
        val classes = compiletime
          .summonAll[Tuple.Map[m.MirroredElemTypes, ClassTag]]
          .toArray
          .map(_.asInstanceOf[ClassTag[?]].runtimeClass)

        deriveSum(typeName, instances, fieldNames, classes)
  case _ =&amp;gt; compiletime.error(&quot;Cannot derive GenCodec&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;compiletime.summonFrom&lt;/strong&gt; is a compile-time conditional that pattern matches on available &lt;em&gt;givens&lt;/em&gt; (implicits) in the current scope. It tries each case in order and uses the first one that successfully resolves. This is the heart of flexible derivation.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;compiletime.constValue&lt;/strong&gt; extracts a literal value (like a String &lt;code&gt;&quot;MR. ROBOT&quot;&lt;/code&gt;) from the type level to the value level. At compile-time, &lt;code&gt;m.MirroredLabel&lt;/code&gt; is a &lt;strong&gt;singleton type&lt;/strong&gt; containing the class name: &lt;code&gt;constValue&lt;/code&gt; materializes it as a runtime &lt;code&gt;String&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;compiletime.constValueTuple&lt;/strong&gt; converts a tuple of types into a tuple of values. For example, field labels in a &lt;code&gt;Mirror&lt;/code&gt; exist as a tuple of singleton string types like &lt;code&gt;(&quot;id&quot;, &quot;name&quot;)&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Tuple.Map&lt;/strong&gt; is a type-level operation that transforms each element of a tuple. If you have a tuple of types &lt;code&gt;(Int, String)&lt;/code&gt; and apply &lt;code&gt;Tuple.Map[..., GenCodec]&lt;/code&gt;, the compiler computes the type &lt;code&gt;(GenCodec[Int], GenCodec[String])&lt;/code&gt;.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Note that we use &lt;code&gt;.toArray.map(_.asInstanceOf[GenCodec[?]])&lt;/code&gt; instead of a direct cast to &lt;code&gt;.asInstanceOf[Array[GenCodec[?]]]&lt;/code&gt;.
In the JVM, arrays are reified, so we cannot cast Array[Any] directly to Array[GenCodec[?]].
Instead, we convert to an array of &lt;code&gt;Any&lt;/code&gt; and then map each element to the desired type.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;Recursive Derivation&lt;/h2&gt;
&lt;p&gt;The simple version works when all fields and subclasses already have &lt;code&gt;GenCodec&lt;/code&gt; instances, but we want to derive GenCodecs for subclasses recursively.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;inline private def summonInstances[Elems &amp;lt;: Tuple](
  summonAllowed: Boolean,
  deriveAllowed: Boolean,
): Tuple =
  inline compiletime.erasedValue[Elems] match
    case _: (elem *: elems) =&amp;gt;
      val elemCodec = compiletime.summonFrom:
        case codec: GenCodec[`elem`] if summonAllowed =&amp;gt; codec
        case _ if deriveAllowed =&amp;gt; derived[elem]

      elemCodec *: summonInstances[elems](summonAllowed, deriveAllowed)
    case _: EmptyTuple =&amp;gt; EmptyTuple
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;compiletime.erasedValue&lt;/strong&gt; lets us pattern match on a type without having a runtime value of that type. It&apos;s a no-op at runtime that tells the compiler: &quot;I want to inspect the structure of this type as if I had a value of it.&quot;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;inline match&lt;/strong&gt; is a powerful Scala 3 feature that ensures the pattern matching is fully expanded at compile-time. Unlike a regular match, &lt;code&gt;inline match&lt;/code&gt; is resolved by the compiler; the cases that don&apos;t match are literally discarded from the generated bytecode, leaving only the branch that corresponds to the actual type.&lt;/p&gt;
&lt;p&gt;Now we update &lt;code&gt;derived&lt;/code&gt; to use recursive summoning:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;inline private def derived[T]: GenCodec[T] = compiletime.summonFrom:
    case m: Mirror.Of[T] =&amp;gt;
      val typeName = compiletime.constValue[m.MirroredLabel]
      val fieldNames = compiletime.constValueTuple[m.MirroredElemLabels].toArray.map(_.asInstanceOf[String])

      m match
        case m: Mirror.ProductOf[T] =&amp;gt; 
          val instances = summonInstances[m.MirroredElemTypes](summonAllowed = true, deriveAllowed = false).toArray.map(_.asInstanceOf[GenCodec[?]])
          deriveProduct(typeName, instances, fieldNames)
        case m: Mirror.SumOf[T] =&amp;gt;
          val instances = summonInstances[m.MirroredElemTypes](summonAllowed = true, deriveAllowed = true).toArray.map(_.asInstanceOf[GenCodec[?]])
          val classes = compiletime
            .summonAll[Tuple.Map[m.MirroredElemTypes, ClassTag]]
            .toArray
            .map(_.asInstanceOf[ClassTag[?]].runtimeClass)

          deriveSum(typeName, instances, fieldNames, classes)
    case _ =&amp;gt; compiletime.error(&quot;Cannot derive GenCodec&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;For product types, we only summon existing instances (no deriving); for sum types, we allow deriving subclasses recursively.&lt;/p&gt;
&lt;h2&gt;Handling Cycles&lt;/h2&gt;
&lt;p&gt;Recursive data structures (like a &lt;code&gt;Node&lt;/code&gt; pointing to another &lt;code&gt;Node&lt;/code&gt;) cause infinite recursion at compile-time because the compiler tries to generate a &lt;code&gt;GenCodec[Node]&lt;/code&gt; while it&apos;s still in the middle of generating a &lt;code&gt;GenCodec[Node]&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;To break this cycle, we introduce a &lt;code&gt;Deferred&lt;/code&gt; wrapper. We place it in the implicit scope &lt;em&gt;before&lt;/em&gt; we start the derivation process:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;final class Deferred[T] extends GenCodec[T]:
  var underlying: GenCodec[T] = null.asInstanceOf[GenCodec[T]]

  def read(input: Input): T = underlying.read(input)
  def write(output: Output, value: T): Unit = underlying.write(output, value)

inline def derived[T]: GenCodec[T] =
  given deferred: Deferred[T] = new Deferred[T]

  val underlying = unsafeDerived[T] // our renamed old &apos;derived&apos;
  deferred.underlying = underlying
  underlying
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;given deferred: Deferred[T]&lt;/code&gt; makes the instance available during the derivation of &lt;code&gt;T&lt;/code&gt; itself.
When the compiler encounters a field of type &lt;code&gt;T&lt;/code&gt; (the recursive step), it will find this &lt;code&gt;given&lt;/code&gt; instead of trying to call &lt;code&gt;derived[T]&lt;/code&gt; again.
Later, we fill in the &lt;code&gt;underlying&lt;/code&gt; field with the fully constructed codec.
This works because we use a &lt;code&gt;var&lt;/code&gt; - the instance is &quot;lazily&quot; completed after the skeleton is created.&lt;/p&gt;
&lt;h2&gt;The &quot;Almost&quot; Part&lt;/h2&gt;
&lt;p&gt;Sometimes we need to derive codecs for singleton types (like &lt;code&gt;object&lt;/code&gt; instances). For individual values, we can use the &lt;code&gt;ValueOf&lt;/code&gt; type class.
The &lt;code&gt;ValueOf[T]&lt;/code&gt; gives us the runtime value of a singleton type. But how do we get its name? For that, we need our first &quot;real&quot; macro.&lt;/p&gt;
&lt;h3&gt;Type Names via Opaque Types&lt;/h3&gt;
&lt;p&gt;When &lt;code&gt;Mirror&lt;/code&gt; isn&apos;t available (e.g., for primitives or singletons), we still need the type&apos;s name for serialization.&lt;/p&gt;
&lt;p&gt;We use an opaque type to carry the type name without runtime overhead:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;opaque type TypeName[T] &amp;lt;: String = String

object TypeName:
  inline given [T] =&amp;gt; TypeName[T] = ${ deriveImpl[T] }

  private def deriveImpl[T: Type](using quotes: Quotes): Expr[TypeName[T]] =
    import quotes.reflect.*
    Expr(TypeRepr.of[T].show)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This macro extracts the fully-qualified type name at compile-time and encodes it as a string literal.
The &lt;code&gt;opaque type&lt;/code&gt; ensures it&apos;s treated distinctly from &lt;code&gt;String&lt;/code&gt; for type safety, but at runtime it&apos;s just a string.&lt;/p&gt;
&lt;p&gt;Finally, we integrate this into our &lt;code&gt;unsafeDerived&lt;/code&gt; method:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;inline private def unsafeDerived[T]: GenCodec[T] = compiletime.summonFrom:
    case m: Mirror.Of[T] =&amp;gt; // as before
    case v: ValueOf[T] =&amp;gt;
      deriveSingleton(compiletime.summonInline[TypeName[T]], v.value)
    case _ =&amp;gt; compiletime.error(&quot;Cannot derive GenCodec&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Handling Annotations&lt;/h3&gt;
&lt;p&gt;Scala 2 &lt;code&gt;scala-commons &lt;/code&gt;supported custom names via an &lt;code&gt;@name&lt;/code&gt; annotation.
We use Scala 3&apos;s &lt;code&gt;RefiningAnnotation&lt;/code&gt; which are more &quot;sticky&quot; than normal ones. They are conceptually kept around when normal refinements would also not be stripped away.
A small macro helps us check for the presence of an annotation at compile-time:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class name(val value: String) extends RefiningAnnotation

@implicitNotFound(&quot;${T} is not annotated with ${A}&quot;)
opaque type HasAnnotation[T, A &amp;lt;: RefiningAnnotation] = A

object HasAnnotation:
  extension [T, A &amp;lt;: RefiningAnnotation](has: HasAnnotation[T, A]) def value: A = has

  inline given [T, A &amp;lt;: RefiningAnnotation] =&amp;gt; HasAnnotation[T, A] = ${ materializeImpl[T, A] }

  private def materializeImpl[T: Type, A &amp;lt;: RefiningAnnotation: Type](using quotes: Quotes): Expr[HasAnnotation[T, A]] =
    import quotes.reflect.*
    TypeRepr.of[T].typeSymbol.getAnnotation(TypeRepr.of[A].typeSymbol) match
      case Some(annot) =&amp;gt; annot.asExprOf[A]
      case _ =&amp;gt; report.errorAndAbort(s&quot;${Type.show[T]} is not annotated with ${Type.show[A]}&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If a type has the annotation, &lt;code&gt;HasAnnotation[T, A]&lt;/code&gt; gives us the annotation object itself.
If not, the macro aborts at compile-time with a clear error.&lt;/p&gt;
&lt;p&gt;By combining &lt;code&gt;HasAnnotation&lt;/code&gt; with &lt;code&gt;summonFrom&lt;/code&gt;, we can check for annotations and fall back to defaults:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class name(val value: String) extends scala.annotation.RefiningAnnotation

inline private def constName[T](inline fallback: String) = compiletime.summonFrom:
  case h: HasAnnotation[T, `name`] =&amp;gt; h.value
  case _ =&amp;gt; fallback
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The backticks around &lt;code&gt;name&lt;/code&gt; are crucial - they tell the compiler to treat &lt;code&gt;name&lt;/code&gt; as a type name, not a type binding.
Without them, the compiler would think &lt;code&gt;name&lt;/code&gt; is a variable in scope, leading to errors.&lt;/p&gt;
&lt;p&gt;For field names, we need to check each field&apos;s type separately:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;inline private def constNames[Tup &amp;lt;: Tuple]: Tuple = inline compiletime.erasedValue[Tup] match
    case _: ((label, tpe) *: tail) =&amp;gt;
      val head = constName[tpe](compiletime.constValue[label].asInstanceOf[String])
      head *: constNames[tail]
    case _: EmptyTuple =&amp;gt; EmptyTuple
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We zip labels with types, then for each field, check if its type has a &lt;code&gt;@name&lt;/code&gt; annotation. If yes, use that; otherwise use the label.&lt;/p&gt;
&lt;p&gt;Now integrate this into &lt;code&gt;unsafeDerived&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;inline private def unsafeDerived[T]: GenCodec[T] = compiletime.summonFrom:
  case m: Mirror.Of[T] =&amp;gt;
    val typeName = constName[T](compiletime.constValue[m.MirroredLabel])
    val fieldNames = constNames[Tuple.Zip[m.MirroredElemLabels, m.MirroredElemTypes]].toArray.map(_.asInstanceOf[String])

    // ... as before, using typeName and fieldNames
  case v: ValueOf[T] =&amp;gt;
    val typeName = constName[T](compiletime.summonInline[TypeName[T]])
    deriveSingleton(typeName, v.value)
  case _ =&amp;gt; compiletime.error(&quot;Cannot derive GenCodec&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Summary&lt;/h2&gt;
&lt;p&gt;We&apos;ve moved from 1,200 lines of complex reflection to a few dozen lines of type-safe, compile-time Scala 3 code.
While we still use macros for annotation processing, the &quot;heavy lifting&quot; is now handled by the compiler&apos;s native understanding
of types.&lt;/p&gt;
&lt;p&gt;I&apos;m defending my bachelor&apos;s thesis the day after tomorrow. I hope I won&apos;t stop blogging after that. If you enjoyed this post, consider following me on &lt;a href=&quot;https://www.linkedin.com/in/halotukozak/&quot;&gt;LinkedIn&lt;/a&gt; for updates on future content!&lt;/p&gt;
</content:encoded><author>Bartłomiej Kozak</author></item><item><title>scala-cli E2E Repo Setup</title><link>https://halotukozak.com/posts/scala-cli-e2e</link><guid isPermaLink="true">https://halotukozak.com/posts/scala-cli-e2e</guid><description>A complete guide to setting up a Scala library project with scala-cli — formatting, testing, code coverage, Maven Central publishing, GitHub Actions CI, and Scaladoc deployment.</description><pubDate>Mon, 02 Mar 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;Why scala-cli?&lt;/h2&gt;
&lt;p&gt;Every time I start a new Scala project, I find myself googling the same setup steps — formatting, testing, coverage,
publishing, documentation. The traditional tools don&apos;t help: &lt;code&gt;sbt&lt;/code&gt; has a steep learning curve, &lt;code&gt;Mill&lt;/code&gt; is fast but
struggles with IntelliJ support. &lt;code&gt;scala-cli&lt;/code&gt; bridges this gap — as simple as a script but as capable as a build tool,
with first-class IDE support that actually works.&lt;/p&gt;
&lt;p&gt;This post is the reference I wish I had: a complete copy-paste workflow for a small library, using only &lt;code&gt;scala-cli&lt;/code&gt;.
Maybe it&apos;ll save you the same googling.&lt;/p&gt;
&lt;h2&gt;The Basics&lt;/h2&gt;
&lt;p&gt;A minimal &lt;code&gt;scala-cli&lt;/code&gt; library project may look like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;my-library/
├── project.scala
├── .scalafmt.conf
├── src/
│   └── MyLib.scala
├── test/
│   └── MyLibTest.scala
└── .scoverage/
    └── report.sc
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Your configuration lives &lt;em&gt;inside&lt;/em&gt; your source files using &quot;directives&quot;. However, as your project grows, you can
consolidate these into a single file named &lt;code&gt;project.scala&lt;/code&gt;.
Directives cover the Scala version, dependencies (both main and test-only), and compiler options — all in one place:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;//&amp;gt; using scala 3.8.2
//&amp;gt; using test.dep org.scalameta::munit::1.2.3

//&amp;gt; using options -deprecation -feature -new-syntax -unchecked
//&amp;gt; using options -language:noAutoTupling
//&amp;gt; using options -Yexplicit-nulls
//&amp;gt; using options -Wsafe-init -Werror -Wunused:all
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;scala-cli&lt;/code&gt; will automatically download the Scala compiler and dependencies, compile all files in the directory, and
execute the entry point.&lt;/p&gt;
&lt;h3&gt;IDE Support&lt;/h3&gt;
&lt;p&gt;If you&apos;re using VS Code (with Metals) or IntelliJ, &lt;code&gt;scala-cli&lt;/code&gt; works out of the box. If you ever feel the IDE is out of
sync, run:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;scala-cli --power setup-ide .
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Formatting with Scalafmt&lt;/h3&gt;
&lt;p&gt;Consistent code style is non-negotiable. &lt;code&gt;scala-cli&lt;/code&gt; has built-in support for &lt;code&gt;scalafmt&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;To format your code:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;scala-cli fmt .
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If you want to customize the style, create a &lt;code&gt;.scalafmt.conf&lt;/code&gt; file in your root directory. &lt;code&gt;scala-cli&lt;/code&gt; will pick it up
automatically. You can also enforce formatting in CI:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;scala-cli fmt . --check
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Testing&lt;/h3&gt;
&lt;p&gt;Testing doesn&apos;t require a separate build module. Just create a &lt;code&gt;*.test.scala&lt;/code&gt; file or put your tests in a &lt;code&gt;tests/&lt;/code&gt;
directory.&lt;/p&gt;
&lt;p&gt;For example, create &lt;code&gt;MyTests.test.scala&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class MyTests extends munit.FunSuite {
  test(&quot;math works&quot;) {
    assertEquals(1 + 1, 2)
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Run all tests in the current directory:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;scala-cli test .
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;scala-cli&lt;/code&gt; is smart enough to include test-only dependencies only when running the &lt;code&gt;test&lt;/code&gt; command.&lt;/p&gt;
&lt;h2&gt;Code Coverage&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;scala-cli&lt;/code&gt; doesn&apos;t have a native &lt;code&gt;coverage&lt;/code&gt; command. My approach here uses the Scala 3 compiler&apos;s built-in coverage
instrumentation directly, with a custom report script that gives you full control. This is exactly how it&apos;s done in
the &lt;a href=&quot;https://github.com/halotukozak/made&quot;&gt;made&lt;/a&gt; project.&lt;/p&gt;
&lt;h3&gt;How Scala 3 coverage instrumentation works&lt;/h3&gt;
&lt;p&gt;The Scala 3 compiler has a built-in coverage phase. When you pass &lt;code&gt;-coverage-out:&amp;lt;dir&amp;gt;&lt;/code&gt;, the compiler does two things
during compilation:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Writes &lt;code&gt;scoverage.coverage&lt;/code&gt;&lt;/strong&gt; — a serialized file describing every instrumentable statement in your source code:
its location, source file, line number, and a unique ID. This is the &lt;em&gt;coverage map&lt;/em&gt;.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Injects measurement calls&lt;/strong&gt; into the compiled bytecode. Every instrumented statement gets a
&lt;code&gt;Invoker.invoked(id, dataDir)&lt;/code&gt; call that writes a tiny file (&lt;code&gt;scoverage.measurements.{id}&lt;/code&gt;) to &lt;code&gt;&amp;lt;dir&amp;gt;&lt;/code&gt; at runtime.
Each file&apos;s existence proves that statement was executed.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;After &lt;code&gt;scala-cli test .&lt;/code&gt; finishes, the output directory contains the coverage map and a measurement file for every
statement that was hit during the test run. The report step is just: deserialize the map, scan which measurement files
exist, compute the ratio.&lt;/p&gt;
&lt;h3&gt;Report Generation&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;Add coverage directives to &lt;code&gt;project.scala&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;//&amp;gt; using options -coverage-out:./.scoverage
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;-coverage-out:./.scoverage&lt;/code&gt; tells the compiler where to write both the coverage map and the runtime measurement
files.
Everything lands in &lt;code&gt;.scoverage/&lt;/code&gt;.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Run tests:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;scala-cli test .
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;After this, &lt;code&gt;.scoverage/&lt;/code&gt; contains &lt;code&gt;scoverage.coverage&lt;/code&gt; (the map) and &lt;code&gt;scoverage.measurements.*&lt;/code&gt; files (one per
executed
statement).&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Generate the report:&lt;/p&gt;
&lt;p&gt;I depend on the scoverage library directly and have written a simple Scala script:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;//&amp;gt; using scala 3.8.2
//&amp;gt; using dep org.scoverage::scalac-scoverage-reporter:2.5.2
//&amp;gt; using dep org.scoverage::scalac-scoverage-domain:2.5.2
//&amp;gt; using dep org.scoverage::scalac-scoverage-serializer:2.5.2

import scoverage.reporter.{ScoverageHtmlWriter, CoberturaXmlWriter, IOUtils}
import scoverage.serialize.Serializer
import java.io.File

val coverageFile = new File(&quot;.scoverage/scoverage.coverage&quot;)
val sourceDir = new File(&quot;.&quot;)
val measurementDir = new File(&quot;.scoverage&quot;)
val outDir = new File(&quot;.scoverage/report&quot;)

if !coverageFile.exists() then
  println(s&quot;Error: Coverage file not found at ${coverageFile.getAbsolutePath}&quot;)
  sys.exit(1)

val coverage = Serializer.deserialize(coverageFile, sourceDir)
val measurementFiles = IOUtils.findMeasurementFiles(measurementDir)
coverage.apply(IOUtils.invoked(measurementFiles.toIndexedSeq))

outDir.mkdirs()

ScoverageHtmlWriter(Seq(sourceDir), outDir, None).write(coverage)
CoberturaXmlWriter(Seq(sourceDir), outDir, None).write(coverage)

println(s&quot;Statement coverage: ${coverage.statementCoverageFormatted}%&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Run it with &lt;code&gt;scala-cli .scoverage/report.sc&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;This is the &quot;deserialize, scan, compute&quot; step from above — &lt;code&gt;Serializer.deserialize&lt;/code&gt; loads the coverage map,
&lt;code&gt;IOUtils.findMeasurementFiles&lt;/code&gt; collects which statements were hit, and the two writers produce the output:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;ScoverageHtmlWriter&lt;/code&gt; — browsable HTML where you can click into each source file and see covered (green) vs
uncovered (red) lines.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;CoberturaXmlWriter&lt;/code&gt; — &lt;code&gt;cobertura.xml&lt;/code&gt;, the standard format that CI tools understand.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;CI integration&lt;/h3&gt;
&lt;p&gt;In GitHub Actions, the coverage step chains naturally after &lt;code&gt;scala-cli test&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;- run: scala-cli --power test .
- run: scala-cli .scoverage/report.sc
- name: Code Coverage Report
  uses: 5monkeys/cobertura-action@master
  continue-on-error: true
  with:
    path: .scoverage/report/cobertura.xml
    minimum_coverage: 80
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;scala-cli test .&lt;/code&gt; runs with instrumentation (because &lt;code&gt;-coverage-out&lt;/code&gt; is in &lt;code&gt;project.scala&lt;/code&gt;), the report script
generates &lt;code&gt;cobertura.xml&lt;/code&gt;, and &lt;code&gt;cobertura-action&lt;/code&gt; posts the coverage summary directly on the PR — no external service
or token needed.&lt;/p&gt;
&lt;h2&gt;Publishing to Maven Central&lt;/h2&gt;
&lt;p&gt;Yes, you can publish libraries to Maven Central (or any repo) using &lt;code&gt;scala-cli&lt;/code&gt;. This requires the &lt;code&gt;--power&lt;/code&gt; flags.&lt;/p&gt;
&lt;p&gt;First, add publishing metadata to your &lt;code&gt;project.scala&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;//&amp;gt; using publish.organization com.example
//&amp;gt; using publish.name my-library
//&amp;gt; using publish.computeVersion git:tag
//&amp;gt; using publish.description &quot;My awesome Scala library&quot;
//&amp;gt; using publish.url https://github.com/yourname/my-library
//&amp;gt; using publish.license Apache-2.0
//&amp;gt; using publish.vcs github:yourname/my-library
//&amp;gt; using publish.repository central
//&amp;gt; using publish.developer &quot;yourname|Your Name|https://github.com/yourname&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;computeVersion git:tag&lt;/code&gt; derives the version from the latest git tag — push &lt;code&gt;v0.1.0&lt;/code&gt; and the published artifact is
&lt;code&gt;0.1.0&lt;/code&gt;. No version string to update manually.&lt;/p&gt;
&lt;h3&gt;PGP Key Setup&lt;/h3&gt;
&lt;p&gt;Maven Central requires all artifacts to be PGP-signed. &lt;code&gt;scala-cli&lt;/code&gt; can generate a key pair for you:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;scala-cli --power config --create-pgp-key --email your@email.com
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This creates a PGP key and stores it in your local &lt;code&gt;scala-cli&lt;/code&gt; config. For CI, you need to export the private key and
passphrase as repository secrets. If you prefer using GPG directly:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;gpg --gen-key
gpg --armor --export-secret-keys YOUR_KEY_ID  # → store as PGP_PRIVATE_KEY secret
gpg --keyserver keyserver.ubuntu.com --send-keys YOUR_KEY_ID
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You&apos;ll need four secrets in your CI environment: &lt;code&gt;SONATYPE_USERNAME&lt;/code&gt;, &lt;code&gt;SONATYPE_PASSWORD&lt;/code&gt;, &lt;code&gt;PGP_PRIVATE_KEY&lt;/code&gt;, and
&lt;code&gt;PGP_PASSPHRASE&lt;/code&gt;. The Sonatype credentials come
from &lt;a href=&quot;https://central.sonatype.com/&quot;&gt;creating an account on Central Portal&lt;/a&gt; — after verifying your namespace (e.g.
&lt;code&gt;io.github.yourname&lt;/code&gt; via GitHub), generate a user token.&lt;/p&gt;
&lt;h3&gt;Running the publish command&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# To a local repository (for testing)
scala-cli --power publish local .

# To Sonatype/Maven Central
scala-cli --power publish . --verbose \
  --user &quot;env:SONATYPE_USERNAME&quot; \
  --password &quot;env:SONATYPE_PASSWORD&quot; \
  --secret-key &quot;env:PGP_PRIVATE_KEY&quot; \
  --secret-key-password &quot;env:PGP_PASSPHRASE&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;&quot;env:...&quot;&lt;/code&gt; syntax tells &lt;code&gt;scala-cli&lt;/code&gt; to read from environment variables rather than passing secrets as arguments.&lt;/p&gt;
&lt;h2&gt;GitHub Actions Integration&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;scala-cli&lt;/code&gt; is perfect for CI because it&apos;s lightweight. Here&apos;s the full CI workflow from &lt;code&gt;made&lt;/code&gt; — two parallel jobs for
formatting and tests (including the coverage pipeline from Section 4):&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;name: Run CI

on:
  push:
    branches: [ main ]
  pull_request:
    branches: [ main ]

jobs:
  lint:
    name: Scalafmt
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v6
      - uses: coursier/cache-action@v8.0
      - uses: VirtusLab/scala-cli-setup@v1
      - run: scala-cli --power fmt --check .

  test:
    name: Test
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v6
        with:
          fetch-depth: 0
      - uses: coursier/cache-action@v8.0
      - uses: VirtusLab/scala-cli-setup@v1
      - run: scala-cli --power test .
      - run: scala-cli .scoverage/report.sc
      - name: Code Coverage Report
        uses: 5monkeys/cobertura-action@master
        continue-on-error: true
        with:
          path: .scoverage/report/cobertura.xml
          minimum_coverage: 80
      - run: scala-cli --power doc .
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;fetch-depth: 0&lt;/code&gt; is needed because &lt;code&gt;publish.computeVersion git:tag&lt;/code&gt; requires the full git history to derive the version.
&lt;code&gt;coursier/cache-action&lt;/code&gt; caches downloaded JVMs and dependencies between runs. The &lt;code&gt;doc&lt;/code&gt; step at the end validates that
Scaladoc generates without errors — a cheap smoke test for documentation quality.&lt;/p&gt;
&lt;p&gt;The publish workflow triggers on version tags:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;name: Publish
on:
  push:
    tags: [ &apos;v*&apos; ]

jobs:
  publish:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v6
        with:
          fetch-depth: 0
      - uses: coursier/cache-action@v8.0
      - uses: VirtusLab/scala-cli-setup@v1
      - run: |
          scala-cli --power publish . --verbose \
            --user &quot;env:SONATYPE_USERNAME&quot; \
            --password &quot;env:SONATYPE_PASSWORD&quot; \
            --secret-key &quot;env:PGP_PRIVATE_KEY&quot; \
            --secret-key-password &quot;env:PGP_PASSPHRASE&quot;
        env:
          SONATYPE_USERNAME: ${{ secrets.SONATYPE_USERNAME }}
          SONATYPE_PASSWORD: ${{ secrets.SONATYPE_PASSWORD }}
          PGP_PRIVATE_KEY: ${{ secrets.PGP_PRIVATE_KEY }}
          PGP_PASSPHRASE: ${{ secrets.PGP_PASSPHRASE }}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Push a tag, artifact lands on Maven Central. That&apos;s it.&lt;/p&gt;
&lt;h2&gt;Documentation Generation&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;scala-cli&lt;/code&gt; can generate Scaladoc with a single command:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;scala-cli --power doc .
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This produces a &lt;code&gt;scala-doc/&lt;/code&gt; directory with browsable HTML documentation. For a library, you&apos;ll want to pass additional
flags to the Scaladoc tool after &lt;code&gt;--&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;scala-cli --power doc . -- \
  -project &quot;My Library&quot; \
  -project-version v0.1.0 \
  -snippet-compiler:compile \
  -source-links:&quot;src=github://yourname/my-library?tag=v0.1.0&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;-snippet-compiler:compile&lt;/code&gt; compiles code snippets in &lt;code&gt;@example&lt;/code&gt; blocks during generation — broken examples fail the
build, not the reader. &lt;code&gt;-source-links&lt;/code&gt; adds clickable &quot;Source&quot; links from the docs back to your GitHub repository at
the right tag.&lt;/p&gt;
&lt;p&gt;In CI, running &lt;code&gt;scala-cli --power doc .&lt;/code&gt; without extra flags is a cheap smoke test — if any Scaladoc comment has a
syntax error or a broken &lt;code&gt;@example&lt;/code&gt; snippet, the build fails.&lt;/p&gt;
&lt;p&gt;One caveat: if your project uses Scala 3 macros, &lt;code&gt;scala-cli doc .&lt;/code&gt; may fail because the Scaladoc compiler tries to
expand them in a context where they can&apos;t run. There&apos;s no clean workaround yet — for now, you may need to exclude
macro-heavy files from documentation generation.&lt;/p&gt;
&lt;h3&gt;Deploying to GitHub Pages&lt;/h3&gt;
&lt;p&gt;In case you would like to deploy Scaladoc to GitHub Pages on every version tag, here&apos;s the workflow.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;name: Deploy documentation to Pages

on:
  push:
    tags: [ &apos;v*&apos; ]
  workflow_dispatch:

permissions:
  contents: read
  pages: write
  id-token: write

concurrency:
  group: &quot;pages&quot;
  cancel-in-progress: true

jobs:
  deploy:
    environment:
      name: github-pages
      url: ${{ steps.deployment.outputs.page_url }}
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v6
        with:
          fetch-depth: 0
      - uses: coursier/cache-action@v8.0
      - uses: VirtusLab/scala-cli-setup@v1
      - name: Generate documentation
        run: |
          scala-cli --power doc . -- \
          -project &quot;M&amp;amp;DE&quot; \
          -project-version ${{ github.ref_name }} \
          -project-footer &quot;made with ❤️ and coffee&quot; \
          -social-links:github::https://github.com/halotukozak/made \
          -snippet-compiler:compile \
          -source-links:&quot;src=github://halotukozak/made?tag=${{ github.ref_name }}&quot; \
          -revision:${{ github.ref_name }}
      - uses: actions/configure-pages@v5
      - uses: actions/upload-pages-artifact@v4
        with:
          path: &apos;scala-doc&apos;
      - uses: actions/deploy-pages@v4
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;${{ github.ref_name }}&lt;/code&gt; injects the git tag (e.g. &lt;code&gt;v0.1.0&lt;/code&gt;) into the project version and source links, so every
tagged release gets documentation with correct versioned links back to the source. &lt;code&gt;-social-links:github::&lt;/code&gt; adds a
GitHub icon linking to the repository. &lt;code&gt;-revision&lt;/code&gt; anchors source links to the exact commit.&lt;/p&gt;
&lt;p&gt;Push a tag — docs land on GitHub Pages automatically, right alongside the Maven Central publish.&lt;/p&gt;
&lt;h2&gt;Extra: Testing Across JVM Versions&lt;/h2&gt;
&lt;p&gt;One of the most powerful features of &lt;code&gt;scala-cli&lt;/code&gt; is its ability to manage JVM installations automatically. Testing your
code against multiple Java versions is a one-liner:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# Run tests on Java 11
scala-cli test . --jvm 11
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You don&apos;t need &lt;code&gt;sdkman&lt;/code&gt; or manually managed &lt;code&gt;$JAVA_HOME&lt;/code&gt;. &lt;code&gt;scala-cli&lt;/code&gt; uses &lt;a href=&quot;https://get-coursier.io/&quot;&gt;coursier&lt;/a&gt; to
download the requested JVM under the hood, ensuring your tests are reproducible across different environments. You can
also pin the version in your &lt;code&gt;project.scala&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;//&amp;gt; using jvm 21
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Wrapping Up&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;scala-cli&lt;/code&gt; is no longer just a &quot;scripting tool&quot;. It&apos;s a robust, fast, and modern way to build Scala applications.
Whether you&apos;re writing a simple automation script or a published library, &lt;code&gt;scala-cli&lt;/code&gt; provides a seamless E2E experience
without the overhead of traditional build tools.&lt;/p&gt;
&lt;h2&gt;References&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://scala-cli.virtuslab.org/&quot;&gt;Official scala-cli Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/halotukozak/made&quot;&gt;M&amp;amp;DE Repository&lt;/a&gt; — the project used as the example throughout this post&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.scala-lang.org/scala3/guides/migration/options-lookup.html&quot;&gt;Scala 3 Compiler Options&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/VirtusLab/scala-cli-setup&quot;&gt;VirtusLab&apos;s GitHub Actions Setup&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/scoverage/scalac-scoverage-plugin&quot;&gt;Scoverage&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.scala-lang.org/scala3/guides/scaladoc/&quot;&gt;Scala 3 Scaladoc&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded><author>Bartłomiej Kozak</author></item><item><title>Homogeneous Tuples in Scala 3</title><link>https://halotukozak.com/posts/scala-type-safe-homogeneous-tuples</link><guid isPermaLink="true">https://halotukozak.com/posts/scala-type-safe-homogeneous-tuples</guid><description>How to prove at compile time that a Scala 3 tuple contains only elements of a single type, using match types, opaque types, and clause interleaving.</description><pubDate>Mon, 06 Apr 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;The Problem: Tuples That Lie&lt;/h2&gt;
&lt;p&gt;Scala 3 tuples are heterogeneous by design. &lt;code&gt;(1, &quot;hello&quot;, true)&lt;/code&gt; is a &lt;code&gt;(Int, String, Boolean)&lt;/code&gt; — each element has its
own type. But sometimes you &lt;em&gt;know&lt;/em&gt; all elements share a single type. Maybe you&apos;re collecting database column IDs, or
accumulating configuration values, or — as in my case — building typed pipelines where a tuple of transformations all
operate on the same domain.&lt;/p&gt;
&lt;p&gt;&quot;Why not just use a &lt;code&gt;List[Int]&lt;/code&gt;?&quot; — because tuples carry their &lt;em&gt;arity&lt;/em&gt; and &lt;em&gt;per-element types&lt;/em&gt; at compile time. In a
larger structure you might have &lt;code&gt;(Column[Int], Column[Int], Column[String])&lt;/code&gt; where only some positions share a type.
You want to operate on the homogeneous prefix without losing track of what&apos;s where. And you don&apos;t want runtime checks
— the whole point is to catch mismatches before the code runs.&lt;/p&gt;
&lt;p&gt;The compiler doesn&apos;t help you here. A &lt;code&gt;(Int, Int, Int)&lt;/code&gt; is still three separate types glued together, and Scala offers
no built-in way to say &quot;this tuple contains only &lt;code&gt;Int&lt;/code&gt;s.&quot;&lt;/p&gt;
&lt;p&gt;Let&apos;s see what happens when you try.&lt;/p&gt;
&lt;h2&gt;What Doesn&apos;t Work&lt;/h2&gt;
&lt;h3&gt;Tuple.map&lt;/h3&gt;
&lt;p&gt;Tuples in Scala 3 have a &lt;code&gt;map&lt;/code&gt; method. But it takes
a &lt;a href=&quot;https://docs.scala-lang.org/scala3/reference/new-types/polymorphic-function-types.html&quot;&gt;polymorphic function&lt;/a&gt;
&lt;code&gt;[t] =&amp;gt; t =&amp;gt; F[t]&lt;/code&gt; — inside that lambda, &lt;code&gt;t&lt;/code&gt; is abstract. You don&apos;t know it&apos;s &lt;code&gt;Int&lt;/code&gt;, so you can&apos;t call &lt;code&gt;Int&lt;/code&gt;-specific
operations:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;val tup = (1, 2, 3)

// Won&apos;t compile — t is not known to be Int:
//tup.map([t] =&amp;gt; (x: t) =&amp;gt; x + 1)

// You&apos;d have to cast, losing all safety:
tup.map[[X] =&amp;gt;&amp;gt; Int]([t] =&amp;gt; (x: t) =&amp;gt; x.asInstanceOf[Int] + 1)
// Returns (2, 3, 4), but nothing stops you from calling this on (&quot;a&quot;, &quot;b&quot;, &quot;c&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We have to explicitly pass &lt;code&gt;[[X] =&amp;gt;&amp;gt; Int]&lt;/code&gt; as the result type because the compiler can&apos;t infer what &lt;code&gt;F&lt;/code&gt; should be when
the return type doesn&apos;t match &lt;code&gt;t&lt;/code&gt;. And of course, the compiler won&apos;t stop you from writing the same code for
&lt;code&gt;(&quot;a&quot;, &quot;b&quot;, &quot;c&quot;)&lt;/code&gt; — you&apos;d get a &lt;code&gt;ClassCastException&lt;/code&gt; instead of a compile error.&lt;/p&gt;
&lt;h3&gt;Converting to Array or List&lt;/h3&gt;
&lt;p&gt;OK, so &lt;code&gt;map&lt;/code&gt; doesn&apos;t work without casts. What about converting the whole tuple to an &lt;code&gt;Array&lt;/code&gt; or &lt;code&gt;List&lt;/code&gt; first?&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;val tup: Tuple = (1, 2, 3)

// Array: JVM arrays are reified — Array[Object] cannot be cast to Array[Int]
tup.toArray.asInstanceOf[Array[Int]] // ClassCastException!

// You can map element-by-element, but that&apos;s a runtime cast with zero compile-time safety:
tup.toArray.map(_.asInstanceOf[Int]).sum // works, but (1, &quot;oops&quot;, 3) blows up at runtime

// List: erasure means the cast silently succeeds...
tup.toList.asInstanceOf[List[Int]].sum // returns 6
// ...but on a mixed tuple the bug just hides until you actually use the values:
val mixed: Tuple = (1, &quot;oops&quot;, 3)
mixed.toList.asInstanceOf[List[Int]].sum // ClassCastException — far from the real mistake
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Every approach so far either crashes at runtime or silently hides the bug. We need a way to prove, at compile time,
that a tuple contains only elements of a given type.&lt;/p&gt;
&lt;h2&gt;The containsOnly Proof&lt;/h2&gt;
&lt;h3&gt;A Match Type&lt;/h3&gt;
&lt;p&gt;Scala 3&apos;s &lt;a href=&quot;https://docs.scala-lang.org/scala3/reference/new-types/match-types.html&quot;&gt;match types&lt;/a&gt; let us compute types
based on pattern matching at the type level. Here&apos;s the idea: walk the tuple recursively and check that every element
matches the target type.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;type ContainsOnly[Tup &amp;lt;: Tuple, T] &amp;lt;: Boolean = Tup match
  case EmptyTuple =&amp;gt; true
  case T *: tail =&amp;gt; ContainsOnly[tail, T]
  case _ =&amp;gt; false
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This reads naturally: an empty tuple trivially contains only &lt;code&gt;T&lt;/code&gt;, a tuple whose head is &lt;code&gt;T&lt;/code&gt; recurses on the tail,
anything else is &lt;code&gt;false&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Let&apos;s test it with &lt;code&gt;summon&lt;/code&gt;, which materializes a given at compile time or fails:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// These compile:
summon[ContainsOnly[(Int, Int, Int), Int] =:= true]
summon[ContainsOnly[EmptyTuple, String] =:= true]

// This doesn&apos;t compile:
// summon[ContainsOnly[(Int, String, Int), Int] =:= true]
// Error: Cannot prove that false =:= true
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;It works! You could even use the match type directly as a constraint with &lt;code&gt;=:=&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;def processInts[Tup &amp;lt;: Tuple](tup: Tup)(using ContainsOnly[Tup, Int] =:= true): String =
  &quot;all ints!&quot;

processInts((1, 2, 3)) // compiles!
// processInts((1, &quot;x&quot;, 3)) // doesn&apos;t compile — Cannot prove that false =:= true
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This works, but has two problems. The error message is cryptic — &quot;Cannot prove that false =:= true&quot; says nothing about
&lt;em&gt;which type&lt;/em&gt; broke it. And &lt;code&gt;=:=&lt;/code&gt; has no public constructor, so you can&apos;t create an instance yourself — meaning there&apos;s
no escape hatch for cases where you &lt;em&gt;know&lt;/em&gt; the constraint holds but the compiler can&apos;t prove it (e.g. after a runtime
check). A dedicated type class solves both.&lt;/p&gt;
&lt;h3&gt;The Type Class&lt;/h3&gt;
&lt;p&gt;The standard approach: wrap the match type in a type class whose &lt;code&gt;given&lt;/code&gt; instance can only be synthesized when the match
type reduces to &lt;code&gt;true&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;A first attempt with a regular class:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@implicitNotFound(&quot;${Tup} does not contain only ${T}&quot;)
class containsOnly[Tup &amp;lt;: Tuple, T]

object containsOnly:

  type Loop[Tup &amp;lt;: Tuple, T] &amp;lt;: Boolean = Tup match
    case EmptyTuple =&amp;gt; true
    case T *: tail =&amp;gt; Loop[tail, T]
    case _ =&amp;gt; false

  inline given [Tup &amp;lt;: Tuple, T] =&amp;gt; (Loop[Tup, T] =:= true) =&amp;gt; containsOnly[Tup, T] = containsOnly()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Here we use the new &lt;a href=&quot;https://docs.scala-lang.org/scala3/reference/contextual/givens.html&quot;&gt;given syntax&lt;/a&gt; introduced in
Scala 3.6. Instead of &lt;code&gt;given [Tup, T](using ...): Result&lt;/code&gt;, we write &lt;code&gt;given [Tup, T] =&amp;gt; (...) =&amp;gt; Result&lt;/code&gt; — each &lt;code&gt;=&amp;gt;&lt;/code&gt;
introduces a new clause. The type parameters come first, then the context parameter (&lt;code&gt;Loop[Tup, T] =:= true&lt;/code&gt; — our
proof that the match type reduced to &lt;code&gt;true&lt;/code&gt;), and finally the result type. It reads left-to-right: &quot;for any &lt;code&gt;Tup&lt;/code&gt; and
&lt;code&gt;T&lt;/code&gt;, given that &lt;code&gt;Loop[Tup, T]&lt;/code&gt; equals &lt;code&gt;true&lt;/code&gt;, produce a &lt;code&gt;containsOnly[Tup, T]&lt;/code&gt;.&quot;&lt;/p&gt;
&lt;p&gt;One subtlety: we&apos;d like &lt;code&gt;Loop&lt;/code&gt; to be &lt;code&gt;private&lt;/code&gt;, but it can&apos;t be — the compiler needs to see it at the call site to
reduce &lt;code&gt;Loop[Tup, T]&lt;/code&gt; and resolve the &lt;code&gt;=:=&lt;/code&gt; evidence.&lt;/p&gt;
&lt;p&gt;This works — &lt;code&gt;containsOnly&lt;/code&gt; instances are only synthesized when &lt;code&gt;Loop&lt;/code&gt; reduces to &lt;code&gt;true&lt;/code&gt;. But every summon allocates a
new &lt;code&gt;containsOnly()&lt;/code&gt; object at runtime, even though the instance carries no data. It&apos;s a pure compile-time proof that
wastes heap space.&lt;/p&gt;
&lt;p&gt;We could create a single reusable instance to avoid repeated allocations, but we can do even better — eliminate the
object entirely with an &lt;a href=&quot;https://docs.scala-lang.org/scala3/reference/other-new-features/opaques.html&quot;&gt;opaque type&lt;/a&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@implicitNotFound(&quot;${Tup} does not contain only ${T}&quot;)
opaque infix type containsOnly[Tup &amp;lt;: Tuple, T] = Boolean

object containsOnly:

  type Loop[Tup &amp;lt;: Tuple, T] &amp;lt;: Boolean = Tup match
    case EmptyTuple =&amp;gt; true
    case T *: tail =&amp;gt; Loop[tail, T]
    case _ =&amp;gt; false

  inline given [Tup &amp;lt;: Tuple, T] =&amp;gt; (Loop[Tup, T] =:= true) =&amp;gt; containsOnly[Tup, T] = true
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now &lt;code&gt;containsOnly[Tup, T]&lt;/code&gt; is just a &lt;code&gt;Boolean&lt;/code&gt; at runtime — literally the value &lt;code&gt;true&lt;/code&gt;. No object, no allocation, no
overhead. The &lt;code&gt;=:=&lt;/code&gt; evidence ensures the compiler only synthesizes this given when &lt;code&gt;Loop&lt;/code&gt; reduces to &lt;code&gt;true&lt;/code&gt;. If you try
to summon &lt;code&gt;containsOnly[(Int, String), Int]&lt;/code&gt;, the evidence can&apos;t be constructed and compilation fails.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;infix&lt;/code&gt; modifier lets us write &lt;code&gt;Tup containsOnly T&lt;/code&gt; instead of &lt;code&gt;containsOnly[Tup, T]&lt;/code&gt; — a small readability win.&lt;/p&gt;
&lt;p&gt;Let&apos;s test that the type class works as a constraint:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;def processInts[Tup &amp;lt;: Tuple](tup: Tup)(using Tup containsOnly Int): Int =
  tup.toList.asInstanceOf[List[Int]].sum // safe now — the proof guarantees all elements are Int

processInts((1, 2, 3)) // compiles: 6
processInts(EmptyTuple) // compiles: 0
// processInts((1, &quot;nope&quot;, 3))  // doesn&apos;t compile!
// error: (Int, String, Int) does not contain only Int
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;@implicitNotFound&lt;/code&gt; annotation gives us a human-readable error instead of the generic &quot;No given instance&quot; message.&lt;/p&gt;
&lt;p&gt;Now we have a reusable, zero-cost proof that a tuple is homogeneous. Time to do something useful with it.&lt;/p&gt;
&lt;h2&gt;Putting containsOnly to Work&lt;/h2&gt;
&lt;h3&gt;mapAs&lt;/h3&gt;
&lt;p&gt;Now let&apos;s put &lt;code&gt;containsOnly&lt;/code&gt; to work. I want a &lt;code&gt;mapAs&lt;/code&gt; extension on &lt;code&gt;Tuple&lt;/code&gt; that requires a &lt;code&gt;containsOnly&lt;/code&gt; proof,
applies a polymorphic function to each element, and preserves the full type information in the result.&lt;/p&gt;
&lt;p&gt;We use &lt;code&gt;extension (tup: Tuple)&lt;/code&gt; and refer to &lt;code&gt;tup.type&lt;/code&gt; in the return type. Why not
&lt;code&gt;extension [Tup &amp;lt;: Tuple](tup: Tup)&lt;/code&gt;? Because &lt;code&gt;Tuple.Map[Tup, F]&lt;/code&gt; is a match type — the compiler needs to see a
&lt;em&gt;concrete&lt;/em&gt; tuple to reduce it. An abstract type parameter &lt;code&gt;Tup&lt;/code&gt; leaves the match stuck. &lt;code&gt;tup.type&lt;/code&gt;, however, is the
singleton type of the actual value, so at the call site the compiler knows the exact shape and &lt;code&gt;Tuple.Map&lt;/code&gt; reduces
correctly.&lt;/p&gt;
&lt;p&gt;Note the &lt;code&gt;asInstanceOf[t &amp;amp; T]&lt;/code&gt; inside the implementation — this cast is safe &lt;em&gt;because&lt;/em&gt; the &lt;code&gt;containsOnly&lt;/code&gt; proof
guarantees every element is already a &lt;code&gt;T&lt;/code&gt;. The cast just narrows the abstract &lt;code&gt;t&lt;/code&gt; to &lt;code&gt;t &amp;amp; T&lt;/code&gt; so the bounded
polymorphic function &lt;code&gt;f&lt;/code&gt; accepts it. Without the proof, this would be exactly the kind of unsafe cast we&apos;re trying to
eliminate.&lt;/p&gt;
&lt;p&gt;A first attempt — one method with all type parameters:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;extension (tup: Tuple)
  inline def mapAs[T, F[_ &amp;lt;: T]](inline f: [t &amp;lt;: T] =&amp;gt; t =&amp;gt; F[t])(using tup.type containsOnly T): Tuple.Map[tup.type, [X] =&amp;gt;&amp;gt; F[X &amp;amp; T]] =
    tup.map[[X] =&amp;gt;&amp;gt; F[X &amp;amp; T]]([t] =&amp;gt; (t: t) =&amp;gt; f(t.asInstanceOf[t &amp;amp; T]))
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This compiles, but using it is painful — Scala can&apos;t partially infer type parameters, so you must specify &lt;em&gt;both&lt;/em&gt;
&lt;code&gt;T&lt;/code&gt; and &lt;code&gt;F&lt;/code&gt; explicitly:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// You&apos;d have to write:
(1, 2, 3).mapAs[Int, Option]([t &amp;lt;: Int] =&amp;gt; (x: t) =&amp;gt; Some(x))

// But you can&apos;t write this (F can&apos;t be inferred from T alone):
// (1, 2, 3).mapAs[Int]([t &amp;lt;: Int] =&amp;gt; (x: t) =&amp;gt; Some(x))
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We want to fix &lt;code&gt;T&lt;/code&gt; first (via &lt;code&gt;mapAs[Int]&lt;/code&gt;) and let the compiler infer &lt;code&gt;F&lt;/code&gt; from the function we pass.&lt;/p&gt;
&lt;h3&gt;Currying Type Parameters with a Wrapper&lt;/h3&gt;
&lt;p&gt;The classic trick: return an intermediate object that captures the first type parameter, then let the caller supply the
second.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;extension (tup: Tuple)
  inline def mapAs[T](using tup.type containsOnly T): MapAs[T, tup.type] = MapAs(tup)

class MapAs[T, Tup &amp;lt;: Tuple](private val underlying: Tup):
  inline def apply[F[_ &amp;lt;: T]](inline f: [t &amp;lt;: T] =&amp;gt; t =&amp;gt; F[t]): Tuple.Map[Tup, [X] =&amp;gt;&amp;gt; F[X &amp;amp; T]] =
    underlying.map[[X] =&amp;gt;&amp;gt; F[X &amp;amp; T]]([t] =&amp;gt; (t: t) =&amp;gt; f(t.asInstanceOf[t &amp;amp; T]))
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Let me unpack what&apos;s going on:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;mapAs[T]&lt;/code&gt; fixes the element type and returns a &lt;code&gt;MapAs&lt;/code&gt; wrapper, but only if the &lt;code&gt;containsOnly&lt;/code&gt; proof exists.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;MapAs.apply[F]&lt;/code&gt; takes the higher-kinded type &lt;code&gt;F&lt;/code&gt; and a polymorphic function &lt;code&gt;f&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://scala-lang.org/api/3.x/scala/Tuple.html&quot;&gt;&lt;code&gt;Tuple.Map[Tup, ...]&lt;/code&gt;&lt;/a&gt; computes the result type: if
&lt;code&gt;Tup = (Int, Int, Int)&lt;/code&gt; and &lt;code&gt;F = Option&lt;/code&gt;, the result is &lt;code&gt;(Option[Int], Option[Int], Option[Int])&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;X &amp;amp; T&lt;/code&gt; intersection ensures the compiler knows each element is a subtype of &lt;code&gt;T&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Testing:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;val ints = (1, 2, 3)
val opts: (Option[Int], Option[Int], Option[Int]) = ints.mapAs[Int]([t &amp;lt;: Int] =&amp;gt; (x: t) =&amp;gt; Some(x))
// opts = (Some(1), Some(2), Some(3))

val strs = (&quot;a&quot;, &quot;b&quot;)
val lengths: (List[String], List[String]) = strs.mapAs[String]([t &amp;lt;: String] =&amp;gt; (x: t) =&amp;gt; List(x))
// lengths = (List(a), List(b))

// This won&apos;t compile:
// (1, &quot;mixed&quot;, 3).mapAs[Int]([t &amp;lt;: Int] =&amp;gt; (x: t) =&amp;gt; Some(x))
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;It works, but there&apos;s a problem. &lt;code&gt;MapAs&lt;/code&gt; is a class — every call allocates a wrapper object on the heap. For a
type-level operation that exists purely to curry type parameters, that&apos;s wasteful.&lt;/p&gt;
&lt;h3&gt;Eliminating the Wrapper Allocation&lt;/h3&gt;
&lt;p&gt;We could try extending &lt;code&gt;AnyVal&lt;/code&gt;, but the optimization isn&apos;t guaranteed. The JVM still allocates when the value class is
used as a generic type parameter, assigned to a supertype, or passed to a method expecting &lt;code&gt;Any&lt;/code&gt;. The proper Scala 3
approach is an opaque type — a compile-time-only abstraction erased to its underlying type with &lt;em&gt;guaranteed&lt;/em&gt; zero
overhead:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;opaque type MapAs[T, Tup &amp;lt;: Tuple] = Tup

object MapAs:
  extension [T, Tup &amp;lt;: Tuple](mapAs: MapAs[T, Tup])
    inline def apply[F[_ &amp;lt;: T]](inline f: [t &amp;lt;: T] =&amp;gt; t =&amp;gt; F[t]): Tuple.Map[Tup, [X] =&amp;gt;&amp;gt; F[X &amp;amp; T]] =
      (mapAs: Tup).map[[X] =&amp;gt;&amp;gt; F[X &amp;amp; T]]([t] =&amp;gt; (t: t) =&amp;gt; f(t.asInstanceOf[t &amp;amp; T]))

extension (tup: Tuple)
  inline def mapAs[T](using tup.type containsOnly T): MapAs[T, tup.type] = tup
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Inside the &lt;code&gt;MapAs&lt;/code&gt; companion, we know that &lt;code&gt;MapAs[T, Tup]&lt;/code&gt; &lt;em&gt;is&lt;/em&gt; &lt;code&gt;Tup&lt;/code&gt;. Outside, the type system enforces the
abstraction boundary. No allocation, no erasure surprises, no caveats. The API is identical:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;val result: (Option[Int], Option[Int], Option[Int]) = (1, 2, 3).mapAs[Int]([t &amp;lt;: Int] =&amp;gt; (x: t) =&amp;gt; Some(x))
// result = (Some(1), Some(2), Some(3))
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Can We Skip the Wrapper Entirely?&lt;/h3&gt;
&lt;p&gt;After all that work on the wrapper, let&apos;s check if Scala 3 actually needs it.
Since &lt;a href=&quot;https://docs.scala-lang.org/sips/clause-interleaving.html&quot;&gt;SIP-47&lt;/a&gt; (clause interleaving, stable since Scala
3.4), you can mix type and value parameter clauses freely:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;extension (tup: Tuple)
  inline def mapAs[T](using tup.type containsOnly T)[F[_ &amp;lt;: T]](inline f: [t &amp;lt;: T] =&amp;gt; t =&amp;gt; F[t]): Tuple.Map[tup.type, [X] =&amp;gt;&amp;gt; F[X &amp;amp; T]] =
    tup.map[[X] =&amp;gt;&amp;gt; F[X &amp;amp; T]]([t] =&amp;gt; (t: t) =&amp;gt; f(t.asInstanceOf[t &amp;amp; T]))
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That&apos;s it. One method. The &lt;code&gt;using&lt;/code&gt; clause sits between the two type parameter lists, so &lt;code&gt;T&lt;/code&gt; is fixed first, the
&lt;code&gt;containsOnly&lt;/code&gt; proof is resolved, and then &lt;code&gt;F&lt;/code&gt; is provided — all in a single call.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;val result = (1, 2, 3).mapAs[Int]([t &amp;lt;: Int] =&amp;gt; (x: t) =&amp;gt; Some(x))
// result = (Some(1), Some(2), Some(3))

// Compile error:
// (1, &quot;nope&quot;).mapAs[Int]([t &amp;lt;: Int] =&amp;gt; (x: t) =&amp;gt; Some(x))
// error: (Int, String) does not contain only Int
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;No wrapper, no opaque type, no &lt;code&gt;AnyVal&lt;/code&gt;. Clause interleaving makes the entire intermediate-object pattern unnecessary.&lt;/p&gt;
&lt;p&gt;Let&apos;s peek at what the compiler actually generates (decompiled from bytecode):&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Tuple3 tup$proxy1 = .MODULE$.apply(BoxesRunTime.boxToInteger(1), BoxesRunTime.boxToInteger(2), BoxesRunTime.boxToInteger(3));
containsOnly$package$ var4 = containsOnly.package..MODULE$;
scala..eq.colon.eq x$1$proxy1 = scala..less.colon.less..MODULE$.refl();
boolean x$2$proxy1 = true;
Function1 f$proxy2 = (t) -&amp;gt; {
    int var1 = BoxesRunTime.unboxToInt(t);
    return scala.Some..MODULE$.apply(BoxesRunTime.boxToInteger(var1));
};
scala.runtime.Tuples..MODULE$.map(tup$proxy1, f$proxy2);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;All the type-level machinery has been erased. The &lt;code&gt;=:=&lt;/code&gt; evidence becomes a call to &lt;code&gt;refl()&lt;/code&gt; (a no-op singleton), the
&lt;code&gt;containsOnly&lt;/code&gt; proof is just &lt;code&gt;true&lt;/code&gt;, and &lt;code&gt;mapAs&lt;/code&gt; inlines down to a single &lt;code&gt;Tuples.map&lt;/code&gt; call with a plain Java lambda.
No wrappers, no intermediate objects — just a tuple, a function, and a runtime map.&lt;/p&gt;
&lt;h2&gt;Beyond Type Constructors&lt;/h2&gt;
&lt;h3&gt;Plain Return Types&lt;/h3&gt;
&lt;p&gt;So far every example used a type constructor like &lt;code&gt;Option&lt;/code&gt; or &lt;code&gt;List&lt;/code&gt; as the result — &lt;code&gt;F&lt;/code&gt; was always something the
compiler could figure out from the function&apos;s return type. But what if your function returns a plain type?&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;val tup = (1, 2, 3)
tup.mapAs[Int]([x &amp;lt;: Int] =&amp;gt; x =&amp;gt; x.toString)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This fails:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Found:    String
Required: (F?[_$1 &amp;lt;: Int])[x]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The problem is that &lt;code&gt;mapAs&lt;/code&gt; expects &lt;code&gt;F[_ &amp;lt;: T]&lt;/code&gt; — a type constructor that wraps &lt;code&gt;T&lt;/code&gt;. &lt;code&gt;Option[Int]&lt;/code&gt; fits because
&lt;code&gt;Option&lt;/code&gt; is &lt;code&gt;[X] =&amp;gt;&amp;gt; Option[X]&lt;/code&gt;. But &lt;code&gt;String&lt;/code&gt; isn&apos;t &lt;code&gt;F[Int]&lt;/code&gt; for any &lt;code&gt;F&lt;/code&gt; — it has no relationship to the input type,
so the compiler can&apos;t infer what &lt;code&gt;F&lt;/code&gt; should be.&lt;/p&gt;
&lt;p&gt;The fix is to tell the compiler explicitly what &lt;code&gt;F&lt;/code&gt; is — a constant function that ignores its argument and returns
&lt;code&gt;String&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;tup.mapAs[Int][[X &amp;lt;: Int] =&amp;gt;&amp;gt; String]([x &amp;lt;: Int] =&amp;gt; (x: x) =&amp;gt; x.toString)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Or equivalently, with a named type alias:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;type KString[_] = String
tup.mapAs[Int][KString]([x &amp;lt;: Int] =&amp;gt; (x: x) =&amp;gt; x.toString)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This pattern comes up more often than you&apos;d expect — any time the result type doesn&apos;t depend on the element type,
you&apos;ll need to spell out &lt;code&gt;F&lt;/code&gt;.&lt;/p&gt;
&lt;h3&gt;Limitation: Abstract Type Members&lt;/h3&gt;
&lt;p&gt;The same explicit-&lt;code&gt;F&lt;/code&gt; technique suggests another interesting use case. Suppose your elements share a common trait with
an abstract type member:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;trait C:
  type T
  def t: T

val tup = (new C { type T = String; val t = &quot;&quot; }, new C { type T = Int; val t = 1 })
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Each element is a &lt;code&gt;C&lt;/code&gt;, but its &lt;code&gt;T&lt;/code&gt; is different. Unfortunatelly, we cannot extract &lt;code&gt;T&lt;/code&gt; with a match type that peels open the refinement:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;type C_Of[t] = C { type T = t }
type Extract_T[X &amp;lt;: C] = X match
  case C_Of[t] =&amp;gt; t

tup.mapAs[C][Extract_T]([x &amp;lt;: C] =&amp;gt; (x: x) =&amp;gt; x.t)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Fails with:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Found:    x.t
Required: Extract_T[x]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A type projection doesn&apos;t help either:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;tup.mapAs[C][[X &amp;lt;: C] =&amp;gt;&amp;gt; X#T]([x &amp;lt;: C] =&amp;gt; (x: x) =&amp;gt; x.t)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Fails with:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;x is not a legal path
since it is not a concrete type
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The root issue is that match types cannot reduce when the scrutinee is an abstract type parameter — the compiler
doesn&apos;t &quot;open&quot; the refinement at the call site. Type projections (&lt;code&gt;X#T&lt;/code&gt;) require a concrete path, which a polymorphic
lambda&apos;s type parameter is not. As of now, abstract type members remain out of reach for &lt;code&gt;mapAs&lt;/code&gt;. If you know a workaround,
I&apos;d love to hear it.&lt;/p&gt;
&lt;h2&gt;Case Closed&lt;/h2&gt;
&lt;p&gt;We went from runtime &lt;code&gt;ClassCastException&lt;/code&gt; through match types, type classes, wrapper classes, opaque types,
and finally arrived at a one-liner using clause interleaving. Each step taught us something about Scala 3&apos;s type
system — and the last step reminded us to check if the language already has a simpler way.&lt;/p&gt;
&lt;p&gt;I&apos;d like to use &lt;code&gt;containsOnly&lt;/code&gt; and &lt;code&gt;mapAs&lt;/code&gt; patterns in &lt;a href=&quot;https://github.com/halotukozak/made&quot;&gt;M&amp;amp;DE&lt;/a&gt;, where typed
pipelines need to transform homogeneous tuples of domain objects while preserving full type information. If you&apos;re
building something similar, grab the code and adjust.&lt;/p&gt;
&lt;p&gt;PS. Thesis defended. What do normal people do with their free time?&lt;/p&gt;
&lt;h2&gt;References&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.scala-lang.org/scala3/reference/new-types/match-types.html&quot;&gt;Match Types&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.scala-lang.org/scala3/reference/other-new-features/opaques.html&quot;&gt;Opaque Type Aliases&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.scala-lang.org/sips/clause-interleaving.html&quot;&gt;SIP-47 — Clause Interleaving&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://scala-lang.org/api/3.x/scala/Tuple.html&quot;&gt;Tuple API&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.scala-lang.org/scala3/reference/new-types/polymorphic-function-types.html&quot;&gt;Polymorphic Function Types&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/halotukozak/made&quot;&gt;M&amp;amp;DE Repository&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded><author>Bartłomiej Kozak</author></item><item><title>Yes, You Can Debug a Scala 3 Macro</title><link>https://halotukozak.com/posts/yes-you-can-debug-a-scala-3-macro</link><guid isPermaLink="true">https://halotukozak.com/posts/yes-you-can-debug-a-scala-3-macro</guid><description>How to debug Scala 3 macros: profiling the compiler, print-debug helpers for types and ASTs, and attaching a JVM debugger to the compiler from Mill, sbt, scala-cli, VS Code or IntelliJ.</description><pubDate>Tue, 28 Apr 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;The State of Scala 3 Macro Docs&lt;/h2&gt;
&lt;p&gt;There is no good Scala 3 macro book. Most of what I know I picked up from forum threads, release notes or papers.
This post is my attempt to collect everything I&apos;ve figured out so far, plus the tips that aren&apos;t written down anywhere
else.&lt;/p&gt;
&lt;p&gt;This post wouldn&apos;t exist without my best friend &lt;a href=&quot;https://www.linkedin.com/in/bartoszbuczek/&quot;&gt;Bartek&lt;/a&gt;, who has a knack
for doing impossible things — mostly because nobody told him they were impossible.&lt;/p&gt;
&lt;h2&gt;Where to Learn From&lt;/h2&gt;
&lt;h3&gt;Tutorials&lt;/h3&gt;
&lt;p&gt;I&apos;d recommend starting with these four. They overlap in places, but each one introduces something the others skip:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://softwaremill.com/scala-3-macros-tips-and-tricks/&quot;&gt;SoftwareMill — Scala 3 Macros: Tips and Tricks&lt;/a&gt; — the best
starting point if you&apos;ve never touched a macro before.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://rockthejvm.com/articles/scala-3-macros-comprehensive-guide&quot;&gt;Rock the JVM — A Comprehensive Guide to Scala 3 Macros&lt;/a&gt;
— longest of the four, with worked examples.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://eed3si9n.com/intro-to-scala-3-macros/&quot;&gt;eed3si9n — Intro to Scala 3 Macros&lt;/a&gt; — concise, focused on the
quote/splice mental model.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.scala-lang.org/scala3/guides/macros/index.html&quot;&gt;Official Scala 3 Macros Guide&lt;/a&gt; and
the &lt;a href=&quot;https://docs.scala-lang.org/scala3/reference/metaprogramming/index.html&quot;&gt;Metaprogramming Reference&lt;/a&gt; — the
official source.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Papers&lt;/h3&gt;
&lt;p&gt;Once the tutorials stop helping, the papers explain &lt;em&gt;why&lt;/em&gt; the API looks the way it does. They&apos;re less scary than they
sound — skim the abstracts and intros first, and dip into the ones that match the question you&apos;re currently stuck on:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://infoscience.epfl.ch/handle/20.500.14299/193908&quot;&gt;Scala 3 Macros: A Technical Report&lt;/a&gt; — the design rationale.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://biboudis.github.io/papers/inlining-scala20.pdf&quot;&gt;Stojanov, Biboudis et al. — Inlining in Scala 3&lt;/a&gt; — how
&lt;code&gt;inline&lt;/code&gt; actually works.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://se.informatik.uni-tuebingen.de/publications/stucki21multistage.pdf&quot;&gt;Stucki et al. — Multi-Stage Programming with Generative and Analytical Macros&lt;/a&gt;
— the theoretical foundation for quotes and splices.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Quotes.scala Is Your Best Friend&lt;/h3&gt;
&lt;p&gt;You&apos;ll spend more time in
&lt;a href=&quot;https://github.com/scala/scala3/blob/main/library/src/scala/quoted/Quotes.scala&quot;&gt;&lt;code&gt;Quotes.scala&lt;/code&gt;&lt;/a&gt; than in any tutorial.
Every tree type and every available method is defined there.&lt;/p&gt;
&lt;p&gt;Each tree kind follows the same four-part shape: the type, the module object, a &lt;code&gt;TypeTest&lt;/code&gt; for pattern matching, and
an extension methods trait. For example:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/** Tree representing an if/then/else `if (...) ... else ...` in the source code. */
type If &amp;lt;: Term
/** Module object of `type If`. */
val If: IfModule

/** `TypeTest` that allows testing at runtime in a pattern match if a `Tree` is an `If`. */
given IfTypeTest: TypeTest[Tree, If]

/** Makes extension methods on `If` available without any imports. */
given IfMethods: IfMethods

/** Methods of the module object `val If`. */
trait IfModule {
  this: If.type =&amp;gt;
  def apply(cond: Term, thenp: Term, elsep: Term): If
  def copy(original: Tree)(cond: Term, thenp: Term, elsep: Term): If
  def unapply(tree: If): (Term, Term, Term)
}

/** Extension methods of `If`. */
trait IfMethods:
  extension (self: If)
    def cond: Term
    def thenp: Term
    def elsep: Term
    def isInline: Boolean
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Once you spot the pattern, the rest of the reflection API stops being a guessing game. The Scaladoc itself is packed
with hints about invariants, tree shapes and idiomatic usage — far more than the guides on the website.&lt;/p&gt;
&lt;p&gt;When &lt;code&gt;Quotes.scala&lt;/code&gt; runs out, the compiler source is the next stop. For example, I&apos;ve learned how to synthesise an
anonymous class by reading &lt;a href=&quot;https://github.com/scala/scala3/blob/6ac089a2528b67d0011cade2fb0f4d063fc26c74/compiler/src/dotty/tools/dotc/ast/tpd.scala&quot;&gt;
&lt;code&gt;tpd.scala&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;
&lt;h3&gt;A Note on IDEs&lt;/h3&gt;
&lt;p&gt;For macro-heavy code, VS Code with Metals has been less painful than IntelliJ. Sometimes (really sometimes) navigation
is better, error messages are clearer, and there are files that compile fine in Metals but IntelliJ refuses to
recognise. Syntax highlighting breaks on nested quotes; the formatter mangles code around splices. I still use
IntelliJ for everything else.&lt;/p&gt;
&lt;h2&gt;Measuring Where the Compiler Spends Time&lt;/h2&gt;
&lt;p&gt;Slow compiles and outright compiler hangs aren&apos;t things you can diagnose with &lt;code&gt;println&lt;/code&gt;. The built-in profiler shows
you exactly which phase and which expansion is eating the clock.&lt;/p&gt;
&lt;p&gt;Enable it via compiler options:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;-Yprofile-enabled
-Yprofile-trace:&amp;lt;path to output file&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This emits a trace in the Chrome/Perfetto format. Load it in &lt;a href=&quot;https://ui.perfetto.dev&quot;&gt;ui.perfetto.dev&lt;/a&gt; and you get a
flamegraph of phases, macro expansions, and type-check calls:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;yes-you-can-debug-a-scala-3-macro/perfetto-profile.png&quot; alt=&quot;Perfetto showing a compile-time profile trace of a Scala 3 macro&quot; /&gt;&lt;/p&gt;
&lt;p&gt;The option is documented exactly once, in a &lt;a href=&quot;https://www.scala-lang.org/news/3.6.3/&quot;&gt;3.6.3 release note&lt;/a&gt; — which is
how you find most Scala tooling features, and yes, it&apos;s as infuriating as it sounds.&lt;/p&gt;
&lt;h2&gt;Print Debugging, but Better&lt;/h2&gt;
&lt;p&gt;Everyone says &lt;code&gt;println&lt;/code&gt; is the only way to debug a macro, so I built myself a pile of utilities on top of it. This
one dumps everything the compiler knows about a type into a single error message:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;def dbg(using quotes: Quotes, printer: quotes.reflect.Printer[quotes.reflect.TypeRepr])(tpe: quotes.reflect.TypeRepr): Nothing =
  quotes.reflect.errorAndAbort(
    s&quot;&quot;&quot;
       |type: ${tpe.show}
       |widen: ${tpe.widen.show}
       |widenTermRefByName: ${tpe.widenTermRefByName.show}
       |widenByName: ${tpe.widenByName.show}
       |dealias: ${tpe.dealias.show}
       |dealiasKeepOpaques: ${tpe.dealiasKeepOpaques.show}
       |simplified: ${tpe.simplified.show}
       |classSymbol: ${tpe.classSymbol}
       |typeSymbol: ${tpe.typeSymbol}
       |termSymbol: ${tpe.termSymbol}
       |isSingleton: ${tpe.isSingleton}
       |baseClasses: ${tpe.baseClasses}
       |isFunctionType: ${tpe.isFunctionType}
       |isContextFunctionType: ${tpe.isContextFunctionType}
       |isErasedFunctionType: ${tpe.isErasedFunctionType}
       |isDependentFunctionType: ${tpe.isDependentFunctionType}
       |isTupleN: ${tpe.isTupleN}
       |typeArgs: ${tpe.typeArgs}
       |&quot;&quot;&quot;.stripMargin,
    )
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Or this one, for when you&apos;re guessing at an AST shape:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;inline def showRawAst(inline body: Any) = ${ showRawAstImpl(&apos;{ body }) }

def showRawAstImpl(body: Expr[Any])(using quotes: Quotes) =
  import quotes.reflect.*
  report.errorAndAbort(Printer.TreeStructure.show(body.asTerm.underlyingArgument))
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;showRawAst(someExpression)&lt;/code&gt; aborts compilation with the raw &lt;code&gt;Apply(Select(Ident(...), ...), ...)&lt;/code&gt; shape of the
expression. When you don&apos;t know which &lt;code&gt;quotes.reflect&lt;/code&gt; case to match on, it tells you.&lt;/p&gt;
&lt;h2&gt;Attaching a Real Debugger&lt;/h2&gt;
&lt;p&gt;My best friend, Bartek, didn&apos;t get the memo that &lt;code&gt;println&lt;/code&gt; was the only way to debug a macro.&lt;/p&gt;
&lt;p&gt;He (and his LLM) treated the compiler as just another
JVM process — attached a debugger, set breakpoints inside our macro implementations, and watched them fire on the
next compile.&lt;/p&gt;
&lt;p&gt;Written down like that it sounds obvious. Macros run during compilation, so the program to debug &lt;em&gt;is&lt;/em&gt; the Scala
compiler. The compile-side recipe is always the same: start the compiler&apos;s JVM with &lt;code&gt;-agentlib:jdwp=...&lt;/code&gt;, whether
you run Mill, sbt or scala-cli. What changes is how you configure the IDE side.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;yes-you-can-debug-a-scala-3-macro/vscode-debugger.png&quot; alt=&quot;VS Code attached to the Scala compiler, stopped on a breakpoint inside a macro implementation&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;Starting the Compile with JDWP&lt;/h3&gt;
&lt;p&gt;The flag:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;-agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=5005
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;suspend=y&lt;/code&gt; pauses the JVM until a debugger connects, so you can attach before anything expands. How to inject the
flag depends on the build tool:&lt;/p&gt;
&lt;h4&gt;mill&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;JAVA_OPTS=&quot;-agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=5005&quot; mill -i YourModule.compile
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;sbt&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;sbt -jvm-debug 5005
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;scala-cli&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;# stop any running Bloop daemon (otherwise it&apos;ll reuse the old JVM without the agent)
scala-cli --power bloop exit

# compile, passing JDWP args to the new Bloop JVM
scala-cli compile . --bloop-java-opt &quot;-agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=5005&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;All three end up the same way: a Scala compiler sitting on port &lt;code&gt;5005&lt;/code&gt;, waiting.&lt;/p&gt;
&lt;h3&gt;VS Code — launch.json&lt;/h3&gt;
&lt;p&gt;Add an attach configuration pointing at port &lt;code&gt;5005&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;version&quot;: &quot;0.2.0&quot;,
  &quot;configurations&quot;: [
    {
      &quot;type&quot;: &quot;scala&quot;,
      &quot;request&quot;: &quot;attach&quot;,
      &quot;name&quot;: &quot;Debug Macro (Compile-time)&quot;,
      &quot;hostName&quot;: &quot;localhost&quot;,
      &quot;port&quot;: 5005,
      &quot;buildTarget&quot;: &quot;YourModuleName&quot;
    }
  ]
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;buildTarget&lt;/code&gt; is the BSP target for the module whose sources contain your breakpoints. Metals lists them in the
status bar; it&apos;s usually the module name from your build file.&lt;/p&gt;
&lt;p&gt;Set breakpoints, start to compile, then Run -&amp;gt; Start Debugging.&lt;/p&gt;
&lt;h3&gt;IntelliJ IDEA — Remote JVM Debug&lt;/h3&gt;
&lt;p&gt;IntelliJ has no BSP-aware attach config for macros, so use the generic remote debugger:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Run -&amp;gt; Edit Configurations -&amp;gt; &lt;strong&gt;+&lt;/strong&gt; -&amp;gt; Remote JVM Debug.&lt;/li&gt;
&lt;li&gt;Host &lt;code&gt;localhost&lt;/code&gt;, port &lt;code&gt;5005&lt;/code&gt;, &quot;Attach to remote JVM&quot;, command line args for remote JVM auto-filled.&lt;/li&gt;
&lt;li&gt;Start to compile.&lt;/li&gt;
&lt;li&gt;Run -&amp;gt; Debug -&amp;gt; your new configuration.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&quot;yes-you-can-debug-a-scala-3-macro/intellij-debugger.png&quot; alt=&quot;IntelliJ IDEA Remote JVM Debug configuration attached to the Scala compiler on port 5005&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;Case Closed&lt;/h2&gt;
&lt;p&gt;There is no official &quot;how to debug a Scala 3 macro&quot; page. Until there is, I hope this one saves someone time.
See ya next time.&lt;/p&gt;
&lt;h2&gt;References&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://softwaremill.com/scala-3-macros-tips-and-tricks/&quot;&gt;SoftwareMill — Scala 3 Macros: Tips and Tricks&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://rockthejvm.com/articles/scala-3-macros-comprehensive-guide&quot;&gt;Rock the JVM — A Comprehensive Guide to Scala 3 Macros&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://eed3si9n.com/intro-to-scala-3-macros/&quot;&gt;eed3si9n — Intro to Scala 3 Macros&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.scala-lang.org/scala3/guides/macros/index.html&quot;&gt;Official Scala 3 Macros Guide&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.scala-lang.org/scala3/reference/metaprogramming/index.html&quot;&gt;Metaprogramming Reference&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/scala/scala3/blob/main/library/src/scala/quoted/Quotes.scala&quot;&gt;&lt;code&gt;Quotes.scala&lt;/code&gt; source&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://ui.perfetto.dev&quot;&gt;Perfetto Trace Viewer&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.scala-lang.org/news/3.6.3/&quot;&gt;Scala 3.6.3 Release Notes — profiler&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://scala-cli.virtuslab.org/docs/cookbooks/introduction/debugging/&quot;&gt;scala-cli Debugging Cookbook&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/scalacenter/scala-debug-adapter&quot;&gt;Scala Debug Adapter Protocol&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded><author>Bartłomiej Kozak</author></item><item><title>Magical Kotlin: Building a Type-Safe Validation DSL</title><link>https://halotukozak.com/posts/magical-kotlin</link><guid isPermaLink="true">https://halotukozak.com/posts/magical-kotlin</guid><description>A walking tour of context parameters, KProperty0, definitely-non-null types, contracts, sealed scopes, explicit backing fields, fun interface, inline+reified and KSP — by building a multiplatform validation DSL from scratch.</description><pubDate>Fri, 12 Jun 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;Kotlin Is Getting Tricky&lt;/h2&gt;
&lt;p&gt;Scala has the reputation for complexity on the JVM.
Kotlin is usually seen as the pragmatic, safe, slightly boring alternative.
I think that is changing.
With a handful of recent features — context parameters, definitely-non-null types, contracts — Kotlin can be just as
clever, and just as fun to abuse.&lt;/p&gt;
&lt;p&gt;To show how these compose, I built a small validation library called &lt;a href=&quot;https://github.com/halotukozak/sure&quot;&gt;&lt;code&gt;sure&lt;/code&gt;&lt;/a&gt;.
It is Kotlin Multiplatform, and the public API reads like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Validatable
data class Address(val city: String, val zip: String) {
    companion object {
        val validator = Validator&amp;lt;Address&amp;gt; {
            field(::city) { notBlank() }
            field(::zip) { lengthIn(5..5) }
        }
    }
}

@Validatable
data class User(val name: String, val age: Int, val address: Address) {
    companion object {
        val validator = Validator&amp;lt;User&amp;gt; {
            field(::name) { notBlank(); lengthIn(1..50) }
            field(::age) { inRange(0..150) }
            validated(::address)   // reuses Address&apos;s own validator, resolved by type
        }
    }
}

User(&quot;alice&quot;, 30, Address(&quot;Kraków&quot;, &quot;30001&quot;)).validate() // ValidationResult.Valid
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;No reflection on user types, no string field names — and that &lt;code&gt;validate()&lt;/code&gt; isn&apos;t hand-written.
A KSP processor generates it at compile time; everything between here and there is how the pieces it relies on
get built.&lt;/p&gt;
&lt;h3&gt;A Note on Valiktor&lt;/h3&gt;
&lt;p&gt;What I built looks a lot like &lt;a href=&quot;https://github.com/valiktor/valiktor&quot;&gt;Valiktor&lt;/a&gt;.
I found it after I&apos;d written most of this.
Valiktor predates context parameters and definitely-non-null types, so its API carries more boilerplate.&lt;/p&gt;
&lt;h2&gt;Setup&lt;/h2&gt;
&lt;p&gt;Everything below uses Kotlin &lt;strong&gt;2.4.0&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;I&apos;ll build the library from scratch — one feature at a time, starting from the simplest function that works and pulling
in a new mechanism only when the previous step&apos;s pain forces it, until we reach the final API.
The same example types ride through every step:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;data class Address(val city: String, val zip: String)
data class User(val name: String, val age: Int, val address: Address)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Step 1: The hand-rolled version&lt;/h2&gt;
&lt;p&gt;The simplest thing that could work — one function:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;fun validateUser(user: User): List&amp;lt;String&amp;gt; {
    val errors = mutableListOf&amp;lt;String&amp;gt;()

    when {
        user.name.isBlank() -&amp;gt; errors += &quot;name: must not be blank&quot;
        user.name.length !in 1..50 -&amp;gt; errors += &quot;name: must be 1..50 characters&quot;
    }
    if (user.age !in 0..150) errors += &quot;age: must be in 0..150&quot;

    if (user.address.city.isBlank()) errors += &quot;address.city: must not be blank&quot;
    if (user.address.zip.length != 5) errors += &quot;address.zip: must be 5 digits&quot;

    return errors
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;It works, but it hurts.
The error list is hand-threaded through every branch.
Field names are strings.
Each rule re-states the field it talks about.
And the nested &lt;code&gt;Address&lt;/code&gt; doesn&apos;t compose — every check on it re-states the &lt;code&gt;address.&lt;/code&gt; prefix by hand, and a second level
of nesting would mean another prefix on top, with nothing to stop a typo in either.&lt;/p&gt;
&lt;h2&gt;Step 2: A scope to hold the errors&lt;/h2&gt;
&lt;p&gt;The first pain is the most basic: the error list is a local variable I pass around by hand.
I&apos;ll move it into an object that travels with the validation — a &lt;em&gt;scope&lt;/em&gt;.
The scope holds the value being checked and owns the error list, so rules just call &lt;code&gt;raise&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class ValidationScope&amp;lt;T&amp;gt;(val value: T) {
    val errors: List&amp;lt;String&amp;gt;
        field = mutableListOf()
    fun raise(message: String) {
        errors += message
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That &lt;code&gt;field = mutableListOf()&lt;/code&gt; under the property is not a typo — it&apos;s an &lt;strong&gt;explicit backing field&lt;/strong&gt;, stable as of
Kotlin 2.4 (experimental behind &lt;code&gt;-Xexplicit-backing-fields&lt;/code&gt; in 2.3). It lets a &lt;code&gt;val&lt;/code&gt; declare two types: a public one and
the one the field actually holds.
Here the public type is &lt;code&gt;List&amp;lt;String&amp;gt;&lt;/code&gt;, but inside the class the name &lt;code&gt;errors&lt;/code&gt; resolves to the &lt;code&gt;MutableList&lt;/code&gt; behind it —
so callers get a read-only view while &lt;code&gt;raise&lt;/code&gt; still appends, with no &quot;private &lt;code&gt;_errors&lt;/code&gt; plus public getter &lt;code&gt;erorrs&lt;/code&gt;&quot;
template.&lt;/p&gt;
&lt;p&gt;To run rules against a scope, I take a lambda with the scope as its receiver — that lambda &lt;em&gt;is&lt;/em&gt; the validator:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class Validator&amp;lt;T&amp;gt;(private val rules: ValidationScope&amp;lt;T&amp;gt;.() -&amp;gt; Unit) {
    fun validate(value: T): List&amp;lt;String&amp;gt; {
        val scope = ValidationScope(value)
        scope.rules()
        return scope.errors
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The call site already reads better — no list to declare, no list to return:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;val userValidator = Validator&amp;lt;User&amp;gt; {
    if (value.name.isBlank()) raise(&quot;name: must not be blank&quot;)
    if (value.age !in 0..150) raise(&quot;age: must be in 0..150&quot;)
    if (value.address.city.isBlank()) raise(&quot;address.city: must not be blank&quot;)
    if (value.address.zip.length != 5) raise(&quot;address.zip: must be 5 digits&quot;)
}

userValidator.validate(User(&quot;&quot;, 200, Address(&quot;Kraków&quot;, &quot;30&quot;)))
// [name: must not be blank, age: must be in 0..150, address.zip: must be 5 digits]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;rules: ValidationScope&amp;lt;T&amp;gt;.() -&amp;gt; Unit&lt;/code&gt; is a &lt;em&gt;function with receiver&lt;/em&gt;: inside the braces, &lt;code&gt;this&lt;/code&gt; is the scope, so &lt;code&gt;value&lt;/code&gt;
and &lt;code&gt;raise&lt;/code&gt; resolve unqualified.
Still, the messages are strings and I&apos;m reaching into &lt;code&gt;value.name&lt;/code&gt; while re-typing &lt;code&gt;&quot;name:&quot;&lt;/code&gt; by hand.
That duplication is the next pain.&lt;/p&gt;
&lt;h2&gt;Step 3: KProperty0 — name the field once&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;&quot;name&quot;&lt;/code&gt; (the label) and &lt;code&gt;value.name&lt;/code&gt; (the read) are the same field spelled twice.
I want to name it once.
Kotlin has bound property references for exactly this: &lt;code&gt;user::name&lt;/code&gt; is a &lt;code&gt;KProperty0&amp;lt;String&amp;gt;&lt;/code&gt; that carries both the
property&apos;s &lt;code&gt;name&lt;/code&gt; and a zero-argument &lt;code&gt;get()&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;To use them, the rules block hands its value back to me, and the scope grows a parent link and a path so a field can
descend into a child scope that still reports into the same error list:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class Validator&amp;lt;T&amp;gt;(private val rules: ValidationScope&amp;lt;T&amp;gt;.(T) -&amp;gt; Unit) {
    fun validate(value: T): List&amp;lt;String&amp;gt; {
        val scope = ValidationScope(value)
        scope.rules(value)
        return scope.errors
    }
}

class ValidationScope&amp;lt;T&amp;gt;(
    val value: T,
    private val parent: ValidationScope&amp;lt;*&amp;gt;? = null,
    val path: String = &quot;&quot;,
) {
    val errors: MutableList&amp;lt;String&amp;gt; = parent?.errors ?: mutableListOf()
    fun raise(message: String) {
        errors += if (path.isEmpty()) message else &quot;$path: $message&quot;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;field&lt;/code&gt; takes the property reference, reads it once, and runs the block against a child scope:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;fun &amp;lt;F&amp;gt; ValidationScope&amp;lt;*&amp;gt;.field(property: KProperty0&amp;lt;F&amp;gt;, block: ValidationScope&amp;lt;F&amp;gt;.(F) -&amp;gt; Unit) {
    val childPath = if (path.isEmpty()) property.name else &quot;$path.${property.name}&quot;
    val child = ValidationScope(property.get(), this, childPath)
    child.block(property.get())
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The leaf checks are extensions on the scope they apply to, so &lt;code&gt;notBlank()&lt;/code&gt; exists only on a &lt;code&gt;ValidationScope&amp;lt;String&amp;gt;&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;fun ValidationScope&amp;lt;String&amp;gt;.notBlank() {
    if (value.isBlank()) raise(&quot;must not be blank&quot;)
}

fun &amp;lt;T : Comparable&amp;lt;T&amp;gt;&amp;gt; ValidationScope&amp;lt;T&amp;gt;.inRange(range: ClosedRange&amp;lt;T&amp;gt;) {
    if (value !in range) raise(&quot;must be in $range&quot;)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The field name is written once now, as a bound reference:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;val userValidator = Validator&amp;lt;User&amp;gt; { user -&amp;gt;
    field(user::name) { notBlank() }
    field(user::age) { inRange(0..150) }
    field(user::address) { address -&amp;gt;
        field(address::city) { notBlank() }
        field(address::zip) { notBlank() }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Better — but two things still grate.
The block has to name its value (&lt;code&gt;user -&amp;gt;&lt;/code&gt;) and every reference repeats it (&lt;code&gt;user::name&lt;/code&gt;, &lt;code&gt;user::age&lt;/code&gt;).
And each check is welded to one receiver: &lt;code&gt;field&lt;/code&gt;, &lt;code&gt;notBlank&lt;/code&gt;, &lt;code&gt;inRange&lt;/code&gt; are all extensions on &lt;code&gt;ValidationScope&lt;/code&gt;, so a
function can require exactly one scope and nothing more.&lt;/p&gt;
&lt;h2&gt;Step 4: Context parameters — drop the receiver juggling&lt;/h2&gt;
&lt;p&gt;I&apos;d rather write &lt;code&gt;field(::name)&lt;/code&gt; than &lt;code&gt;field(user::name)&lt;/code&gt;, and lose the &lt;code&gt;user -&amp;gt;&lt;/code&gt;.
For &lt;code&gt;::name&lt;/code&gt; to resolve, &lt;code&gt;this&lt;/code&gt; inside the block has to be the &lt;code&gt;User&lt;/code&gt; — but the block still has to reach the scope, to
&lt;code&gt;raise&lt;/code&gt;.
That&apos;s two receivers at once, and an ordinary &lt;code&gt;T.() -&amp;gt; Unit&lt;/code&gt; only gives you one.&lt;/p&gt;
&lt;p&gt;Carrying an implicit dependency like this is one of the things &lt;strong&gt;context parameters&lt;/strong&gt; are good for.
A context parameter is a dependency a function declares without making it &lt;em&gt;the&lt;/em&gt; receiver.
The rules block becomes an extension on the value, with a scope available in context:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class Validator&amp;lt;T&amp;gt;(private val rules: context(ValidationScope&amp;lt;T&amp;gt;) T.() -&amp;gt; Unit) {
    fun validate(value: T): List&amp;lt;String&amp;gt; {
        val scope = ValidationScope(value)
        rules(scope, value)
        return scope.errors
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now &lt;code&gt;this&lt;/code&gt; is the &lt;code&gt;User&lt;/code&gt;, so &lt;code&gt;::name&lt;/code&gt; resolves, while the &lt;code&gt;ValidationScope&amp;lt;User&amp;gt;&lt;/code&gt; rides along in context — no &lt;code&gt;user -&amp;gt;&lt;/code&gt;.
&lt;code&gt;field&lt;/code&gt; declares the scope it needs as a context parameter instead of an extension receiver:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;context(scope: ValidationScope&amp;lt;*&amp;gt;)
fun &amp;lt;F&amp;gt; field(property: KProperty0&amp;lt;F&amp;gt;, block: ValidationScope&amp;lt;F&amp;gt;.(F) -&amp;gt; Unit) {
    val childPath = if (scope.path.isEmpty()) property.name else &quot;${scope.path}.${property.name}&quot;
    val child = ValidationScope(property.get(), scope, childPath)
    child.block(property.get())
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The checks move from receiver to context too, sharing one &lt;code&gt;check&lt;/code&gt; helper. Note the predicate flags the &lt;em&gt;failure&lt;/em&gt;: when
it returns &lt;code&gt;true&lt;/code&gt;, the value is bad and &lt;code&gt;check&lt;/code&gt; raises — so each leaf describes what&apos;s wrong, not what&apos;s allowed.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;context(scope: ValidationScope&amp;lt;T&amp;gt;)
fun &amp;lt;T&amp;gt; check(predicate: (T) -&amp;gt; Boolean, onError: (T) -&amp;gt; String) {
    if (predicate(scope.value)) scope.raise(onError(scope.value))   // predicate true → raise
}

context(_: ValidationScope&amp;lt;String&amp;gt;)
fun notBlank() = check&amp;lt;String&amp;gt;({ it.isBlank() }) { &quot;must not be blank&quot; }

context(_: ValidationScope&amp;lt;String&amp;gt;)
fun lengthIn(range: IntRange) = check&amp;lt;String&amp;gt;({ it.length !in range }) { &quot;must be $range characters&quot; }

context(_: ValidationScope&amp;lt;T&amp;gt;)
fun &amp;lt;T : Comparable&amp;lt;T&amp;gt;&amp;gt; inRange(range: ClosedRange&amp;lt;T&amp;gt;) = check({ it !in range }) { &quot;must be in $range&quot; }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The top-level call site loses both the &lt;code&gt;user -&amp;gt;&lt;/code&gt; and the repeated receiver — only a nested &lt;code&gt;field&lt;/code&gt; still names its value
(&lt;code&gt;address -&amp;gt;&lt;/code&gt;), because a &lt;code&gt;field&lt;/code&gt; block hands the value in as its lambda argument rather than as &lt;code&gt;this&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;val userValidator = Validator&amp;lt;User&amp;gt; {
    field(::name) { notBlank() }
    field(::age) { inRange(0..150) }
    field(::address) { address -&amp;gt;
        field(address::city) { notBlank() }
        field(address::zip) { lengthIn(5..5) }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The one trap worth calling out: a context parameter is &lt;em&gt;not&lt;/em&gt; a second &lt;code&gt;this&lt;/code&gt;. If you remember the old &lt;em&gt;context
receivers&lt;/em&gt;, those did add a second receiver — context &lt;strong&gt;parameters&lt;/strong&gt; don&apos;t. Inside &lt;code&gt;Validator&amp;lt;User&amp;gt; { … }&lt;/code&gt; there&apos;s a
single &lt;code&gt;this&lt;/code&gt; (the &lt;code&gt;User&lt;/code&gt;), so &lt;code&gt;::name&lt;/code&gt; resolves against it, while the scope just rides along:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Inside…&lt;/th&gt;
&lt;th&gt;the single &lt;code&gt;this&lt;/code&gt; is…&lt;/th&gt;
&lt;th&gt;the scope is…&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Validator&amp;lt;User&amp;gt; { … }&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;the &lt;code&gt;User&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;an ambient &lt;code&gt;context(ValidationScope&amp;lt;User&amp;gt;)&lt;/code&gt; parameter — &lt;em&gt;not&lt;/em&gt; a &lt;code&gt;this&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;field(::name) { … }&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;the &lt;code&gt;ValidationScope&amp;lt;String&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;the receiver itself (&lt;code&gt;field&lt;/code&gt;&apos;s block is &lt;code&gt;ValidationScope&amp;lt;F&amp;gt;.(F)&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;You can&apos;t call the scope&apos;s members directly — &lt;code&gt;raise(&quot;…&quot;)&lt;/code&gt; won&apos;t resolve in the outer block, and there&apos;s no
&lt;code&gt;this@ValidationScope&lt;/code&gt; label. Its only job is to &lt;em&gt;satisfy other functions that ask for one&lt;/em&gt;: when &lt;code&gt;notBlank()&lt;/code&gt; declares
&lt;code&gt;context(_: ValidationScope&amp;lt;String&amp;gt;)&lt;/code&gt;, the compiler finds the nearest matching value and wires it in. It&apos;s resolution,
not dispatch, and at the JVM level it lowers to an ordinary leading argument. A function can also ask for several
context parameters at once — which is what lets a &lt;code&gt;field&lt;/code&gt; block stay a plain extension on the scope while the value
arrives as the lambda argument.&lt;/p&gt;
&lt;h2&gt;Step 5: Nesting — the sealed scope family and the value contract&lt;/h2&gt;
&lt;p&gt;Stacking &lt;code&gt;field&lt;/code&gt; already nests one object in another — &lt;code&gt;address.city&lt;/code&gt; works because each call extends the parent&apos;s path
with &lt;code&gt;.name&lt;/code&gt;.
The real test is collections, where errors must still report a sensible path — &lt;code&gt;tags[2]&lt;/code&gt;, &lt;code&gt;headers[Accept]&lt;/code&gt; — but each
kind of descent builds that path differently.
A single &lt;code&gt;ValidationScope&lt;/code&gt; class can&apos;t express &quot;append &lt;code&gt;.name&lt;/code&gt;&quot; vs &quot;append &lt;code&gt;[index]&lt;/code&gt;&quot; vs &quot;append &lt;code&gt;[key]&lt;/code&gt;&quot; cleanly, so I
split it into a small two-tier family.
The base declares the contract; one intermediate owns the error list, the other forwards errors to its parent; and a
leaf per nesting kind only has to say how it builds its &lt;code&gt;path&lt;/code&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@ValidationDsl
sealed class ValidationScope&amp;lt;out T&amp;gt; {
    internal abstract val path: String
    internal abstract fun addError(error: ValidationError)

    fun raise(message: String) = addError(ValidationError(path, message))
}

// owns the error list — RootScope, and the throwaway EphemeralScope from Step 8
sealed class ParentScope&amp;lt;out T&amp;gt; : ValidationScope&amp;lt;T&amp;gt;() {
    val errors: List&amp;lt;ValidationError&amp;gt;
        field = mutableListOf()
    final override fun addError(error: ValidationError) {
        errors += error
    }
}

// no list of its own — forwards every error up to its parent
sealed class ChildrenScope&amp;lt;out T&amp;gt; : ValidationScope&amp;lt;T&amp;gt;() {
    abstract val parent: ValidationScope&amp;lt;*&amp;gt;
    final override fun addError(error: ValidationError) = parent.addError(error)
}

internal class RootScope&amp;lt;out T&amp;gt;(val value: T) : ParentScope&amp;lt;T&amp;gt;() {
    override val path = &quot;&quot;
}

internal class FieldScope&amp;lt;out T&amp;gt;(
    val value: T,
    name: String,
    override val parent: ValidationScope&amp;lt;*&amp;gt;,
) : ChildrenScope&amp;lt;T&amp;gt;() {
    override val path = if (parent.path.isEmpty()) name else &quot;${parent.path}.$name&quot;
}

internal class ItemScope&amp;lt;out T&amp;gt;(
    val value: T,
    index: Int,
    override val parent: ValidationScope&amp;lt;*&amp;gt;,
) : ChildrenScope&amp;lt;T&amp;gt;() {
    override val path = &quot;${parent.path}[$index]&quot;
}

internal class EntryScope&amp;lt;out T&amp;gt;(
    val value: T,
    key: Any?,
    override val parent: ValidationScope&amp;lt;*&amp;gt;,
) : ChildrenScope&amp;lt;T&amp;gt;() {
    override val path = &quot;${parent.path}[$key]&quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Two intermediate classes carry all the plumbing.
&lt;code&gt;ParentScope&lt;/code&gt; owns the list — behind the explicit backing field from Step 2, so &lt;code&gt;addError&lt;/code&gt; appends internally while
callers only read — and &lt;code&gt;ChildrenScope&lt;/code&gt; forwards &lt;code&gt;addError&lt;/code&gt; to its &lt;code&gt;parent&lt;/code&gt;, so every nested error bubbles up to the one
&lt;code&gt;RootScope&lt;/code&gt; at the top.
A leaf then declares only its &lt;code&gt;path&lt;/code&gt; and holds its &lt;code&gt;value&lt;/code&gt;; &lt;code&gt;raise&lt;/code&gt; lives once, on the base.
Both intermediates and the base are &lt;code&gt;sealed&lt;/code&gt;, which matters for the accessor next.&lt;/p&gt;
&lt;p&gt;Errors are now a small type instead of a bare string, so the path rides along (this becomes a sealed type with &lt;code&gt;Field&lt;/code&gt;/
&lt;code&gt;Element&lt;/code&gt;/&lt;code&gt;Root&lt;/code&gt; cases in &lt;a href=&quot;#step-11-structured-translatable-messages--fun-interface&quot;&gt;Step 11&lt;/a&gt;):&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;data class ValidationError(val path: String, val message: String)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;The value contract&lt;/h3&gt;
&lt;p&gt;Start with the payoff, because the syntax below only makes sense once you know what it buys.
Picture a user validating a nullable field — &lt;code&gt;optional(::nickname) { … }&lt;/code&gt; where &lt;code&gt;nickname: String?&lt;/code&gt;. Inside that block
I want to call &lt;code&gt;notBlank()&lt;/code&gt;, but &lt;code&gt;notBlank()&lt;/code&gt; only exists on &lt;code&gt;ValidationScope&amp;lt;String&amp;gt;&lt;/code&gt;, not &lt;code&gt;ValidationScope&amp;lt;String?&amp;gt;&lt;/code&gt;.
Without help I&apos;d be writing &lt;code&gt;value!!&lt;/code&gt; or &lt;code&gt;value?.let { … }&lt;/code&gt; at every single check — exactly the null-noise this library
exists to delete. What I want instead: one null check at the top of the block, and the compiler treats the value as a
plain &lt;code&gt;String&lt;/code&gt; for the rest — no &lt;code&gt;!!&lt;/code&gt;, no &lt;code&gt;?&lt;/code&gt;, no cast.&lt;/p&gt;
&lt;p&gt;A &lt;strong&gt;contract&lt;/strong&gt; delivers that. It&apos;s a promise the getter makes to the compiler, stated in the &lt;code&gt;contract { }&lt;/code&gt; block:
&lt;em&gt;when this getter returns a non-null value, treat the receiver as a scope of the non-null type&lt;/em&gt; — written
&lt;code&gt;returnsNotNull() implies (this@value is ValidationScope&amp;lt;T &amp;amp; Any&amp;gt;)&lt;/code&gt;. That &lt;code&gt;T &amp;amp; Any&lt;/code&gt; is a &lt;strong&gt;definitely-non-null type&lt;/strong&gt;:
&quot;the non-null version of an unbounded generic &lt;code&gt;T&lt;/code&gt;&quot; — for nullable &lt;code&gt;T = X?&lt;/code&gt; it&apos;s &lt;code&gt;X&lt;/code&gt;, for already-non-null &lt;code&gt;T&lt;/code&gt; it
collapses to &lt;code&gt;T&lt;/code&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@OptIn(ExperimentalContracts::class)
val &amp;lt;T&amp;gt; ValidationScope&amp;lt;T&amp;gt;.value: T
    get() {
        contract {
            returnsNotNull() implies (this@value is ValidationScope&amp;lt;T &amp;amp; Any&amp;gt;)
        }

        return when (this) {
            is RootScope -&amp;gt; value
            is FieldScope -&amp;gt; value
            is ItemScope -&amp;gt; value
            is EntryScope -&amp;gt; value
            is EphemeralScope -&amp;gt; value
        }
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Why an extension and not a member?
The base scope owns no &lt;code&gt;value&lt;/code&gt; — each leaf declares its own — so as a member, &lt;code&gt;value&lt;/code&gt; would have to be &lt;code&gt;abstract&lt;/code&gt; on
&lt;code&gt;ValidationScope&lt;/code&gt;. And Kotlin forbids contracts on &lt;code&gt;abstract&lt;/code&gt; (or &lt;code&gt;open&lt;/code&gt;) declarations: a &lt;code&gt;contract { }&lt;/code&gt; describes one
concrete body, but an abstract member has none, and an open one could be overridden out from under its promise. An
extension is neither — it&apos;s a single final function with a real body, the exhaustive &lt;code&gt;when&lt;/code&gt; that reconstructs &lt;code&gt;value&lt;/code&gt;
from whichever leaf &lt;code&gt;this&lt;/code&gt; happens to be. That&apos;s the form a &lt;code&gt;contract { }&lt;/code&gt; is allowed on, and its receiver parameter is
what &lt;code&gt;returnsNotNull() implies (this@value is ValidationScope&amp;lt;T &amp;amp; Any&amp;gt;)&lt;/code&gt; smart-casts.&lt;/p&gt;
&lt;p&gt;In Step 7 this same contract lets one helper handle nullable and non-nullable fields with no cast.&lt;/p&gt;
&lt;p&gt;The collection combinators each spin up the matching scope, so a deep failure still reports the right path:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;context(scope: ValidationScope&amp;lt;List&amp;lt;T&amp;gt;&amp;gt;)
inline fun &amp;lt;T : Any&amp;gt; eachItem(rule: ValidationScope&amp;lt;T&amp;gt;.(T) -&amp;gt; Unit) {
    scope.value.forEachIndexed { index, item -&amp;gt;
        ItemScope(item, index, scope).rule(item)
    }
}

context(scope: ValidationScope&amp;lt;Map&amp;lt;K, V&amp;gt;&amp;gt;)
inline fun &amp;lt;K, V : Any&amp;gt; eachValue(rule: ValidationScope&amp;lt;V&amp;gt;.(V) -&amp;gt; Unit) {
    for ((k, v) in scope.value) EntryScope(v, k, scope).rule(v)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A failure inside &lt;code&gt;eachItem&lt;/code&gt; now reports &lt;code&gt;tags[2]: must not be blank&lt;/code&gt;, because &lt;code&gt;ItemScope&lt;/code&gt; built that path from its
parent&apos;s.&lt;/p&gt;
&lt;h2&gt;Step 6: @DslMarker — closing the leak&lt;/h2&gt;
&lt;p&gt;Now that scopes nest, the DSL has a quiet bug.
Inside a nested block, &lt;code&gt;this&lt;/code&gt; is the inner scope — but the enclosing scope is still in lexical reach, and both expose
&lt;code&gt;raise&lt;/code&gt;, &lt;code&gt;addError&lt;/code&gt;, and friends.
A member meant for the outer scope can silently resolve there from an inner block, attaching an error at the wrong path,
with no error from the compiler.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;@DslMarker&lt;/code&gt; is the fence against this.
Annotate the scope type and the compiler forbids an &lt;em&gt;implicit&lt;/em&gt; call that would skip past the nearest receiver to an
outer one of the same marker:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@DslMarker
annotation class ValidationDsl
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That&apos;s the annotation already sitting on &lt;code&gt;ValidationScope&lt;/code&gt; back in Step 5.
After it, reaching an outer scope from a nested block is a compile error unless you qualify it explicitly (
&lt;code&gt;this@Validator.raise(...)&lt;/code&gt;).
You only ever see the innermost scope by default — which is what you wanted.&lt;/p&gt;
&lt;h2&gt;Step 7: Nullable fields — field vs optional&lt;/h2&gt;
&lt;p&gt;So far &lt;code&gt;field&lt;/code&gt; assumed a non-null property.
Real DTOs have nullable fields, with two sensible behaviors: &lt;em&gt;skip if null&lt;/em&gt; (&lt;code&gt;optional&lt;/code&gt;), or &lt;em&gt;fail if null&lt;/em&gt; (a required
field that happens to be nullable).
The definitely-non-null type from Step 5 carries the whole signature:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;context(scope: ValidationScope&amp;lt;*&amp;gt;)
inline fun &amp;lt;F : Any&amp;gt; field(
    property: KProperty0&amp;lt;F&amp;gt;,
    block: ValidationScope&amp;lt;F&amp;gt;.(F) -&amp;gt; Unit = {},
) {
    val value = property.get()
    FieldScope(value, property.name, scope).block(value)
}

context(scope: ValidationScope&amp;lt;*&amp;gt;)
inline fun &amp;lt;F : Any&amp;gt; optional(
    property: KProperty0&amp;lt;F?&amp;gt;,
    block: ValidationScope&amp;lt;F&amp;gt;.(F) -&amp;gt; Unit,
) {
    val value = property.get()
    if (value != null) {
        FieldScope(value, property.name, scope).block(value)
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;optional&lt;/code&gt; takes a &lt;code&gt;KProperty0&amp;lt;F?&amp;gt;&lt;/code&gt; and, inside the null check, the value narrows to &lt;code&gt;F&lt;/code&gt;.
&lt;code&gt;field&lt;/code&gt; constrains its block to &lt;code&gt;F : Any&lt;/code&gt;, so even on a nullable property the rules see a non-null value.
No casts anywhere — &lt;code&gt;F &amp;amp; Any&lt;/code&gt; does the narrowing in the type, the Step 5 contract does it on the value.&lt;/p&gt;
&lt;h2&gt;Step 8: Combinators that need a private scope — anyOf / not&lt;/h2&gt;
&lt;p&gt;Some combinators don&apos;t want their inner errors recorded — they only care &lt;em&gt;whether&lt;/em&gt; the inner rules passed.
&lt;code&gt;anyOf&lt;/code&gt; succeeds if any branch is clean; &lt;code&gt;not&lt;/code&gt; succeeds if its rule fails.
Both run rules against a throwaway scope whose errors never reach the parent.
This is the fifth scope, and it falls straight out of the Step 5 split: it &lt;em&gt;owns&lt;/em&gt; its errors rather than forwarding
them, so it&apos;s a &lt;code&gt;ParentScope&lt;/code&gt;, and that&apos;s the entire definition — the list, the backing field, and &lt;code&gt;addError&lt;/code&gt; are all
inherited.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;internal class EphemeralScope&amp;lt;out T&amp;gt;(
    val value: T,
    parent: ValidationScope&amp;lt;*&amp;gt;,
) : ParentScope&amp;lt;T&amp;gt;() {
    override val path = parent.path
}

context(scope: ValidationScope&amp;lt;T&amp;gt;)
fun &amp;lt;T&amp;gt; anyOf(vararg rules: ValidationScope&amp;lt;T&amp;gt;.() -&amp;gt; Unit, message: () -&amp;gt; String) {
    val anyValid = rules.any { rule -&amp;gt;
        val isolated = EphemeralScope(scope.value, scope)
        isolated.rule()
        isolated.errors.isEmpty()
    }
    if (!anyValid) scope.raise(message())
}

context(scope: ValidationScope&amp;lt;T&amp;gt;)
fun &amp;lt;T&amp;gt; not(rule: ValidationScope&amp;lt;T&amp;gt;.() -&amp;gt; Unit, message: () -&amp;gt; String) {
    val isolated = EphemeralScope(scope.value, scope)
    isolated.rule()
    if (isolated.errors.isEmpty()) scope.raise(message())   // rule passed → negation fails
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;EphemeralScope&lt;/code&gt; keeps its own list instead of delegating up, so the parent never sees the trial-run errors.&lt;/p&gt;
&lt;p&gt;The rule language is done: nesting with correct paths, &lt;code&gt;@DslMarker&lt;/code&gt; safety, &lt;code&gt;optional&lt;/code&gt;/&lt;code&gt;field&lt;/code&gt; for
nullables, and &lt;code&gt;anyOf&lt;/code&gt;/&lt;code&gt;not&lt;/code&gt;. Every pain from the hand-rolled Step 1 function is now gone.&lt;/p&gt;
&lt;p&gt;The remaining steps change register. Steps 1–8 fixed &lt;em&gt;pains&lt;/em&gt;; from here on the rules are settled and the work is
dressing them in a public API — make &lt;code&gt;Validator&lt;/code&gt; findable by type, translate messages, and finally generate
&lt;code&gt;validate()&lt;/code&gt;. Same library, outer layer.&lt;/p&gt;
&lt;h2&gt;Step 9: The Validator class — inline, reified, noinline&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;Validator&lt;/code&gt; has been a thin wrapper.
I want three things from it: construct it as &lt;code&gt;Validator&amp;lt;User&amp;gt; { … }&lt;/code&gt;, look it up later by type, and validate an
arbitrary value.
Looking up by type means storing &lt;code&gt;T::class&lt;/code&gt;, which means the type must survive to runtime — &lt;code&gt;reified&lt;/code&gt; — and that forces
the constructing function &lt;code&gt;inline&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;open class Validator&amp;lt;T&amp;gt;(
    protected val kClass: KClass&amp;lt;T &amp;amp; Any&amp;gt;,
    internal val applyRules: ValidationScope&amp;lt;T&amp;gt;.() -&amp;gt; Unit,
) {
    companion object {
        inline operator fun &amp;lt;reified T : Any&amp;gt; invoke(
            noinline rules: context(ValidationScope&amp;lt;T&amp;gt;) T.() -&amp;gt; Unit,
        ): Validator&amp;lt;T&amp;gt; = Validator(T::class) { rules(value) }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Three deliberate keywords:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;reified T&lt;/code&gt;&lt;/strong&gt; keeps the type at runtime, so &lt;code&gt;T::class&lt;/code&gt; is a real &lt;code&gt;KClass&lt;/code&gt; for the registry — and that&apos;s what forces
&lt;code&gt;inline&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;noinline rules&lt;/code&gt;&lt;/strong&gt; is the counterweight: the lambda is &lt;em&gt;stored&lt;/em&gt; in the &lt;code&gt;Validator&lt;/code&gt;, so it must exist as a real
object in bytecode, and inlined lambdas don&apos;t — their bodies are copied into the call site with nothing left to store.&lt;/li&gt;
&lt;li&gt;The rules type &lt;code&gt;context(ValidationScope&amp;lt;T&amp;gt;) T.() -&amp;gt; Unit&lt;/code&gt; is the same context-plus-receiver shape from Step 4, now at
the top level — &lt;code&gt;this&lt;/code&gt; is the value, the scope is in context.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Three smaller choices in the signature are worth a line each:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;KClass&amp;lt;T &amp;amp; Any&amp;gt;&lt;/code&gt;&lt;/strong&gt;, not &lt;code&gt;KClass&amp;lt;T&amp;gt;&lt;/code&gt;. The class leaves &lt;code&gt;T&lt;/code&gt; unbounded so it can wrap a nullable target, but
&lt;code&gt;KClass&lt;/code&gt;&apos;s own parameter is &lt;code&gt;Any&lt;/code&gt;-bounded — there&apos;s no &lt;code&gt;KClass&lt;/code&gt; of a nullable type. &lt;code&gt;T::class&lt;/code&gt; already produces a
&lt;code&gt;KClass&amp;lt;T &amp;amp; Any&amp;gt;&lt;/code&gt;, so the definitely-non-null projection is the only thing that fits the field.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;operator fun invoke&lt;/code&gt;&lt;/strong&gt; on the companion, instead of a plain constructor. Building a &lt;code&gt;Validator&lt;/code&gt; needs &lt;code&gt;T::class&lt;/code&gt;,
which needs &lt;code&gt;reified&lt;/code&gt;, which needs &lt;code&gt;inline&lt;/code&gt; — and a constructor can be none of those. So the real entry point has to
be an inline reified factory function. Naming it &lt;code&gt;invoke&lt;/code&gt; on the companion keeps the constructor-like call site:
&lt;code&gt;Validator&amp;lt;User&amp;gt; { … }&lt;/code&gt; resolves to &lt;code&gt;Validator.Companion.invoke&amp;lt;User&amp;gt;(…)&lt;/code&gt;, so nothing at the call site has to change.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;open&lt;/code&gt;&lt;/strong&gt; class with an &lt;strong&gt;&lt;code&gt;open fun validate&lt;/code&gt;&lt;/strong&gt;. &lt;code&gt;@Validatable(with = …)&lt;/code&gt; (Step 12) lets a caller register a custom
&lt;code&gt;Validator&lt;/code&gt; subclass in place of the default, and that&apos;s only possible if the class can be extended and &lt;code&gt;validate&lt;/code&gt;
overridden — a &lt;code&gt;final&lt;/code&gt; class would slam the door.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Step 10: Validating an Any — the isInstanceOf contract&lt;/h2&gt;
&lt;p&gt;The point of the registry is to validate a value you only know as &lt;code&gt;Any?&lt;/code&gt;.
After a runtime type check I want to use it as &lt;code&gt;T&lt;/code&gt; without an unchecked cast — and a contract buys that:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@OptIn(ExperimentalContracts::class)
private fun &amp;lt;T : Any&amp;gt; Any.isInstanceOf(kClass: KClass&amp;lt;T&amp;gt;): Boolean {
    contract { returns(true) implies (this@isInstanceOf is T) }
    return kClass.isInstance(this)
}

open fun validate(value: Any?): ValidationResult = when {
    value == null -&amp;gt; ValidationResult.Invalid(listOf(ValidationError(&quot;&quot;, &quot;must not be null&quot;)))
    !value.isInstanceOf(kClass) -&amp;gt;
        ValidationResult.Invalid(listOf(ValidationError(&quot;&quot;, &quot;expected ${kClass.simpleName}&quot;)))
    else -&amp;gt; {
        val scope = RootScope(value)   // value smart-cast to T here
        applyRules(scope)
        if (scope.errors.isEmpty()) ValidationResult.Valid else ValidationResult.Invalid(scope.errors)
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;returns(true) implies (this is T)&lt;/code&gt; reads alarming — it&apos;s an &lt;em&gt;unverified&lt;/em&gt; assertion, meaning the compiler can&apos;t prove it
and takes the claim on faith, then smart-casts &lt;code&gt;value&lt;/code&gt; to &lt;code&gt;T&lt;/code&gt; with no &lt;code&gt;as T&lt;/code&gt; and no warning. What makes it safe rather
than reckless is the function body: &lt;code&gt;KClass.isInstance&lt;/code&gt; performs the real runtime type check, so by the time the
assertion is &quot;trusted&quot; it has already been verified at runtime. The contract just teaches the compiler what that
&lt;code&gt;Boolean&lt;/code&gt; result &lt;em&gt;means&lt;/em&gt;.
&lt;code&gt;ValidationResult&lt;/code&gt; is just &lt;code&gt;Valid&lt;/code&gt; or &lt;code&gt;Invalid(errors)&lt;/code&gt; — a sealed result type that replaces the raw &lt;code&gt;List&lt;/code&gt; once there&apos;s
a type mismatch to report.&lt;/p&gt;
&lt;h2&gt;Step 11: Structured, translatable messages — fun interface&lt;/h2&gt;
&lt;p&gt;The messages have been bare strings this whole time.
Errors shouldn&apos;t hard-code English, so I replace the string with a &lt;code&gt;Message&lt;/code&gt; carrying a stable &lt;code&gt;key&lt;/code&gt;, pre-stringified
&lt;code&gt;args&lt;/code&gt;, and a default &lt;code&gt;text&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;data class Message(val key: String, val args: List&amp;lt;String&amp;gt; = emptyList(), val text: String = key) {
    fun render(translator: Translator? = null): String = translator?.translate(key, args) ?: text

    companion object {
        val NotBlank = Message(&quot;validation.notBlank&quot;, text = &quot;must not be blank&quot;)
        fun LengthIn(range: IntRange) =
            Message(&quot;validation.lengthIn&quot;, listOf(range.toString()), &quot;must be $range characters&quot;)
        // …
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The translator is a single-method interface, so I declare it &lt;code&gt;fun interface&lt;/code&gt; and any caller can hand it a lambda:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;fun interface Translator {
    fun translate(key: String, args: List&amp;lt;String&amp;gt;): String?
}

val pl = Translator { key, args -&amp;gt; catalogue[key]?.format(args) }
error.message.render(pl)   // a lambda where an interface is expected
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;render&lt;/code&gt; falls back to &lt;code&gt;text&lt;/code&gt; when no translator resolves the key — useful out of the box, localizable when you need it.
The &lt;code&gt;ValidationError&lt;/code&gt; from Step 5 grows into a sealed type whose cases carry a &lt;code&gt;Message&lt;/code&gt; and a path:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sealed interface ValidationError {
    val message: Message

    data class Field(val path: String, override val message: Message) : ValidationError
    data class Element(val path: String, val index: Int, override val message: Message) : ValidationError
    data class Root(override val message: Message) : ValidationError
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is the one place the Step 5 split shows a seam.
With a single &lt;code&gt;ValidationError(path, message)&lt;/code&gt;, &lt;code&gt;raise&lt;/code&gt; could live once on the base.
Now the &lt;em&gt;case&lt;/em&gt; depends on the scope — &lt;code&gt;Root&lt;/code&gt; for root and ephemeral scopes, &lt;code&gt;Field&lt;/code&gt; for a field or map entry,
&lt;code&gt;Element&lt;/code&gt; (with its index) for a list item — so &lt;code&gt;raise(message: Message)&lt;/code&gt; goes back to &lt;code&gt;abstract&lt;/code&gt; on the base, and each
leaf builds its own:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;abstract fun raise(message: Message)   // on ValidationScope, replacing the String version

// RootScope, EphemeralScope
override fun raise(message: Message) = raise(ValidationError.Root(message))

// FieldScope, EntryScope
override fun raise(message: Message) = raise(ValidationError.Field(path, message))

// ItemScope
override fun raise(message: Message) = raise(ValidationError.Element(parent.path, index, message))
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Every &lt;code&gt;check&lt;/code&gt; now returns a &lt;code&gt;Message&lt;/code&gt; instead of a &lt;code&gt;String&lt;/code&gt;; the rest of the structure is untouched.&lt;/p&gt;
&lt;h2&gt;Step 12: @Validatable + KSP — generating validate()&lt;/h2&gt;
&lt;p&gt;One pain is left: calling &lt;code&gt;someValidator.validate(req)&lt;/code&gt; by hand and wiring up a registry.
The shape I&apos;m after is the one from the very top of this post — tag a class &lt;code&gt;@Validatable&lt;/code&gt;, point it at its validator,
and get a &lt;code&gt;validate()&lt;/code&gt; extension for free:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Validatable
data class Address(val city: String, val zip: String) {
    companion object {
        val validator = Validator&amp;lt;Address&amp;gt; {
            field(::city) { notBlank() }
            field(::zip) { lengthIn(5..5) }
        }
    }
}

@Validatable
data class User(val name: String, val age: Int, val address: Address) {
    companion object {
        val validator = Validator&amp;lt;User&amp;gt; {
            field(::name) { notBlank(); lengthIn(1..50) }
            field(::age) { inRange(0..150) }
            validated(::address)   // Address&apos;s validator, looked up by type
        }
    }
}

User(&quot;alice&quot;, 30, Address(&quot;Kraków&quot;, &quot;30001&quot;)).validate()   // ValidationResult — nothing hand-wired
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The annotation itself is tiny.
It marks the class and, optionally, names an external validator object instead of the companion &lt;code&gt;validator&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Target(AnnotationTarget.CLASS)
annotation class Validatable(val with: KClass&amp;lt;out Validator&amp;lt;*&amp;gt;&amp;gt; = Validator::class)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The shape is deliberately the same as &lt;code&gt;@Serializable&lt;/code&gt; from &lt;code&gt;kotlinx.serialization&lt;/code&gt;: tag a class, and a compile-time
processor emits the boilerplate you&apos;d otherwise hand-write — there &lt;code&gt;encodeToString&lt;/code&gt;/&lt;code&gt;decodeFromString&lt;/code&gt; wiring, here a
&lt;code&gt;validate()&lt;/code&gt; extension. Same ergonomics, same &quot;no reflection, no runtime cost&quot; promise; the only difference is that
&lt;code&gt;kotlinx.serialization&lt;/code&gt; ships a full compiler plugin while this is a few hundred lines of KSP.&lt;/p&gt;
&lt;p&gt;Turning that &lt;code&gt;@Validatable&lt;/code&gt; into a real &lt;code&gt;validate()&lt;/code&gt; is a compile-time job,
and &lt;a href=&quot;https://kotlinlang.org/docs/ksp-overview.html&quot;&gt;KSP&lt;/a&gt; — Kotlin Symbol Processing — is the tool for it.
It&apos;s the lightweight successor to &lt;code&gt;kapt&lt;/code&gt;: instead of generating Java stubs and running a &lt;code&gt;javac&lt;/code&gt; annotation processor,
KSP hands you a resolved view of the program&apos;s &lt;em&gt;Kotlin&lt;/em&gt; symbols (classes, functions, properties, annotations) and lets
you emit new source files, which the compiler then picks up in the same build.
No reflection, no runtime cost, no stub round-trip — the generated &lt;code&gt;validate()&lt;/code&gt; is as if you&apos;d typed it.&lt;/p&gt;
&lt;p&gt;The processor isn&apos;t annotated or imported anywhere — KSP discovers it through a &lt;code&gt;META-INF/services&lt;/code&gt; entry, a plain
service-loader file naming the provider class:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# src/main/resources/META-INF/services/com.google.devtools.ksp.processing.SymbolProcessorProvider
sure.ksp.ValidationExtensionProcessorProvider
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The provider&apos;s only job is to hand KSP a &lt;code&gt;SymbolProcessor&lt;/code&gt;, wired with the two services every processor leans on: a
&lt;code&gt;CodeGenerator&lt;/code&gt; to emit files and a &lt;code&gt;KSPLogger&lt;/code&gt; to report problems back through the compiler.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class ValidationExtensionProcessorProvider : SymbolProcessorProvider {
    override fun create(environment: SymbolProcessorEnvironment): SymbolProcessor =
        ValidationExtensionProcessor(environment.codeGenerator, environment.logger)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;One detail sets the tone for the whole processor: it works on &lt;em&gt;strings&lt;/em&gt; KSP resolves for it, never on the &lt;code&gt;sure&lt;/code&gt; types
directly — the KSP module doesn&apos;t even depend on the runtime one. So the fully-qualified names it cares about are just
constants:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private const val ANNOTATION_FQN = &quot;sure.Validatable&quot;
private const val VALIDATOR_FQN = &quot;sure.Validator&quot;
private const val VALIDATION_SCOPE_FQN = &quot;sure.ValidationScope&quot;
private const val VALIDATION_RESULT_FQN = &quot;sure.ValidationResult&quot;
private const val GENERATED_PACKAGE = &quot;sure&quot;
private const val GENERATED_FILE = &quot;GeneratedValidationExtensions&quot;
private const val VALIDATOR_FIELD = &quot;validator&quot;
private const val WITH_ARG = &quot;with&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;process&lt;/code&gt; is the workhorse. KSP runs in &lt;em&gt;rounds&lt;/em&gt; — it calls &lt;code&gt;process&lt;/code&gt; again whenever a round generated new symbols that
themselves need processing — so a one-shot generator latches a &lt;code&gt;generated&lt;/code&gt; flag and bails on re-entry. It asks the
&lt;code&gt;Resolver&lt;/code&gt; for every class carrying &lt;code&gt;@Validatable&lt;/code&gt;, turns each into a small &lt;code&gt;ValidatorRef&lt;/code&gt;, and writes one file:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private class ValidationExtensionProcessor(
    private val codeGenerator: CodeGenerator,
    private val logger: KSPLogger,
) : SymbolProcessor {
    private var generated = false

    override fun process(resolver: Resolver): List&amp;lt;KSAnnotated&amp;gt; {
        if (generated) return emptyList()

        val classes = resolver.getSymbolsWithAnnotation(ANNOTATION_FQN, false)
            .filterIsInstance&amp;lt;KSClassDeclaration&amp;gt;()
            .toList()
        if (classes.isEmpty()) return emptyList()

        val refs = classes.mapNotNull { it.toValidatorRef() }
        if (refs.isEmpty()) return emptyList()

        val originatingFiles = classes.mapNotNull { it.containingFile }.distinct()
        codeGenerator.createNewFile(
            Dependencies(false, *originatingFiles.toTypedArray()),
            GENERATED_PACKAGE,
            GENERATED_FILE,
        ).use { stream -&amp;gt; OutputStreamWriter(stream).use { it.write(render(refs)) } }

        generated = true
        return emptyList()
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Three things in there carry weight. &lt;code&gt;getSymbolsWithAnnotation(ANNOTATION_FQN, false)&lt;/code&gt; returns a flat sequence of
annotated symbols — the &lt;code&gt;false&lt;/code&gt; (&lt;code&gt;inDepth&lt;/code&gt;) keeps it shallow, since &lt;code&gt;@Validatable&lt;/code&gt; only ever lands on a top-level class,
never nested inside another annotated one. The &lt;code&gt;List&amp;lt;KSAnnotated&amp;gt;&lt;/code&gt; that &lt;code&gt;process&lt;/code&gt; &lt;em&gt;returns&lt;/em&gt; is KSP&apos;s deferral channel:
symbols that couldn&apos;t be resolved this round and should be retried next one; a finished one-shot returns nothing.
And &lt;code&gt;Dependencies(aggregating = false, *originatingFiles)&lt;/code&gt; is the incremental-build wiring — it ties the generated file
to the exact &lt;code&gt;@Validatable&lt;/code&gt; sources it was built from, so editing one of them regenerates only what&apos;s affected instead
of the whole module.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;toValidatorRef&lt;/code&gt; is where the annotation is read. A &lt;code&gt;@Validatable&lt;/code&gt; type names its validator one of two ways, and the
processor tries them in order — an explicit &lt;code&gt;with =&lt;/code&gt; first, then the companion &lt;code&gt;validator&lt;/code&gt; — walking the KSP symbol tree
rather than reflecting. These are all extension functions on &lt;code&gt;KSClassDeclaration&lt;/code&gt;, nested in the processor:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    private fun KSClassDeclaration.toValidatorRef(): ValidatorRef? {
    val receiverFqn = qualifiedName?.asString() ?: run {
        logger.warn(&quot;@Validatable type ${simpleName.asString()} has no qualified name&quot;, this)
        return null
    }
    val validatorExpression = customValidatorFqn() ?: companionValidatorExpression(receiverFqn) ?: return null
    return ValidatorRef(receiverFqn, validatorExpression)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;with =&lt;/code&gt; path has a wrinkle. KSP doesn&apos;t hand you a &lt;code&gt;KClass&lt;/code&gt; — it hands you a &lt;code&gt;KSType&lt;/code&gt;, a &lt;em&gt;symbol&lt;/em&gt; in its model of
the program, so you resolve the annotation, find the &lt;code&gt;with&lt;/code&gt; argument, and read its declaration&apos;s qualified name. The
catch is the default: &lt;code&gt;@Validatable&lt;/code&gt; declares &lt;code&gt;with = Validator::class&lt;/code&gt; as its &quot;unset&quot; sentinel, so a value equal to
&lt;code&gt;sure.Validator&lt;/code&gt; means &lt;em&gt;no custom validator was given&lt;/em&gt; and the code returns &lt;code&gt;null&lt;/code&gt; to fall through to the companion:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    private fun KSClassDeclaration.customValidatorFqn(): String? {
    val annotation = annotations.firstOrNull {
        it.annotationType.resolve().declaration.qualifiedName?.asString() == ANNOTATION_FQN
    } ?: return null

    val withType = annotation.arguments
        .firstOrNull { it.name?.asString() == WITH_ARG }
        ?.value as? KSType ?: return null

    val withFqn = withType.declaration.qualifiedName?.asString()
    return withFqn?.takeUnless { it == VALIDATOR_FQN }   // default sentinel → not a custom validator
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The companion path scans the class&apos;s nested declarations for the companion object, then for a property named
&lt;code&gt;validator&lt;/code&gt;. A missing one isn&apos;t a reason to emit code that won&apos;t compile — it&apos;s a user mistake, so the processor
&lt;code&gt;logger.error&lt;/code&gt;s against the offending class and fails the build with a message that says exactly how to fix it:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    private fun KSClassDeclaration.companionValidatorExpression(receiverFqn: String): String? {
    val companion = declarations.filterIsInstance&amp;lt;KSClassDeclaration&amp;gt;().firstOrNull { it.isCompanionObject }
    val hasValidator = companion?.getAllProperties()?.any { it.simpleName.asString() == VALIDATOR_FIELD } == true
    return if (!hasValidator) {
        logger.error(
            &quot;@Validatable class ${simpleName.asString()} must declare a `$VALIDATOR_FIELD` property in its &quot; +
                    &quot;companion object, or specify @Validatable($WITH_ARG = SomeObject::class)&quot;,
            this,
        )
        null
    } else {
        &quot;$receiverFqn.$VALIDATOR_FIELD&quot;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;logger.warn&lt;/code&gt; vs &lt;code&gt;logger.error&lt;/code&gt; split is deliberate: a class with no qualified name (anonymous or local) is just
skipped with a warning, but a &lt;code&gt;@Validatable&lt;/code&gt; whose validator can&apos;t be resolved is a hard error that stops the build.&lt;/p&gt;
&lt;p&gt;There&apos;s no clever code generator behind &lt;code&gt;render&lt;/code&gt;. It&apos;s a &lt;code&gt;buildString&lt;/code&gt; that prints Kotlin source as text from the
&lt;code&gt;(receiver, validator)&lt;/code&gt; pairs — no templating engine, no AST builder, just &lt;code&gt;appendLine&lt;/code&gt;. For each &lt;code&gt;@Validatable&lt;/code&gt; type it
emits a &lt;code&gt;validate()&lt;/code&gt; extension, then one shared &lt;code&gt;validatorsByClass&lt;/code&gt; map, a &lt;code&gt;reified validatorFor&amp;lt;T&amp;gt;()&lt;/code&gt; lookup over it,
and a &lt;code&gt;validated(::field)&lt;/code&gt; overload that uses that lookup to resolve a nested validator by type:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    private fun render(refs: List&amp;lt;ValidatorRef&amp;gt;): String = buildString {
    appendLine(&quot;package $GENERATED_PACKAGE&quot;)
    appendLine()
    for (ref in refs) {
        appendLine(
            &quot;fun ${ref.receiverFqn}.validate(): $VALIDATION_RESULT_FQN = &quot; +
                    &quot;${ref.validatorExpression}.validate(this)&quot;,
        )
        appendLine()
    }
    appendLine(&quot;@PublishedApi&quot;)
    appendLine(&quot;internal val validatorsByClass: Map&amp;lt;kotlin.reflect.KClass&amp;lt;*&amp;gt;, $VALIDATOR_FQN&amp;lt;*&amp;gt;&amp;gt; = mapOf(&quot;)
    for (ref in refs) {
        appendLine(&quot;    ${ref.receiverFqn}::class to ${ref.validatorExpression},&quot;)
    }
    appendLine(&quot;)&quot;)
    appendLine()
    appendLine(&quot;@Suppress(\&quot;UNCHECKED_CAST\&quot;)&quot;)
    appendLine(&quot;inline fun &amp;lt;reified T : Any&amp;gt; validatorFor(): $VALIDATOR_FQN&amp;lt;T&amp;gt; =&quot;)
    appendLine(&quot;    validatorsByClass[T::class] as? $VALIDATOR_FQN&amp;lt;T&amp;gt;&quot;)
    appendLine(&quot;        ?: error(\&quot;No validator registered for \${T::class.qualifiedName}\&quot;)&quot;)
    appendLine()
    // a validated(::field) overload that finds the sub-validator in the registry by type
    appendLine(&quot;context(_: $VALIDATION_SCOPE_FQN&amp;lt;*&amp;gt;)&quot;)
    appendLine(&quot;inline fun &amp;lt;reified F : Any&amp;gt; validated(property: kotlin.reflect.KProperty0&amp;lt;F&amp;gt;) =&quot;)
    appendLine(&quot;    validated(property, validatorFor&amp;lt;F&amp;gt;())&quot;)
}
}

private data class ValidatorRef(val receiverFqn: String, val validatorExpression: String)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Everything is a fully-qualified name because generated source has no imports to lean on — &lt;code&gt;$VALIDATION_RESULT_FQN&lt;/code&gt;,
&lt;code&gt;kotlin.reflect.KClass&lt;/code&gt;, and the collected &lt;code&gt;receiverFqn&lt;/code&gt;s all print in full so the file compiles wherever it lands.
For the two &lt;code&gt;@Validatable&lt;/code&gt; types above, that produces a single &lt;code&gt;sure/GeneratedValidationExtensions.kt&lt;/code&gt; the compiler
picks up in the same build — one &lt;code&gt;validate()&lt;/code&gt; per type, all of them in the shared registry:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package sure

fun com.example.Address.validate(): sure.ValidationResult =
    com.example.Address.validator.validate(this)

fun com.example.User.validate(): sure.ValidationResult =
    com.example.User.validator.validate(this)

@PublishedApi
internal val validatorsByClass: Map&amp;lt;kotlin.reflect.KClass&amp;lt;*&amp;gt;, sure.Validator&amp;lt;*&amp;gt;&amp;gt; = mapOf(
    com.example.Address::class to com.example.Address.validator,
    com.example.User::class to com.example.User.validator,
)

@Suppress(&quot;UNCHECKED_CAST&quot;)
inline fun &amp;lt;reified T : Any&amp;gt; validatorFor(): sure.Validator&amp;lt;T&amp;gt; =
    validatorsByClass[T::class] as? sure.Validator&amp;lt;T&amp;gt;
        ?: error(&quot;No validator registered for ${T::class.qualifiedName}&quot;)

context(_: sure.ValidationScope&amp;lt;*&amp;gt;)
inline fun &amp;lt;reified F : Any&amp;gt; validated(property: kotlin.reflect.KProperty0&amp;lt;F&amp;gt;) =
    validated(property, validatorFor&amp;lt;F&amp;gt;())
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;validate()&lt;/code&gt; extension is what the call site at the very top of this post resolves to; &lt;code&gt;validatorFor&amp;lt;T&amp;gt;()&lt;/code&gt; backs the
type-keyed registry that &lt;code&gt;Validator&lt;/code&gt; was made &lt;code&gt;reified&lt;/code&gt; for back in Step 9. The generated &lt;code&gt;validated(::address)&lt;/code&gt; overload
closes a small loop with it — because every &lt;code&gt;@Validatable&lt;/code&gt; type is in the registry, a nested field can pull its
sub-validator by type with no explicit reference, exactly the call the two snippets above use.&lt;/p&gt;
&lt;p&gt;That convenience comes with a caveat worth stating plainly: &lt;code&gt;validatorFor&amp;lt;F&amp;gt;()&lt;/code&gt; is a &lt;em&gt;runtime&lt;/em&gt; map lookup, not a
compile-time guarantee. &lt;code&gt;validated(::address)&lt;/code&gt; type-checks whether or not an &lt;code&gt;Address&lt;/code&gt; validator was ever registered;
if the field&apos;s type isn&apos;t &lt;code&gt;@Validatable&lt;/code&gt; (or its module wasn&apos;t on the path when the registry was generated), the
lookup misses and &lt;code&gt;error(...)&lt;/code&gt; throws at validation time. The explicit &lt;code&gt;validated(::address, Address.validator)&lt;/code&gt; form
keeps that honest — passing the validator directly is checked by the compiler. So this is the usual derivation
trade-off: the zero-argument overload is convenient, the explicit one is statically safe.&lt;/p&gt;
&lt;p&gt;This is the Kotlin equivalent of Scala&apos;s automatic type-class derivation — except instead of inductive &lt;code&gt;given&lt;/code&gt;s resolved
by the compiler, it&apos;s a code generator emitting plain source. The same trade-off shows up there too: Scala&apos;s implicit
resolution fails the &lt;em&gt;compile&lt;/em&gt;, whereas this registry lookup can only fail at runtime.&lt;/p&gt;
&lt;p&gt;That closes the loop: the public API from the very top of this post is now fully assembled.&lt;/p&gt;
&lt;h2&gt;Through the Decompiler&lt;/h2&gt;
&lt;p&gt;The library is done — the section below is a bonus for the curious. Most of the &quot;magic&quot; above exists only in source;
decompiling the JVM target shows it flattening into ordinary bytecode patterns. If you don&apos;t care what it lowers to,
jump to &lt;a href=&quot;#case-closed&quot;&gt;Case Closed&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Context parameters&lt;/strong&gt; are the clearest case.
A check like &lt;code&gt;notBlank()&lt;/code&gt; has no receiver in Kotlin, but its &lt;code&gt;context(_: ValidationScope&amp;lt;String&amp;gt;)&lt;/code&gt; lowers to a plain
leading parameter:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public static final void notBlank(ValidationScope $context) {
    check($context, NotBlankPredicate.INSTANCE, NotBlankMessage.INSTANCE);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;field(::name)&lt;/code&gt;&lt;/strong&gt; inlines into the caller.
The property reference becomes a fresh synthetic class with a &lt;code&gt;get()&lt;/code&gt;; there is no reflective dispatch, just a
specialized getter call:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;KProperty0 property$iv = (KProperty0) new PropertyReference0Impl($receiver) {
    public Object get() {
        return ((User) this.receiver).getName();
    }
};
Object value$iv = property$iv.get();
FieldScope fs = new FieldScope(value$iv, property$iv.getName(), scope$iv);
// … inlined block body …
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;reified T&lt;/code&gt;&lt;/strong&gt; in &lt;code&gt;Validator&amp;lt;User&amp;gt; { … }&lt;/code&gt; lowers to a literal &lt;code&gt;User.class&lt;/code&gt; at the call site —
&lt;code&gt;Reflection.getOrCreateKotlinClass(User.class)&lt;/code&gt; — captured into the &lt;code&gt;Validator&lt;/code&gt; constructor.
The &lt;code&gt;noinline rules&lt;/code&gt; lambda forced a real &lt;code&gt;Function&lt;/code&gt; object, so it shows up as a synthetic class rather than copied-in
code; that&apos;s the whole point of &lt;code&gt;noinline&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Contracts&lt;/strong&gt; leave &lt;em&gt;no trace at all&lt;/em&gt; in bytecode.
&lt;code&gt;returns(true) implies (this is T)&lt;/code&gt; and &lt;code&gt;returnsNotNull() implies …&lt;/code&gt; are compile-time-only — they change what the Kotlin
compiler will let you write, then evaporate.
The decompiled &lt;code&gt;isInstanceOf&lt;/code&gt; is a one-liner returning &lt;code&gt;kClass.isInstance(this)&lt;/code&gt;; the smart cast it enabled became an
ordinary, checked-at-source assignment with no runtime cast inserted.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Explicit backing fields&lt;/strong&gt; collapse to exactly what you&apos;d hand-write.
The &lt;code&gt;val errors: List&lt;/code&gt; / &lt;code&gt;field = mutableListOf()&lt;/code&gt; pair becomes one private field and a read-only getter — no second
property, and crucially no setter:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private final List errors = new ArrayList();        // the single backing field
public final List getErrors() { return this.errors; }  // read-only — no setErrors emitted
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Inside the class, where &lt;code&gt;errors&lt;/code&gt; means the &lt;code&gt;MutableList&lt;/code&gt;, &lt;code&gt;errors += message&lt;/code&gt; is just a method call on that same field:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;((Collection) this.errors).add(message);   // from `errors += message`
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;So the encapsulation is real, not a wrapper: callers see &lt;code&gt;List&lt;/code&gt;, the class mutates the one underlying instance, and
nothing is allocated to bridge the two.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;fun interface&lt;/code&gt;&lt;/strong&gt; doesn&apos;t allocate a class per lambda.
&lt;code&gt;Translator { key, args -&amp;gt; … }&lt;/code&gt; lowers to an &lt;code&gt;invokedynamic&lt;/code&gt; call site backed by &lt;code&gt;LambdaMetafactory&lt;/code&gt; — the same
machinery as a plain Kotlin/Java lambda, the runtime spins up the implementation on first use:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// the `Translator { … }` becomes:
0: invokedynamic #40,  0   // InvokeDynamic #0:translate:()Lsure/Translator;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Definitely-non-null types&lt;/strong&gt; mostly vanish.
&lt;code&gt;F &amp;amp; Any&lt;/code&gt; erases to its ordinary bound — there&apos;s no special JVM type — so the guarantee is carried by &lt;code&gt;@Metadata&lt;/code&gt; plus
the occasional &lt;code&gt;Intrinsics.checkNotNullParameter&lt;/code&gt; guard the compiler drops in at a public boundary. At runtime it&apos;s a
plain non-null reference like any other.&lt;/p&gt;
&lt;h2&gt;Case Closed&lt;/h2&gt;
&lt;p&gt;The library is, structurally, a type class: type-indexed dispatch (&lt;code&gt;validatorFor&amp;lt;T&amp;gt;()&lt;/code&gt;), behavior parameterized by type,
and — via KSP — &lt;em&gt;derivation&lt;/em&gt;. Where the Scala version of this story leans on &lt;code&gt;Mirror&lt;/code&gt;s and inductive &lt;code&gt;given&lt;/code&gt;s, Kotlin
gets there with context parameters and a KSP code generator for the derivation.&lt;/p&gt;
&lt;p&gt;As it turns out, Kotlin can be scary too.&lt;/p&gt;
&lt;p&gt;The full source — multiplatform targets, every built-in check, the KSP processor, tests — is
at &lt;a href=&quot;https://github.com/halotukozak/sure&quot;&gt;github.com/halotukozak/sure&lt;/a&gt;.&lt;/p&gt;
</content:encoded><author>Bartłomiej Kozak</author></item></channel></rss>