Metaprogramming in Scala 3: Macros

Scala 3 introduces a completely redesigned macro system that provides powerful metaprogramming capabilities while maintaining type safety and ease of use. This lesson explores the new macro API, showing you how to generate code at compile time, create domain-specific languages, and perform compile-time computations.

Introduction to Scala 3 Macros

Macros in Scala 3 allow you to generate and transform code at compile time. Unlike runtime reflection, macros provide type-safe code generation with full compiler support and IDE integration.

Key Concepts

// Core macro concepts in Scala 3:
// 1. Quotes ('{ ... }) - wrap expressions to create AST nodes
// 2. Splices (${ ... }) - insert expressions into quotes
// 3. Expr[T] - typed expression representing code of type T
// 4. Quotes - context providing compiler information
// 5. Type representations - work with types at compile time

Basic Macro Syntax

Simple Expression Macros

import scala.quoted.*

// Simple macro that prints debug information
inline def debug[T](inline value: T): T = ${ debugImpl('value) }

def debugImpl[T: Type](value: Expr[T])(using Quotes): Expr[T] = {
  import quotes.reflect.*

  // Extract the source code representation
  val valueRepr = value.asTerm.show
  val typeRepr = TypeRepr.of[T].show

  // Generate code that prints debug info and returns the value
  '{
    println(s"Debug: ${${Expr(valueRepr)}} : ${${Expr(typeRepr)}} = ${$value}")
    $value
  }
}

// Usage
val result = debug(42 + 8)
// Output: Debug: scala.Int.+(42)(8) : scala.Int = 50

val name = debug("Scala")
// Output: Debug: "Scala" : java.lang.String = Scala

Compile-Time Assertions

import scala.quoted.*

// Macro for compile-time assertions
inline def staticAssert(inline condition: Boolean, inline message: String): Unit = 
  ${ staticAssertImpl('condition, 'message) }

def staticAssertImpl(condition: Expr[Boolean], message: Expr[String])(using Quotes): Expr[Unit] = {
  import quotes.reflect.*

  condition.value match {
    case Some(true) => 
      '{ () } // Assertion passes, generate empty code
    case Some(false) =>
      val msg = message.valueOrAbort
      report.errorAndAbort(s"Static assertion failed: $msg")
    case None =>
      report.errorAndAbort("Condition must be a compile-time constant")
  }
}

// Usage
staticAssert(1 + 1 == 2, "Math is working")  // Compiles fine
// staticAssert(1 + 1 == 3, "Math is broken")   // Compilation error

Compile-Time String Processing

import scala.quoted.*

// Macro for compile-time string validation and processing
inline def validateEmail(inline email: String): String = 
  ${ validateEmailImpl('email) }

def validateEmailImpl(email: Expr[String])(using Quotes): Expr[String] = {
  email.value match {
    case Some(emailStr) =>
      if (emailStr.contains("@") && emailStr.contains(".")) {
        email
      } else {
        quotes.reflect.report.errorAndAbort(s"Invalid email format: $emailStr")
      }
    case None =>
      quotes.reflect.report.errorAndAbort("Email must be a string literal")
  }
}

// Usage
val validEmail = validateEmail("user@example.com")  // OK
// val invalidEmail = validateEmail("invalid-email")    // Compilation error

// Compile-time string interpolation with validation
inline def sql(inline query: String): PreparedStatement = 
  ${ sqlImpl('query) }

def sqlImpl(query: Expr[String])(using Quotes): Expr[PreparedStatement] = {
  query.value match {
    case Some(queryStr) =>
      // Validate SQL syntax at compile time
      if (queryStr.toLowerCase.startsWith("select") || 
          queryStr.toLowerCase.startsWith("insert") ||
          queryStr.toLowerCase.startsWith("update") ||
          queryStr.toLowerCase.startsWith("delete")) {
        '{ PreparedStatement(${query}) }
      } else {
        quotes.reflect.report.errorAndAbort(s"Invalid SQL query: $queryStr")
      }
    case None =>
      quotes.reflect.report.errorAndAbort("SQL query must be a string literal")
  }
}

case class PreparedStatement(query: String)

// Usage
val selectQuery = sql("SELECT * FROM users WHERE id = ?")
// val invalidQuery = sql("DROP TABLE users")  // Compilation error

Advanced Macro Techniques

AST Manipulation and Code Generation

import scala.quoted.*

// Macro that generates getter methods for case class fields
inline def generateGetters[T]: String = ${ generateGettersImpl[T] }

def generateGettersImpl[T: Type](using Quotes): Expr[String] = {
  import quotes.reflect.*

  val tpe = TypeRepr.of[T]
  val typeSymbol = tpe.typeSymbol

  if (!typeSymbol.flags.is(Flags.Case)) {
    report.errorAndAbort(s"${typeSymbol.name} is not a case class")
  }

  val fields = typeSymbol.caseFields
  val getterMethods = fields.map { field =>
    val fieldName = field.name
    val fieldType = tpe.memberType(field).show
    s"def get${fieldName.capitalize}: $fieldType = this.$fieldName"
  }

  val result = getterMethods.mkString("\n")
  Expr(result)
}

// Usage with case class
case class Person(name: String, age: Int, email: String)

val getters = generateGetters[Person]
println(getters)
// Output:
// def getName: String = this.name
// def getAge: Int = this.age
// def getEmail: String = this.email

// Macro that creates a builder pattern for case classes
inline def generateBuilder[T]: String = ${ generateBuilderImpl[T] }

def generateBuilderImpl[T: Type](using Quotes): Expr[String] = {
  import quotes.reflect.*

  val tpe = TypeRepr.of[T]
  val typeSymbol = tpe.typeSymbol
  val className = typeSymbol.name

  if (!typeSymbol.flags.is(Flags.Case)) {
    report.errorAndAbort(s"$className is not a case class")
  }

  val fields = typeSymbol.caseFields
  val builderClass = s"${className}Builder"

  val fieldDeclarations = fields.map { field =>
    val fieldName = field.name
    val fieldType = tpe.memberType(field).show
    s"private var _$fieldName: Option[$fieldType] = None"
  }.mkString("\n  ")

  val setterMethods = fields.map { field =>
    val fieldName = field.name
    val fieldType = tpe.memberType(field).show
    s"""def with${fieldName.capitalize}(value: $fieldType): $builderClass = {
       |    _$fieldName = Some(value)
       |    this
       |  }""".stripMargin
  }.mkString("\n\n  ")

  val buildMethod = {
    val requiredFields = fields.map { field =>
      val fieldName = field.name
      s"_$fieldName.getOrElse(throw new IllegalStateException(\"$fieldName is required\"))"
    }.mkString(", ")

    s"""def build(): $className = {
       |    $className($requiredFields)
       |  }""".stripMargin
  }

  val builderCode = s"""class $builderClass {
     |  $fieldDeclarations
     |
     |  $setterMethods
     |
     |  $buildMethod
     |}
     |
     |object $builderClass {
     |  def apply(): $builderClass = new $builderClass()
     |}""".stripMargin

  Expr(builderCode)
}

// Usage
case class User(id: Long, name: String, email: String, age: Int)
val builderCode = generateBuilder[User]
println(builderCode)

Type-Level Programming with Macros

import scala.quoted.*

// Macro for compile-time type checking and transformation
trait TypeInfo[T] {
  def typeName: String
  def isNumeric: Boolean
  def isPrimitive: Boolean
}

object TypeInfo {
  inline given derived[T]: TypeInfo[T] = ${ deriveTypeInfo[T] }
}

def deriveTypeInfo[T: Type](using Quotes): Expr[TypeInfo[T]] = {
  import quotes.reflect.*

  val tpe = TypeRepr.of[T]
  val typeName = tpe.show

  val isNumeric = tpe <:< TypeRepr.of[Byte] || 
                  tpe <:< TypeRepr.of[Short] ||
                  tpe <:< TypeRepr.of[Int] ||
                  tpe <:< TypeRepr.of[Long] ||
                  tpe <:< TypeRepr.of[Float] ||
                  tpe <:< TypeRepr.of[Double] ||
                  tpe <:< TypeRepr.of[BigInt] ||
                  tpe <:< TypeRepr.of[BigDecimal]

  val isPrimitive = tpe.typeSymbol.flags.is(Flags.Final) && 
                   (tpe <:< TypeRepr.of[AnyVal] || tpe =:= TypeRepr.of[String])

  '{
    new TypeInfo[T] {
      def typeName: String = ${Expr(typeName)}
      def isNumeric: Boolean = ${Expr(isNumeric)}
      def isPrimitive: Boolean = ${Expr(isPrimitive)}
    }
  }
}

// Usage
def printTypeInfo[T](value: T)(using typeInfo: TypeInfo[T]): Unit = {
  println(s"Value: $value")
  println(s"Type: ${typeInfo.typeName}")
  println(s"Is numeric: ${typeInfo.isNumeric}")
  println(s"Is primitive: ${typeInfo.isPrimitive}")
}

// These will have compile-time generated TypeInfo instances
printTypeInfo(42)
printTypeInfo("Hello")
printTypeInfo(3.14)
printTypeInfo(List(1, 2, 3))

Macro-Generated DSL

import scala.quoted.*

// DSL for building SQL queries at compile time
case class SqlQuery(sql: String, params: List[Any])

// Macro for type-safe SQL DSL
inline def query(inline builder: QueryBuilder ?=> SqlQuery): SqlQuery = 
  ${ queryImpl('builder) }

def queryImpl(builder: Expr[QueryBuilder ?=> SqlQuery])(using Quotes): Expr[SqlQuery] = {
  import quotes.reflect.*

  // This is a simplified implementation
  // In practice, you'd analyze the AST to validate SQL syntax
  '{ 
    given QueryBuilder = new QueryBuilder()
    $builder
  }
}

class QueryBuilder {
  private var tableName: String = ""
  private var columns: List[String] = List("*")
  private var whereConditions: List[String] = Nil
  private var parameters: List[Any] = Nil

  def select(cols: String*): QueryBuilder = {
    columns = cols.toList
    this
  }

  def from(table: String): QueryBuilder = {
    tableName = table
    this
  }

  def where(condition: String, params: Any*): QueryBuilder = {
    whereConditions = whereConditions :+ condition
    parameters = parameters ++ params
    this
  }

  def build(): SqlQuery = {
    val sql = s"SELECT ${columns.mkString(", ")} FROM $tableName" +
              (if (whereConditions.nonEmpty) s" WHERE ${whereConditions.mkString(" AND ")}" else "")
    SqlQuery(sql, parameters)
  }
}

// Extension methods for QueryBuilder
extension (qb: QueryBuilder) {
  def result: SqlQuery = qb.build()
}

// Usage
val userQuery = query {
  val qb = summon[QueryBuilder]
  qb.select("id", "name", "email")
    .from("users")
    .where("age > ?", 18)
    .where("status = ?", "active")
    .result
}

println(userQuery.sql)
// Output: SELECT id, name, email FROM users WHERE age > ? AND status = ?
println(userQuery.params)
// Output: List(18, active)

Compile-Time Code Generation

Automatic Serialization/Deserialization

import scala.quoted.*
import scala.deriving.Mirror

// Macro for generating JSON serializers
trait JsonEncoder[T] {
  def encode(value: T): String
}

trait JsonDecoder[T] {
  def decode(json: String): Option[T]
}

object JsonCodec {
  inline def derived[T](using m: Mirror.Of[T]): JsonCodec[T] = 
    ${ deriveJsonCodec[T]('m) }
}

trait JsonCodec[T] extends JsonEncoder[T] with JsonDecoder[T]

def deriveJsonCodec[T: Type](mirror: Expr[Mirror.Of[T]])(using Quotes): Expr[JsonCodec[T]] = {
  import quotes.reflect.*

  val tpe = TypeRepr.of[T]
  val typeSymbol = tpe.typeSymbol

  if (typeSymbol.flags.is(Flags.Case)) {
    deriveCaseClassCodec[T](mirror)
  } else {
    report.errorAndAbort(s"Cannot derive JsonCodec for non-case class ${typeSymbol.name}")
  }
}

def deriveCaseClassCodec[T: Type](mirror: Expr[Mirror.Of[T]])(using Quotes): Expr[JsonCodec[T]] = {
  import quotes.reflect.*

  val tpe = TypeRepr.of[T]
  val typeSymbol = tpe.typeSymbol
  val fields = typeSymbol.caseFields

  // Generate encoder
  val encodeImpl = {
    val fieldEncodings = fields.map { field =>
      val fieldName = field.name
      s""""$fieldName": " + encodeValue(value.$fieldName) + """"
    }.mkString(""" + "," + """)

    s"""def encode(value: ${tpe.show}): String = {
       |  "{" + $fieldEncodings + "}"
       |}""".stripMargin
  }

  // Generate decoder (simplified)
  val decodeImpl = s"""def decode(json: String): Option[${tpe.show}] = {
     |  // Simplified JSON parsing - in practice use a proper JSON library
     |  try {
     |    Some(parseJson(json))
     |  } catch {
     |    case _: Exception => None
     |  }
     |}""".stripMargin

  '{
    new JsonCodec[T] {
      def encode(value: T): String = {
        def encodeValue(v: Any): String = v match {
          case s: String => "\"" + s + "\""
          case n: Int => n.toString
          case n: Long => n.toString
          case n: Double => n.toString
          case b: Boolean => b.toString
          case other => "\"" + other.toString + "\""
        }

        // This would be generated per case class
        value match {
          case _ => "{}" // Simplified implementation
        }
      }

      def decode(json: String): Option[T] = {
        // Simplified implementation
        None
      }
    }
  }
}

// Usage
case class Person(name: String, age: Int, email: String)

given JsonCodec[Person] = JsonCodec.derived[Person]

val person = Person("Alice", 30, "alice@example.com")
val json = summon[JsonCodec[Person]].encode(person)

Performance Optimization Macros

import scala.quoted.*

// Macro for unrolling loops at compile time
inline def unroll[T](inline n: Int)(inline body: Int => T): Unit = 
  ${ unrollImpl('n, 'body) }

def unrollImpl[T: Type](n: Expr[Int], body: Expr[Int => T])(using Quotes): Expr[Unit] = {
  n.value match {
    case Some(nValue) if nValue <= 20 => // Limit unrolling to prevent code bloat
      val statements = (0 until nValue).map { i =>
        '{ ${body}(${Expr(i)}) }
      }
      Expr.block(statements.toList, '{ () })
    case Some(nValue) =>
      quotes.reflect.report.warning(s"Loop unrolling skipped for n=$nValue (too large)")
      '{ 
        for (i <- 0 until $n) {
          $body(i)
        }
      }
    case None =>
      quotes.reflect.report.errorAndAbort("Loop count must be a compile-time constant")
  }
}

// Usage
unroll(5) { i =>
  println(s"Iteration $i")
}

// This generates:
// println(s"Iteration 0")
// println(s"Iteration 1") 
// println(s"Iteration 2")
// println(s"Iteration 3")
// println(s"Iteration 4")

// Compile-time memoization
inline def memoize[A, B](inline f: A => B): A => B = 
  ${ memoizeImpl('f) }

def memoizeImpl[A: Type, B: Type](f: Expr[A => B])(using Quotes): Expr[A => B] = {
  '{
    val cache = scala.collection.mutable.Map.empty[A, B]
    (a: A) => cache.getOrElseUpdate(a, $f(a))
  }
}

// Usage
val fibonacci: Int => Long = memoize { n: Int =>
  if (n <= 1) n.toLong
  else fibonacci(n - 1) + fibonacci(n - 2)
}

Error Handling and Diagnostics

Custom Error Messages

import scala.quoted.*

// Macro with detailed error reporting
inline def requirePositive[T](inline value: T)(using num: Numeric[T]): T = 
  ${ requirePositiveImpl('value) }

def requirePositiveImpl[T: Type](value: Expr[T])(using Quotes): Expr[T] = {
  import quotes.reflect.*

  val tpe = TypeRepr.of[T]
  if (!(tpe <:< TypeRepr.of[Numeric[_]])) {
    report.errorAndAbort(s"requirePositive can only be used with numeric types, got ${tpe.show}")
  }

  '{
    val v = $value
    if (summon[Numeric[T]].compare(v, summon[Numeric[T]].zero) <= 0) {
      throw new IllegalArgumentException(s"Expected positive value, got $v")
    }
    v
  }
}

// Macro with position-aware error reporting
inline def todo(inline message: String): Nothing = 
  ${ todoImpl('message) }

def todoImpl(message: Expr[String])(using Quotes): Expr[Nothing] = {
  import quotes.reflect.*

  val pos = Position.ofMacroExpansion
  val fileName = pos.sourceFile.name
  val line = pos.startLine + 1
  val column = pos.startColumn + 1

  val msg = message.valueOrAbort
  report.errorAndAbort(s"TODO: $msg (at $fileName:$line:$column)")
}

// Usage
def calculateSomething(x: Int): Int = {
  val positive = requirePositive(x)
  todo("Implement the actual calculation")  // Compilation error with location
}

Macro Testing and Debugging

import scala.quoted.*

// Macro for inspecting AST at compile time
inline def inspectAst[T](inline expr: T): T = ${ inspectAstImpl('expr) }

def inspectAstImpl[T: Type](expr: Expr[T])(using Quotes): Expr[T] = {
  import quotes.reflect.*

  val term = expr.asTerm
  val pretty = Printer.TreeAnsiCode.show(term)
  val structure = Printer.TreeStructure.show(term)

  println(s"=== AST Inspection ===")
  println(s"Expression: ${term.show}")
  println(s"Type: ${TypeRepr.of[T].show}")
  println(s"Pretty Print:\n$pretty")
  println(s"Structure:\n$structure")
  println(s"====================")

  expr
}

// Usage (will print AST information at compile time)
val result = inspectAst(List(1, 2, 3).map(_ * 2).filter(_ > 2))

// Macro for compile-time benchmarking
inline def compileBenchmark[T](inline name: String)(inline code: T): T = 
  ${ compileBenchmarkImpl('name, 'code) }

def compileBenchmarkImpl[T: Type](name: Expr[String], code: Expr[T])(using Quotes): Expr[T] = {
  val startTime = System.nanoTime()
  val result = code
  val endTime = System.nanoTime()
  val duration = (endTime - startTime) / 1_000_000.0

  val nameStr = name.valueOrAbort
  println(s"Compile-time benchmark '$nameStr': ${duration}ms")

  result
}

Best Practices and Guidelines

Macro Design Principles

// 1. Keep macros simple and focused
// Bad: One macro doing everything
inline def megaMacro[T](inline value: T): String = ${ megaMacroImpl('value) }

// Good: Separate concerns
inline def debug[T](inline value: T): T = ${ debugImpl('value) }
inline def typeInfo[T]: String = ${ typeInfoImpl[T] }

// 2. Provide good error messages
def validateImpl[T: Type](value: Expr[T])(using Quotes): Expr[T] = {
  import quotes.reflect.*

  // Bad: Generic error
  // report.errorAndAbort("Validation failed")

  // Good: Specific, actionable error
  val pos = Position.ofMacroExpansion
  val tpe = TypeRepr.of[T].show
  report.errorAndAbort(
    s"Validation failed for type $tpe at ${pos.sourceFile.name}:${pos.startLine + 1}. " +
    s"Expected a case class with at least one field."
  )
}

// 3. Handle edge cases gracefully
def safeFieldAccess[T: Type](fieldName: Expr[String])(using Quotes): Expr[Option[Any]] = {
  import quotes.reflect.*

  val tpe = TypeRepr.of[T]
  val fieldNameStr = fieldName.valueOrAbort

  val field = tpe.typeSymbol.memberFields.find(_.name == fieldNameStr)
  field match {
    case Some(f) => 
      // Generate safe field access code
      '{ Some(???) }
    case None =>
      report.warning(s"Field '$fieldNameStr' not found in type ${tpe.show}")
      '{ None }
  }
}

// 4. Use types to enforce correctness
trait Validated[T]
object Validated {
  inline def apply[T](inline value: T): Validated[T] = ${ validateAndWrap[T]('value) }
}

def validateAndWrap[T: Type](value: Expr[T])(using Quotes): Expr[Validated[T]] = {
  // Perform compile-time validation
  // Return wrapped value only if validation passes
  '{
    new Validated[T] {
      // Implementation
    }
  }
}

Performance Considerations

// Macro performance best practices

// 1. Cache expensive computations
object MacroCache {
  private val typeInfoCache = scala.collection.mutable.Map.empty[String, String]

  def getTypeInfo[T: Type](using Quotes): String = {
    import quotes.reflect.*
    val tpe = TypeRepr.of[T]
    val key = tpe.show
    typeInfoCache.getOrElseUpdate(key, {
      // Expensive computation here
      generateTypeInfo(tpe)
    })
  }

  private def generateTypeInfo(tpe: quotes.reflect.TypeRepr): String = {
    // Implementation
    tpe.show
  }
}

// 2. Minimize AST traversal
def efficientAnalysis[T: Type](using Quotes): Expr[String] = {
  import quotes.reflect.*

  val tpe = TypeRepr.of[T]

  // Good: Single traversal with accumulated results
  val (fieldCount, methodCount, isCase) = tpe.typeSymbol match {
    case sym =>
      val fields = sym.memberFields
      val methods = sym.memberMethods
      val isCase = sym.flags.is(Flags.Case)
      (fields.length, methods.length, isCase)
  }

  Expr(s"Fields: $fieldCount, Methods: $methodCount, Case class: $isCase")
}

// 3. Use lazy evaluation for optional features
class MacroAnalyzer[T: Type](using Quotes) {
  import quotes.reflect.*

  private val tpe = TypeRepr.of[T]

  lazy val fields = tpe.typeSymbol.memberFields
  lazy val methods = tpe.typeSymbol.memberMethods
  lazy val companions = tpe.typeSymbol.companionModule

  def generateFieldInfo(): Expr[String] = {
    // Only computes fields when actually needed
    Expr(fields.map(_.name).mkString(", "))
  }
}

Real-World Examples

Configuration DSL

// Macro-powered configuration DSL
import scala.quoted.*

case class Config(values: Map[String, Any])

inline def config(inline block: ConfigBuilder ?=> Unit): Config = 
  ${ configImpl('block) }

def configImpl(block: Expr[ConfigBuilder ?=> Unit])(using Quotes): Expr[Config] = {
  '{
    val builder = new ConfigBuilder()
    given ConfigBuilder = builder
    $block
    builder.build()
  }
}

class ConfigBuilder {
  private val values = scala.collection.mutable.Map.empty[String, Any]

  def set(key: String, value: Any): Unit = {
    values(key) = value
  }

  def build(): Config = Config(values.toMap)
}

extension (key: String) {
  def :=(value: Any)(using builder: ConfigBuilder): Unit = {
    builder.set(key, value)
  }
}

// Usage
val appConfig = config {
  "database.url" := "jdbc:postgresql://localhost/mydb"
  "database.username" := "user"
  "database.password" := "secret"
  "server.port" := 8080
  "server.host" := "0.0.0.0"
}

Type-Safe Query Builder

// Advanced type-safe query builder with macros
import scala.quoted.*

trait Table[T] {
  def tableName: String
  def columns: List[String]
}

class Query[T](val sql: String, val parameters: List[Any])

inline def table[T]: Table[T] = ${ deriveTable[T] }

def deriveTable[T: Type](using Quotes): Expr[Table[T]] = {
  import quotes.reflect.*

  val tpe = TypeRepr.of[T]
  val symbol = tpe.typeSymbol
  val tableName = symbol.name.toLowerCase
  val columns = symbol.caseFields.map(_.name)

  '{
    new Table[T] {
      def tableName: String = ${Expr(tableName)}
      def columns: List[String] = ${Expr(columns)}
    }
  }
}

inline def select[T](columns: String*): QueryBuilder[T] = 
  ${ selectImpl[T]('columns) }

def selectImpl[T: Type](columns: Expr[Seq[String]])(using Quotes): Expr[QueryBuilder[T]] = {
  '{
    new QueryBuilder[T](${columns}.toList)
  }
}

class QueryBuilder[T](selectedColumns: List[String]) {
  def from(table: Table[T]): FromClause[T] = {
    new FromClause[T](selectedColumns, table)
  }
}

class FromClause[T](columns: List[String], table: Table[T]) {
  def where(condition: String, params: Any*): WhereClause[T] = {
    new WhereClause[T](columns, table, List(condition), params.toList)
  }

  def build(): Query[T] = {
    val columnStr = if (columns.isEmpty) "*" else columns.mkString(", ")
    val sql = s"SELECT $columnStr FROM ${table.tableName}"
    new Query[T](sql, Nil)
  }
}

class WhereClause[T](
  columns: List[String], 
  table: Table[T], 
  conditions: List[String], 
  params: List[Any]
) {
  def and(condition: String, newParams: Any*): WhereClause[T] = {
    new WhereClause[T](columns, table, conditions :+ condition, params ++ newParams)
  }

  def build(): Query[T] = {
    val columnStr = if (columns.isEmpty) "*" else columns.mkString(", ")
    val whereStr = if (conditions.nonEmpty) s" WHERE ${conditions.mkString(" AND ")}" else ""
    val sql = s"SELECT $columnStr FROM ${table.tableName}$whereStr"
    new Query[T](sql, params)
  }
}

// Usage
case class User(id: Long, name: String, email: String, age: Int)

given Table[User] = table[User]

val query = select[User]("name", "email")
  .from(summon[Table[User]])
  .where("age > ?", 18)
  .and("email LIKE ?", "%@example.com")
  .build()

println(query.sql)
// Output: SELECT name, email FROM user WHERE age > ? AND email LIKE ?

Conclusion

Scala 3 macros provide powerful metaprogramming capabilities for:

Code Generation:

  • Automatic derivation of type class instances
  • Builder pattern generation
  • Serialization/deserialization code
  • Performance optimizations through unrolling

Compile-Time Safety:

  • Static validation and assertions
  • Type-safe DSLs with compile-time checking
  • Configuration validation
  • SQL query verification

Developer Experience:

  • Better error messages with precise locations
  • IDE integration with full type information
  • Debugging tools for macro development
  • Performance analysis at compile time

Best Practices:

  • Keep macros focused and composable
  • Provide clear error messages
  • Cache expensive computations
  • Use types to enforce correctness

Scala 3 macros enable powerful compile-time programming while maintaining type safety and IDE support, making them an excellent tool for library authors and application developers who need advanced code generation and validation capabilities.