Skip to main content

ScalaEmitter

ScalaEmitter is the core emission engine that converts IR models to formatted Scala source code. It provides the main entry point and methods to emit any IR construct as a properly formatted string.

Use Cases

  • Converting a complete ScalaFile IR to a Scala source string
  • Emitting individual type definitions as Scala code
  • Generating imports, annotations, methods, and type references
  • Formatting code with consistent indentation and style

Main Emission Method

Emit a complete Scala file:

import zio.blocks.codegen.ir._
import zio.blocks.codegen.emit._

val file = ScalaFile(
packageDecl = PackageDecl("com.example"),
types = List(
CaseClass("User", List(Field("id", TypeRef.Long)))
)
)

val config = EmitterConfig()
val sourceCode = ScalaEmitter.emit(file, config)
// sourceCode is a String ready to write to a .scala file

Key Operations

All core operations are shown below:

Emitting Type References

Convert a type reference to Scala syntax:

import zio.blocks.codegen.ir._
import zio.blocks.codegen.emit._

// These methods are used internally by emit, but available if you need them:
ScalaEmitter.emitTypeRef(TypeRef.String)
// Returns: "String"

ScalaEmitter.emitTypeRef(TypeRef.list(TypeRef.Int))
// Returns: "List[Int]"

ScalaEmitter.emitTypeRef(TypeRef.map(TypeRef.String, TypeRef.Int))
// Returns: "Map[String, Int]"

Emitting Type Parameters

Emit generic type parameters with bounds:

import zio.blocks.codegen.ir._

val unbounded = TypeParam("T")
// Emits: "T"

val bounded = TypeParam("T", upperBound = Some(TypeRef("Serializable")))
// Emits: "T <: Serializable"

val covariant = TypeParam("T", variance = Variance.Covariant)
// Emits: "+T"

Emitting Annotations

Convert annotations to Scala syntax:

import zio.blocks.codegen.ir._

val deprecated = Annotation("deprecated")
// Emits: "@deprecated"

val withArg = Annotation("Deprecated", args = List("message" -> "\"Use newMethod instead\""))
// Emits: "@Deprecated(message = \"Use newMethod instead\")"

Configuration

The emitter respects EmitterConfig settings:

import zio.blocks.codegen.ir._
import zio.blocks.codegen.emit._

val file = ScalaFile(PackageDecl("com.example"))

val config = EmitterConfig(
indentWidth = 2, // Spaces per indent level
sortImports = true, // Sort import statements
trailingCommas = true, // Add trailing commas in collections
scala3Syntax = true // Scala 3 syntax features
)

ScalaEmitter.emit(file, config)

See EmitterConfig for all available options.

Examples

Practical examples demonstrate common usage:

Example 1: Emit a Complete File

Generate a Scala file with multiple types:

import zio.blocks.codegen.ir._
import zio.blocks.codegen.emit._

val file = ScalaFile(
packageDecl = PackageDecl("com.api"),
imports = List(
Import.WildcardImport("zio"),
Import.SingleImport("scala.collection", "Seq")
),
types = List(
CaseClass(
"ApiResponse",
List(
Field("status", TypeRef.Int),
Field("data", TypeRef.optional(TypeRef.String))
)
)
)
)

val config = EmitterConfig(indentWidth = 2)

Emits:

import zio.blocks.codegen.emit._
ScalaEmitter.emit(file, config)
// res6: String = """package com.api
//
// import scala.collection.Seq
// import zio.*
//
// case class ApiResponse(
// status: Int,
// data: Option[String],
// )
// """

Example 2: Cross-Scala Compatibility

Generate code for different Scala versions:

import zio.blocks.codegen.ir._
import zio.blocks.codegen.emit._

val file = ScalaFile(
packageDecl = PackageDecl("com.example"),
types = List(
Enum(
name = "Status",
cases = List(
EnumCase.SimpleCase("Active"),
EnumCase.SimpleCase("Inactive")
)
)
)
)

// For Scala 3: emits actual enum
val scala3Config = EmitterConfig(
scala3Syntax = true
)
val scala3Code = ScalaEmitter.emit(file, scala3Config)

