Compiling Kotlin to Jack Bytecode: Adding a Jack Target to the Kotlin Compiler
The article shows how to extend the Kotlin compiler with a new Jack target by reusing its front‑end, adding a CLI module and a backend that transforms Kotlin IR into Jack bytecode, then running the generated .jack files on the Nand2Tetris virtual machine.
This article explains how to extend the Kotlin compiler with a new target that generates Jack bytecode, using the Jack language and virtual machine introduced in the Nand2Tetris project.
Jack Language and Bytecode
Jack is a simple, object‑oriented, weakly typed language whose programs start from a main function inside a Main class. A minimal example is:
class Main {
function void main() {
do Output.printInt(1 + (2 * 3));
return;
}
}The corresponding Jack VM commands (stack‑based) are:
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
returnThe VM supports four command categories: arithmetic, memory access, program flow, and function call.
Kotlin → Jack Compilation Flow
To add a Jack target, the Kotlin compiler’s front‑end can be reused; the main work is converting the intermediate representation (IR) to Jack bytecode. The required steps are:
Add a Kotlin/Jack standard library (e.g., an Output class).
Create a cli-jack module that parses command‑line arguments and invokes the backend.
Create a backend-jack module that transforms IR into Jack bytecode.
cli‑jack Module
The entry point is a class extending CLICompiler . When kotlinc is invoked, the org.jetbrains.kotlin.preloading.Preloader loads the appropriate target class.
class K2JackCompiler : CLICompiler
() {
override val defaultPerformanceManager = K2JSCompilerPerformanceManager()
override fun createMetadataVersion(versionArray: IntArray): BinaryVersion = KlibMetadataVersion(*versionArray)
override fun createArguments(): K2JSCompilerArguments = K2JSCompilerArguments()
override fun executableScriptFileName(): String = "kotlinc-jack"
override fun MutableList
.addPlatformOptions(arguments: K2JSCompilerArguments) { }
override fun doExecute(
arguments: K2JSCompilerArguments,
configuration: CompilerConfiguration,
rootDisposable: Disposable,
paths: KotlinPaths?
): ExitCode {
// 1. parse CLI args
val outputDirPath = arguments.outputDir ?: return COMPILATION_ERROR
val outputName = arguments.moduleName ?: return COMPILATION_ERROR
// 2. generate FIR
val firOutput = compileModulesToAnalyzedFirWithLightTree(...)
// 3. FIR → IR
val fir2IrActualizedResult = transformFirToIr(...)
// 4. IR → Jack bytecode
IrModuleToJackTransformer().generateCode(fir2IrActualizedResult.irModuleFragment, outputDirPath, outputName)
return OK
}
}backend‑jack Module
The backend implements the IR‑to‑Jack transformation using a visitor pattern. Key classes include:
class IrModuleToJackTransformer {
fun generateCode(irModule: IrModuleFragment, outputDirPath: String, outputName: String) {
irModule.files.forEach { file ->
val context = JackGenerationContext(outputDirPath, outputName)
file.accept(IrFileToJackTransformer(), data = context)
}
}
}
class IrFileToJackTransformer : BaseIrElementToJackTransformer() {
override fun visitFile(declaration: IrFile, context: JackGenerationContext) {
super.visitFile(declaration, context)
declaration.declarations.forEach { it.accept(IrDeclarationToJackTransformer(), context) }
}
}
// ... other visitors for expressions, statements, etc.Running the Compiler
After building the cli-jack and backend-jack modules, the compiler can be executed via the preloader:
package org.jetbrains.kotlin.preloading;
@SuppressWarnings("UseOfSystemOutOrSystemErr")
public class Preloader {
public static void main(String[] args) throws Exception {
try {
String[] testArgs = {
"-cp", "./dist/kotlinc/lib/kotlin-compiler.jar",
"org.jetbrains.kotlin.cli.jack.K2JackCompiler",
"-libraries", "./dist/kotlinc/lib/kotlin-stdlib-js.klib",
"-ir-output-dir", "/path/to/out",
"-ir-output-name", "FibRecursive",
"/path/to/FibRecursive.kt"
};
run(testArgs);
} catch (PreloaderException e) {
System.err.println("error: " + e.toString());
}
}
}The generated .jack files can be executed on the online Nand2Tetris VM ( https://nand2tetris.github.io/web-ide/vm ) to verify correctness.
Conclusion
The article demonstrates that, thanks to Kotlin’s layered architecture, adding a new compilation target is straightforward: implement a backend module that translates IR to the desired bytecode and wire it through a CLI module. The example shows a complete flow from Kotlin source to Jack bytecode, including language basics, VM command set, and practical compiler code.
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.