Compilation to the JVM and TASTy

Scala · Expert · 16 min

Knowing what the compiler emits explains boxing, binary compatibility issues and profiler output.

scalac compiles in two outputs: JVM class files and TASTy files (.tasty), a typed abstract syntax tree of every definition. TASTy lets later compilations see full Scala types (including those erased in bytecode), lets Scala 2.13 consume Scala 3 libraries through its TASTy reader, and powers macros and tools like documentation and IDE support.

How constructs map to bytecode:

  • A class is a JVM class; a val field is a private field plus an accessor method.
  • An object becomes a class Name$ with a static MODULE$ instance, plus static forwarders on Name for Java callers.
  • A trait becomes a JVM interface with default methods; fields in traits are implemented in the classes that mix them in.
  • Lambdas compile via invokedynamic and LambdaMetafactory, like Java's.
  • Top-level definitions go into a synthetic File$package object.
  • lazy val uses a thread-safe initialisation scheme with extra checks on each access.

Erasure removes type arguments, so generic code works on Object: an Int in a List[Int] or passed to a generic method is boxed to java.lang.Integer. Hot numeric code should use Array[Int] (a real int[]) or specialised structures; opaque types and inline methods avoid boxing that wrappers would add.

Binary compatibility is subtle: adding a parameter with a default or a method to a trait can break already compiled code. Library authors check it with MiMa (sbt-mima-plugin). Inspect output with javap -c -p on the class files.

Example

// Wallet.scala, compile with: scala compile Wallet.scala
// then: javap -p -cp <output dir> Wallet Wallet$ Shape
trait Shape:
  def area: Double
  def describe: String = s"area $area"  // -> interface default method

class Wallet(val owner: String):         // -> private field + owner() accessor
  lazy val id: Int = owner.hashCode      // -> guarded lazy initialisation

object Wallet:                           // -> class Wallet$ with MODULE$
  def empty = Wallet("nobody")           //    + static forwarder Wallet.empty()

def sumAll(xs: List[Int]): Int = xs.sum  // top level -> Wallet$package
def sumArray(xs: Array[Int]): Int =      // Array[Int] is a real int[]: no boxing
  var t = 0; var i = 0
  while i < xs.length do { t += xs(i); i += 1 }
  t

@main def jvm(): Unit =
  println(Wallet.empty.owner)            // nobody
  println(sumArray(Array(1, 2, 3)))      // 6
  println(classOf[Wallet].getDeclaredFields.map(_.getName).toList.contains("owner"))  // true

Remember

  • Objects compile to Name$ with a MODULE$ singleton; traits to interfaces
  • TASTy files keep full types for later compilation, macros and tools
  • Erasure boxes primitives in generic code; use arrays for hot numeric loops
  • Check library binary compatibility with MiMa

Watch out for

  • Assuming a source-compatible change to a library is also binary compatible.

Check yourself

  1. Why can a List[Int] be slower than an Array[Int] for numeric work?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: docs.scala-lang.org/scala3/guides/tasty-overview.html

Builds on

Unlocks

Nothing builds on this yet.