Kotlin â WebAssembly · a compiler written in C
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.
backendWASM-GCstructs · call_ref · EH
servernoneruns client-side
end-to-end tests366frontend: 657
runtime deps0nothing installed
greeter â minikotlin Studio
12345678
// 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.
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.
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