MiniKotlin – A Kotlin Compiler That Runs in a Browser Tab

TheWiggles1 pts0 comments

minikotlin — a Kotlin compiler that runs in a browser tab

Kotlin → WebAssembly · a compiler written in C<br>A Kotlin compiler<br>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 .

Open the Studio

Read a specimen

backendWASM-GCstructs · call_ref · EH

servernoneruns client-side

end-to-end tests366frontend: 657

runtime deps0nothing installed

greeter — minikotlin Studio

RUN

greeter / src

Main.kt

Greeter.kt

output

Console

12345678

// Main.kt + Greeter.kt compile as one unit<br>fun main() {<br>val g = Greeter("WebAssembly")<br>println(g.greet())<br>(1..3).forEach { println("tick $it") }

build 2 .kt &rarr; main.wasm · ok, 41ms

Hello, WebAssembly

tick 1

tick 2

tick 3

the pipeline<br>.kt →<br>lex→<br>parse→<br>sema→<br>HIR →<br>MIR →<br>WASM-GC →<br>run

01<br>One pass, all the way down to bytecode.

No intermediate VM, no external backend. The frontend — lexer, parser, semantic analysis (it&rsquo;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<br>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<br>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<br>sealed hierarchies with exhaustive when, is checks compiled to ref.test, and flow-sensitive smart-casting that holds across branches.

Null safetytypes<br>Nullable types end to end — ?. safe calls, ?: elvis and !! assertions — including nullable primitives, boxed through Any.

Genericstypes<br>Type parameters on functions and classes — fun id(x: T): T — lowered over a boxed Any representation.

Operators & extensionsergonomics<br>Operator overloading (plus, get, …) dispatched to the LHS class, extension functions in their own namespace, and custom accessors with a backing field.

Coroutinesnon-blocking<br>launch, delay and coroutineScope — real suspension compiled as CPS over closures, with no Asyncify, no JSPI and no threads.

Standard libraryhand-written<br>String/Char operations, list higher-order functions (map/filter/forEach…), kotlin.math, and the scope functions let/apply/run/also/with.

03<br>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<br>A specimen, compiled and run.

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

Race.kt

import kotlinx.coroutines.*

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

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

fun main() = runBlocking {<br>val lanes = listOf(Fast(), Slow())<br>coroutineScope {<br>lanes.forEach { lane -><br>launch {<br>delay(lane.pace())<br>println("lane ${lane.id} in")<br>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,...

lane wasm kotlin compiler class runs

Related Articles