About

The compiler

The Vary compiler takes .vary source files and produces JVM bytecode. It is an ahead-of-time compiler: there is no interpreter.

The compiler is written in Kotlin. It shares the JVM runtime with the code it produces. The bytecode generator uses the ASM library directly, and runtime support functions are @JvmStatic methods that compiled Vary code calls without a foreign function boundary. Kotlin's sealed classes model the AST, coroutines drive the language server, and the Java ecosystem provides classloader isolation for the mutation engine.

How it works

The compiler is a pipeline. Each stage takes the output of the previous stage and produces a new representation:

Source text
  → Lexer (tokens)
  → Parser (AST)
  → Constant folder (optimized AST)
  → Dead code eliminator (trimmed AST)
  → Type checker (typed AST + symbol table)
  → Bytecode generator (JVM .class bytes)

There is no intermediate representation between the AST and JVM bytecode. The codegen phase walks the typed AST and emits bytecode instructions directly. This keeps the compiler simple, auditable, and deterministic.

Compilation model

Modules are compiled recursively. When the type checker encounters an import, it compiles the imported module first, extracts its public type signatures, and continues checking the importing module. The full module graph is then traversed to determine load order.

Compilation is deterministic given the same source, compiler version, and flags. Compiled bytecode is cached using a content-addressed scheme keyed on the SHA-256 hash of those inputs. Multiple variants of the same file (normal, debug, coverage, mutation) coexist in the cache without conflict. When the source has not changed, recompilation is skipped entirely.

What ships in the compiler

The compiler is a single fat JAR that includes:

CapabilityCommand
Run programsvary run
Type checkingvary check
Test runnervary test
Mutation testingvary mutate
Code formattervary fmt
Language servervary lsp
Project scaffoldingvary new
Artifact cachingvary cache
Build and packagevary build

See the CLI reference for the full list.

Type system

Vary's type system maps directly to the JVM:

Vary typeJVM type
Intlong (64-bit)
Floatdouble (64-bit)
Boolboolean
StrString
List[T]ArrayList
Dict[K, V]HashMap
Set[T]HashSet

One integer type and one float type. No width selection, no signed/unsigned distinction. This is a deliberate simplification.

Null safety is enforced at compile time. T? marks a value that may be None. Flow narrowing inside if x is not None { } blocks promotes T? to T without wrapper types or monadic syntax.

Contracts

Functions can declare in {} preconditions, out(r) {} return-value postconditions, and old() entry-state captures. Classes and data types can declare invariant {} blocks. These all compile to runtime checks that throw ContractViolation on failure. The mutation engine treats contract violations as kills, so contracts double as test oracles without additional test code. See Contracts for the full reference.

Mutation

Mutation testing operates at both AST and JVM bytecode levels. Bytecode mutation patches individual instructions and runs each variant in an isolated classloader, reusing the artifact cache. AST mutation rewrites the syntax tree before compilation. Both levels are built into the compiler, not bolted on as external tools. See Mutation testing for details.

What the compiler does not do

OmissionDetail
No IRThere is no intermediate representation between the AST and JVM bytecode.
No interpreterVary is always compiled ahead of time.
No incremental compilationThe artifact cache provides fast rebuilds, but within a single file, compilation is all-or-nothing.
← Language design choices
Performance →