minikotlin
Kotlin WebAssembly  ·  a compiler written in C

A Kotlin compiler
that runs in a browser tab.

minikotlin is written from scratch in C and emits WebAssembly GC bytecode by hand — no JVM, no LLVM, no Binaryen, no Gradle. The compiler is itself compiled to WASM, so .kt source goes in and a running .wasm module comes out, entirely in the tab.

backend
WASM-GCstructs · call_ref · EH
server
noneruns client-side
end-to-end tests
366frontend: 657
runtime deps
0nothing installed
greeter — minikotlin Studio
// Main.kt + Greeter.kt compile as one unit
fun main() {
    val g = Greeter("WebAssembly")
    println(g.greet())
    (1..3).forEach { println("tick $it") }
}
build 2 .kt → main.wasm · ok, 41ms
Hello, WebAssembly
tick 1
tick 2
tick 3
the pipeline .kt lex parse sema HIR MIR WASM-GC run
01

One pass, all the way down to bytecode.

No intermediate VM, no external backend. The frontend — lexer, parser, semantic analysis (it’s called mkf) — hands off to two of its own IRs before writing WASM-GC by hand.

input
Kotlin source
Multiple .kt files, compiled as one unit so they can see each other.
frontend · mkf
lex · parse · sema
Names, types and smart-casts resolved. 657 frontend tests.
high IR
HIR
A desugared, typed tree that still sits close to the language.
mid IR
MIR
Lowered to ops, locals, struct layouts and vtables.
codegen
WASM-GC
Bytecode emitted directly. No LLVM, no Binaryen in the loop.
output
main.wasm
Instantiated and run in the same browser tab.

The compiler ships as WASM itself, so it runs where your code runs — no toolchain to install.


02

The Kotlin it speaks today.

Not a token subset. These are lowered properly onto the WASM-GC type system — each one has end-to-end tests behind it.

Classes & objectsobject model
Inheritance (open/override), interfaces with default methods, data class with generated equals/hashCode/copy, enum, and named, companion & anonymous object expressions.
Sealed & smart-castscontrol flow
sealed hierarchies with exhaustive when, is checks compiled to ref.test, and flow-sensitive smart-casting that holds across branches.
Null safetytypes
Nullable types end to end — ?. safe calls, ?: elvis and !! assertions — including nullable primitives, boxed through Any.
Genericstypes
Type parameters on functions and classes — fun <T> id(x: T): T — lowered over a boxed Any representation.
Operators & extensionsergonomics
Operator overloading (plus, get, …) dispatched to the LHS class, extension functions in their own namespace, and custom accessors with a backing field.
Coroutinesnon-blocking
launch, delay and coroutineScope — real suspension compiled as CPS over closures, with no Asyncify, no JSPI and no threads.
Standard libraryhand-written
String/Char operations, list higher-order functions (map/filter/forEach…), kotlin.math, and the scope functions let/apply/run/also/with.

03

How a Kotlin idea becomes a WASM instruction.

The lowering is the interesting part of any compiler. Four real ones — each maps a language construct onto a concrete WASM-GC mechanism, written by hand.

L.01

class instance struct.new

Every class becomes a GC struct type; properties are real struct fields. Allocation is struct.new, not a hand-rolled heap of bytes.

L.02

virtual call call_ref

Open and overridden methods go through a per-class vtable. A virtual call is a function-reference load followed by call_ref — true dynamic dispatch.

L.03

type check ref.test

An is check and a when (x) { is T -> } arm compile to ref.test, and the narrowed value is reused through a ref.cast — smart-casting for free.

L.04

coroutine CPS closure

A suspension point splits the function at the seam and captures the rest as a continuation. A bare delay hands a token to the host and resumes from setTimeout — genuinely off the stack.


04

A specimen, compiled and run.

Everything below is supported Kotlin. The Studio highlights it with the compiler’s own lexer, then runs the resulting WASM in place.

Race.kt
import kotlinx.coroutines.*

sealed class Lane(val id: Int)
class Fast : Lane(1)
class Slow : Lane(2)

fun Lane.pace(): Long = when (this) {
    is Fast -> 120
    is Slow -> 300
}

fun main() = runBlocking {
    val lanes = listOf(Fast(), Slow())
    coroutineScope {
        lanes.forEach { lane ->
            launch {
                delay(lane.pace())
                println("lane ${lane.id} in")
            }
        }
    }
    println("race over")
}

Two coroutines, actually racing.

Each launch suspends at its delay and yields. The faster lane resumes first; coroutineScope waits for both children before the last line runs. No blocking and no Asyncify — the suspension is compiled into continuation closures.

The sealed Lane, the when (this) { is … } dispatch and the Lane.pace() extension are all lowered for real, not interpreted.

> lane 1 in
> lane 2 in
> race over

The Studio is the whole thing.

Make a project, write a few .kt files that compile as one unit so they can see each other, hit Run, and read the output — all in the tab, nothing installed.

Open the Studio multi-file editor · runs offline · 0 dependencies