A from-scratch Kotlin pipeline written in C —
lexer · parser · sema · HIR · MIR · WASM —
that emits WASM GC structs for class instances,
vtables + call_ref for virtual dispatch, and
WASM EH tags for try/catch.
No JVM bytecode, no Compose runtime, no Java interop —
just the Kotlin language core, compiled directly to a WASM
module your browser can run.
Open classes, override, interface dispatch, nullable
refs with ?. and ?:, default args, data
classes, try/catch — all going through a
proper IR pipeline and out as WASM GC bytes. Sample below: a
polymorphic callSound dispatched through a vtable.
open class Animal { open fun sound(): Int = 0 } class Dog : Animal() { override fun sound(): Int = 1 } class Cat : Animal() { override fun sound(): Int = 2 } // Receiver typed Animal — virtual dispatch via vtable. fun callSound(a: Animal): Int = a.sound() fun testPoly(): Int = callSound(Dog()) + callSound(Cat()) + callSound(Animal())
testPoly() = 3 // 1 (Dog) + 2 (Cat) + 0 (Animal)
Same source → run it yourself in the
playground (single-sample
editor / output split), or open the
full test suite and watch all 27
compiled .wasm modules instantiate via
WebAssembly.instantiate against the same protocol
the Node CI uses.
Every Kotlin class lowers to a real WASM GC struct
— primary-ctor properties, val / var
fields, this.field reads/writes. No
linear-memory bump allocator, no shadow stack — the engine's
GC tracks instances natively.
open
class Sub : Base() emits proper subtype
declarations. Field layouts inherit; (ref Sub)
flows where (ref Base) is expected via WASM GC's
structural subtyping — no runtime cast needed.
call_ref
Each open class gets a per-class vtable struct (subtyped down
the chain) and a vtable global initialized via
ref.func. Method calls load the receiver's
__vt and call_ref the slot — true
polymorphism end-to-end.
interface I { fun m(): Int } compiles to an
abstract base; class C : I overrides via the
same vtable machinery. Single-interface implementations
dispatch polymorphically through an interface-typed param.
?. / ?:
null literal → ref.null;
b == null → ref.is_null;
?. and ?: lower to block-as-expression
with a hidden temp + null-check if. Ref-typed
chain like o?.inner ?: Inner(-1) works
end-to-end.
fun f(x: Int = 1) — call-site splicing. Each
missing arg lowers to the captured default expression as if
inlined. No runtime $default wrapper, no
bitmask — constants fold straight through.
try / catch via WASM EH
Each thrown class gets its own tag with signature
(ref null $Class) → (). throw emits
the EH proposal's throw op; try { }
catch (e: T) { } compiles to a
try_table + nested handler block dispatching by
tag.
when / for / while
when (x) { 1 -> ...; in 0..9 -> ...; else -> ... }
desugars to a nested if-cascade.
for (i in lo..hi) specializes to a
while with i++. Mutable locals,
structured WASM block/loop/br_if
— no Relooper needed.
object
companion object { val DEFAULT = 42 } and
top-level object Singleton { val X = 7 } get
their properties promoted to mangled module globals.
Box.DEFAULT resolves to a static read.
mkf (Mini Kotlin Frontend) handles the first three
stages — tokenisation, parsing, type resolution; vendored into
the backend as a static library. HIR stays
Kotlin-aware (smart casts, when-cascades, lambdas-as-expression),
MIR is mutable-locals three-address code that maps
one-to-one onto WASM local.get /
local.set, and the codegen emits binary
struct.new, call_ref,
try_table, and friends directly — no
wabt, no binaryen.
val, fun bodiesif/else, when, while, for (i in lo..hi)val/var, mutation, recursionclass with primary ctor, val/var fields, methods, thisopen / override with vtable dispatchinterface (single impl) via the same vtable pathobject property promotionnull literal, ?., ?:data class, enum class entriesthrow / try/catch via WASM EHString + heap (UTF-16 array16, no Compose)+, .., in)"x = $x")FunctionN classList, Map, iterator protocollet, run, apply)println, performance.now)inline+reified, reflection — v2