zio-golem-code-generation

star 131

Generating Scala code in the golem subproject. Use when adding code generation steps, build-time source generators, scalameta AST construction, or sbt/Mill sourceGenerators to the golem/ subtree.

zio By zio schedule Updated 3/26/2026

name: zio-golem-code-generation description: "Generating Scala code in the golem subproject. Use when adding code generation steps, build-time source generators, scalameta AST construction, or sbt/Mill sourceGenerators to the golem/ subtree."

ZIO Golem Code Generation

Guidelines for writing Scala code generators in the golem subproject.

Core Principle: Scalameta AST, Never String Templates

Always construct generated code as scalameta AST nodes using quasiquotes (q"...", t"...", source"...", param"..."). Never use string interpolation or text templates to produce Scala source files.

// ✅ Correct — typed AST
val tree = q"""
  object $name {
    def register(): Unit = {
      ..$registrations
      ()
    }
  }
"""

// ❌ Wrong — string interpolation
val code = s"""object $name {
  def register(): Unit = { ... }
}"""

Shared Codegen Library (golem/codegen/)

All build-time code generation logic lives in golem/codegen/, a pure (no ZIO, no sbt, no Mill) Scala library that cross-compiles to Scala 2.12 (for sbt) and Scala 3.3.7 (for Mill). Both plugins depend on this shared library.

Cross-compilation constraints

Because the library must compile under Scala 2.12:

  • Use import scala.meta.dialects.Scala213 for the implicit dialect needed by quasiquotes and .parse[T] calls.
  • For parsing with a specific dialect, use dialects.Scala3(code).parse[Source] (explicit dialect application) rather than implicit val d: Dialect = ... which causes ambiguity.
  • Use parseMeta[T](code)(implicit parse: Parse[T]) helper pattern for snippet parsing.
  • Avoid Scala 3-only syntax in shared code.

Type and term references

Parse dotted strings into scalameta AST nodes for use in quasiquotes:

private def parseMeta[T](code: String)(implicit parse: Parse[T]): T =
  Scala213(code).parse[T].get

private def parseTermRef(dotted: String): Term.Ref =
  parseMeta[Term](dotted).asInstanceOf[Term.Ref]

private def parseType(tpe: String): Type =
  parseMeta[Type](tpe)

private def parseImporter(dotted: String): List[Importer] =
  parseMeta[Stat](s"import $dotted").asInstanceOf[Import].importers

API pattern

Generators should expose a pure, effect-free API that accepts source text and returns generated outputs + diagnostics:

object MyCodegen {
  final case class GeneratedFile(relativePath: String, content: String)
  final case class Warning(path: Option[String], message: String)
  final case class Result(files: Seq[GeneratedFile], warnings: Seq[Warning])

  def generate(inputs: ...): Result = {
    // 1. Parse/scan inputs
    // 2. Build scalameta AST via quasiquotes
    // 3. Pretty-print via .syntax
    // 4. Return GeneratedFile with relative path + content
  }
}

The plugin wrappers (sbt/Mill) handle file I/O, logging, and build-tool integration.

Build Integration Pattern

sbt Plugin (golem/sbt/)

The sbt plugin GolemPlugin is an AutoPlugin compiled as part of the meta-build via ProjectRef in project/plugins.sbt. It hooks into sourceGenerators:

Compile / sourceGenerators += Def.task {
  val inputs = scalaSources.map { f =>
    MyCodegen.SourceInput(f.getAbsolutePath, IO.read(f))
  }
  val result = MyCodegen.generate(inputs)
  result.warnings.foreach(w => log.warn(s"[golem] ${w.message}"))
  result.files.map { gf =>
    val out = managedRoot / gf.relativePath
    IO.write(out, gf.content)
    out
  }
}.taskValue

For new generators:

  1. Add the pure generation logic to golem/codegen/src/main/scala/golem/codegen/.
  2. Add sbt integration in golem/sbt/src/main/scala/golem/sbt/.
  3. Hook into Compile / sourceGenerators as a .taskValue.
  4. Use FileFunction.cached with FileInfo.hash if the generation has an input file (schema, WIT, etc.) to avoid unnecessary regeneration.

Mill Plugin (golem/mill/)

The Mill plugin GolemAutoRegister is a trait mixed into ScalaJSModule. It uses generatedSources and T { ... } tasks. Follow the same pattern as golemGeneratedAutoRegisterSources.

Shared logic, not duplicated

All generation logic lives in golem/codegen/. The sbt and Mill plugins are thin wrappers that:

  • Collect source files and read their contents
  • Call the shared generate(...) function
  • Log warnings
  • Write returned files under managed/generated roots
  • Configure build-tool-specific hooks (module initializers, compile dependencies)

When adding a new generation step, implement the logic once in golem/codegen/, then add thin wrappers in both GolemPlugin.scala and GolemAutoRegister.scala.

Existing Code Generation in Golem

1. Auto-Registration (shared codegen + sbt/Mill wrappers)

Scans sources for @agentImplementation classes using scalameta's parser, then generates RegisterAgents.scala and per-package __GolemAutoRegister_*.scala files using scalameta quasiquotes.

Files:

  • golem/codegen/src/main/scala/golem/codegen/autoregister/AutoRegisterCodegen.scala — shared logic
  • golem/sbt/src/main/scala/golem/sbt/GolemPlugin.scala — sbt wrapper
  • golem/mill/src/golem/mill/GolemAutoRegister.scala — Mill wrapper

2. Scala 3 Macros (compile-time, not build-time)

Macros generate code at compile time, not as a build step. They live in golem/macros/ and use scala.quoted.*:

  • AgentDefinitionMacro — extracts AgentMetadata from @agentDefinition traits
  • AgentImplementationMacro — generates implementation wrappers from @agentImplementation classes
  • AgentClientMacro — generates RPC client types
  • AgentCompanionMacro — generates companion object boilerplate (get, getPhantom, etc.)

These are not build-time code generators. Do not confuse them with sourceGenerators.

Generation Pipeline Shape

Follow this pipeline for new generators:

1. Load schema/input    (WIT file, annotation scan, external spec)
2. Parse into models    (typed case classes, not raw strings)
3. Classify/transform   (determine what code to emit)
4. Build AST            (scalameta quasiquotes)
5. Pretty-print         (.syntax on the AST root)
6. Return               (GeneratedFile with relativePath + content)

The plugin wrappers handle file writing, formatting, and incremental build integration.

Conventions

  • Pure functions — all generator methods are pure. No ZIO, no sbt/Mill types, no file I/O in the shared library.
  • Trait mixin composition — split generators into traits (ModelGenerator, ClientGenerator, etc.) and mix them into the main codegen class if complexity warrants it.
  • Dialect-aware — use dialects.Scala3 for parsing user sources (with Scala213 fallback). Use Scala213 for quasiquote construction (compatible with both 2.12 and 3.x codegen host).
  • Generated file header — include /** Generated. Do not edit. */ as a comment in generated objects/classes.
  • Output location — write to sourceManaged (sbt) or T.dest (Mill), never to source directories.
Install via CLI
npx skills add https://github.com/zio/zio-blocks --skill zio-golem-code-generation
Repository Details
star Stars 131
call_split Forks 178
navigation Branch main
article Path SKILL.md
More from Creator