プログラミング
Minikotlin: ブラウザ内でKotlinをWebAssemblyにコンパイルする
要約
Minikotlinは、C言語でゼロから書かれたKotlinコンパイラで、JVMやLLVMなどの外部ツールに依存せず、直接WebAssembly GCバイトコードを生成します。コンパイラ自体もWASMにコンパイルされるため、ブラウザタブ内でKotlinソースコードを入力として受け取り、実行可能なWASMモジュールを出力できます。これにより、ツールチェーンのインストール不要で、ブラウザ上でエンドツーエンドのコンパイルと実行が可能になります。
全文翻訳
Kotlin → WebAssembly · Cで書かれたコンパイラブラウザタブで動作するKotlinコンパイラ。minikotlinはC言語でゼロから書かれており、手作業でWebAssembly GCバイトコードを生成します — JVM、LLVM、Binaryen、Gradleは一切使用しません。コンパイラ自体がWASMにコンパイルされるため、.ktソースを入力すると、タブ内で完全に実行可能な.wasmモジュールが出力されます。
Studioを開く
specimen backendWASM-GCstructs · call_ref · EH servernoneruns client-side end-to-end tests366frontend: 657 runtime deps0nothing installed greeter — minikotlin Studio RUN 12345678 // Main.kt + Greeter.ktを1つのユニットとしてコンパイルします 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 1パスで、バイトコードまで全て処理します。中間VMや外部バックエンドはありません。フロントエンド — レキサー、パーサー、セマンティックアナライザ(mkfと呼ばれます) — は、WASM-GCを手書きする前に、独自のIRを2つ経由します。
input Kotlin source
複数の.ktファイルが1つのユニットとしてコンパイルされるため、互いに参照できます。
frontend · mkf lex · parse · sema
名前、型、スマートキャストが解決されます。657のフロントエンドテスト。
high IR
HIR
言語に近い、デシュガーされた型付きツリー。
mid IR
MIR
オペレーション、ローカル変数、構造体レイアウト、vtableに低レベル化されます。
codegen
WASM-GC
バイトコードが直接生成されます。LLVMやBinaryenはループに含まれません。
output main.wasm
同じブラウザタブでインスタンス化され、実行されます。
コンパイラ自体がWASMとして提供されるため、コードが実行される場所で動作します — インストールするツールチェーンはありません。
02 今日サポートされているKotlin。
トークンのサブセットではありません。これらはWASM-GCの型システムに適切に低レベル化されており、それぞれにエンドツーエンドのテストがあります。
Classes & objects
オブジェクトモデル
継承(open/override)、デフォルトメソッドを持つインターフェース、生成されたequals/hashCode/copyを持つデータクラス、enum、名前付き、コンパニオン、匿名オブジェクト式。
Sealed & smart-casts
制御フロー
網羅的なwhenとisチェックを持つsealed階層はref.testにコンパイルされ、フローセンシティブなスマートキャストはブランチをまたいで保持されます。
Null safety
型
ull許容型のエンドツーエンド — ?.セーフコール、?:エルビス演算子、!!アサーション — プリミティブ型もnull許容で、Any経由でボックス化されます。
Generics
型
関数とクラスの型パラメータ — fun <T> id(x: T): T — ボックス化されたAny表現に低レベル化されます。
Operators & extensions
エルゴノミクス
演算子オーバーロード(plus, get, …)はLHSクラスにディスパッチされ、拡張関数は独自の名前空間に、カスタムアクセサはバックフィールドを持ちます。
Coroutines
ノンブロッキングなlaunch、delay、coroutineScope — 非同期変換(Asyncify)やJSPI、スレッドなしで、実際のサスペンションがクロージャ上のCPSとしてコンパイルされます。
Standard library
手書きのString/Char操作、リストの高階関数(map/filter/forEach…)、kotlin.math、スコープ関数let/apply/run/also/with。
03 KotlinのアイデアがWASM命令になるまで。
低レベル化は、あらゆるコンパイラにおける興味深い部分です。4つの実際の低レベル化ステップ — それぞれが言語構成要素を具体的なWASM-GCメカニズムにマッピングし、手作業で記述されています。
L.01 class instance → struct.new
すべてのクラスはGC構造体型になり、プロパティは実際の構造体フィールドになります。アロケーションは、手作りのバイトヒープではなく、struct.newです。
L.02 virtual call → call_ref
openおよびoverrideされたメソッドは、クラスごとのvtableを経由します。仮想呼び出しは、関数参照のロードとそれに続くcall_ref — 真の動的ディスパッチです。
L.03 type check → ref.test
isチェックとwhen (x) { is T -> }のアームはref.testにコンパイルされ、絞り込まれた値はref.castを通じて再利用されます — スマートキャストが無料で利用できます。
L.04 coroutine → CPS closure
サスペンションポイントは関数をその境界で分割し、残りを継続としてキャプチャします。単純なdelayはホストにトークンを渡し、setTimeoutから再開します — スタックから完全に外れます。
04 サンプルコードのコンパイルと実行。
以下はすべてサポートされているKotlinです。Studioはコンパイラ自身のレキサーでこれをハイライトし、結果のWASMをその場で実行します。
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 {
launch {
delay(lane.pace())
println("lane ${lane.id} in")
}
}
}
println("race over")
}
2つのコルーチンが実際にレースしています。各launchはdelayでサスペンドし、制御を譲ります。速いレーンが先に再開し、coroutineScopeは最後の行が実行される前に両方のチャイルドコルーチンの完了を待ちます。ブロッキングもAsyncifyもなし — サスペンションは継続クロージャにコンパイルされます。sealedクラスLane、when (this) { is … }ディスパッチ、Lane.pace()拡張機能はすべて、解釈ではなく、実際に低レベル化されています。
> lane 1 in
> lane 2 in
> race over