minikotlin

A Kotlin compilerthat 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.

Open the Studio

Read a specimen

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.

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

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.

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.

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.

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.

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.

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.

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.

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.

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