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.
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.
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.
The compiler is a single fat JAR that includes:
| Capability | Command |
|---|---|
| Run programs | vary run |
| Type checking | vary check |
| Test runner | vary test |
| Mutation testing | vary mutate |
| Code formatter | vary fmt |
| Language server | vary lsp |
| Project scaffolding | vary new |
| Artifact caching | vary cache |
| Build and package | vary build |
See the CLI reference for the full list.
Vary's type system maps directly to the JVM:
| Vary type | JVM type |
|---|---|
Int | long (64-bit) |
Float | double (64-bit) |
Bool | boolean |
Str | String |
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.
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 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.
| Omission | Detail |
|---|---|
| No IR | There is no intermediate representation between the AST and JVM bytecode. |
| No interpreter | Vary is always compiled ahead of time. |
| No incremental compilation | The artifact cache provides fast rebuilds, but within a single file, compilation is all-or-nothing. |