// For Scala 2: emits sealed trait (fallback)
val scala2Config = EmitterConfig(
scala3Syntax = false
)
val scala2Code = ScalaEmitter.emit(file, scala2Config)

Scala 3 emits:

import zio.blocks.codegen.emit._
scala3Code
// res8: String = """package com.example
//
// enum Status {
// case Active, Inactive
// }
// """

Scala 2 emits:

import zio.blocks.codegen.emit._
scala2Code
// res9: String = """package com.example
//
// sealed trait Status
//
// object Status {
// case object Active extends Status
// case object Inactive extends Status
// }
// """

Example 3: Sealed Trait with Multiple Cases

Emit a complete ADT:

import zio.blocks.codegen.ir._
import zio.blocks.codegen.emit._

val file = ScalaFile(
packageDecl = PackageDecl("com.errors"),
types = List(
SealedTrait(
name = "DomainError",
cases = List(
SealedTraitCase.CaseClassCase(
CaseClass("ValidationError", List(
Field("field", TypeRef.String),
Field("message", TypeRef.String)
))
),
SealedTraitCase.CaseClassCase(
CaseClass("NotFound", List(
Field("resource", TypeRef.String),
Field("id", TypeRef.Long)
))
),
SealedTraitCase.CaseObjectCase("Unauthorized"),
SealedTraitCase.CaseObjectCase("InternalError")
)
)
)
)

val config = EmitterConfig(trailingCommas = true)

Emits:

import zio.blocks.codegen.emit._
ScalaEmitter.emit(file, config)
// res11: String = """package com.errors
//
// sealed trait DomainError
//
// object DomainError {
// case class ValidationError(
// field: String,
// message: String,
// ) extends DomainError
// case class NotFound(
// resource: String,
// id: Long,
// ) extends DomainError
// case object Unauthorized extends DomainError
// case object InternalError extends DomainError
// }
// """

Example 4: Generic Types

Emit polymorphic types with proper type parameter syntax:

import zio.blocks.codegen.ir._
import zio.blocks.codegen.emit._

val file = ScalaFile(
packageDecl = PackageDecl("com.containers"),
types = List(
CaseClass(
name = "Wrapper",
fields = List(Field("value", TypeRef("A"))),
typeParams = List(
TypeParam("A")
)
),
CaseClass(
name = "Pair",
fields = List(
Field("left", TypeRef("A")),
Field("right", TypeRef("B"))
),
typeParams = List(
TypeParam("A"),
TypeParam("B")
)
)
)
)

Emits:

import zio.blocks.codegen.emit._
ScalaEmitter.emit(file, EmitterConfig())
// res13: String = """package com.containers
//
// case class Wrapper[A](
// value: A,
// )
//
// case class Pair[A, B](
// left: A,
// right: B,
// )
// """

Example 5: Formatting Customization

Control code style with configuration:

import zio.blocks.codegen.ir._
import zio.blocks.codegen.emit._

val file = ScalaFile(
packageDecl = PackageDecl("com.example"),
imports = List(
Import.WildcardImport("scala.collection"),
Import.SingleImport("java.time", "Instant")
),
types = List(
CaseClass(
name = "Record",
fields = List(
Field("a", TypeRef.String),
Field("b", TypeRef.Int),
Field("c", TypeRef.Boolean)
)
)
)
)

// Wide indentation, sorted imports, trailing commas
val config = EmitterConfig(
indentWidth = 4,
sortImports = true,
trailingCommas = true
)

Emits:

import zio.blocks.codegen.emit._
ScalaEmitter.emit(file, config)
// res15: String = """package com.example
//
// import java.time.Instant
// import scala.collection.*
//
// case class Record(
// a: String,
// b: Int,
// c: Boolean,
// )
// """

Design Philosophy

ScalaEmitter follows three principles:

  1. Pure: No side effects. Takes IR + config, returns a string. Your code writes files.
  2. Configurable: EmitterConfig controls formatting. Add options as needed for your generator.
  3. Cross-Scala: Targets both Scala 3 (enums, derives, * imports) and Scala 2 (sealed traits, _ imports) from the same IR.