Compiling Kotlin to Jack Bytecode: Adding a New Kotlin Target
The article shows how to extend Kotlin by adding a Jack‑bytecode backend, detailing the required standard‑library, CLI module, and IR‑to‑Jack transformer implementation, and demonstrates compiling a simple Kotlin program to Jack bytecode that runs correctly on the Nand2Tetris virtual machine.
Last year the author read the book "Computer Systems: From Zero to Modern Computer" which guides building a 16‑bit Hack computer from NAND gates, and after completing its projects you can construct a Hack computer, develop an assembler compiler and a stack‑based VM, and design the high‑level Jack language with its compiler and standard library.
Jack is a simple object‑oriented weakly‑typed language whose execution always starts from the main function of a Main class. A minimal example is shown:
class Main {
function void main() {
do Output.printInt(1 + (2 * 3));
return;
}
}The article explains the four kinds of Jack VM commands (arithmetic, memory access, program flow, function call) and shows the translation of the above program into Jack bytecode.
function Main.main 0
push constant 1
push constant 2
push constant 3
call Math.multiply 2
add
call Output.printInt 1
pop temp 0
push constant 0
returnIt then discusses how to add a new Kotlin target that emits Jack bytecode. Because Kotlin’s compiler is layered, only the backend that converts IR to Jack bytecode needs to be implemented. The required steps are:
Add a Kotlin/Jack standard library (e.g., the Output class).
Create a cli-jack module that parses command‑line arguments, invokes the existing Kotlin front‑end, and calls the new backend.
Create a backend-jack module that implements an IrModuleToJackTransformer and visitor classes to walk the IR and emit Jack instructions.
Key snippets of the new modules are presented, for example the CLI compiler entry class:
class K2JackCompiler : CLICompiler
() {
override fun createMetadataVersion(versionArray: IntArray): BinaryVersion {
return KlibMetadataVersion(*versionArray)
}
// ... other overrides ...
override fun doExecute(arguments: K2JSCompilerArguments,
configuration: CompilerConfiguration,
rootDisposable: Disposable,
paths: KotlinPaths?): ExitCode {
// generate FIR, transform to IR, then to Jack bytecode
IrModuleToJackTransformer().generateCode(irModuleFragment, outputDirPath, outputName)
return OK
}
}The backend transformer walks each IR file and delegates to IrFileToJackTransformer which generates the corresponding Jack instructions.
class IrModuleToJackTransformer {
fun generateCode(irModule: IrModuleFragment, outputDirPath: String, outputName: String) {
irModule.files.forEach { file ->
val context = JackGenerationContext(outputDirPath, outputName)
file.accept(IrFileToJackTransformer(), data = context)
}
}
}After building the modules, the compiler can be invoked via the preloader class:
org.jetbrains.kotlin.preloading.Preloader.main(args)Running the generated Jack bytecode on the online Nand2Tetris VM confirms correctness. The article concludes that extending Kotlin with a new target is straightforward thanks to its modular architecture.
Sohu Tech Products
A knowledge-sharing platform for Sohu's technology products. As a leading Chinese internet brand with media, video, search, and gaming services and over 700 million users, Sohu continuously drives tech innovation and practice. We’ll share practical insights and tech news here.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.