作ったものの紹介
GitHub:
https://github.com/lechatthecat/willow
Willowは、Craneliftを利用してネイティブコードへコンパイルする、実験的な静的型付きプログラミング言語です。
クラスベースのOOP、代数的enum、パターンマッチ、Option / Result、GCに加え、タスク、チャネル、キャンセル、selectを備えたスタックレスな非同期ランタイムを実装しています。
このプロジェクトを始めた理由の一つは、コンパイラ、GC、スケジューラをそれぞれ独立したものとして扱うのではなく、最初から一緒に設計した場合に、それらがどう相互作用するのかを理解したかったからです。
Willowはまだプロダクションで使える段階ではありません。
ただ、ある程度複雑なプログラムを実行でき、複数モジュールをコンパイルでき、既存の処理系と意味のあるベンチマークを取れるところまでは来ました。
なぜ作ったのか
自分が好きな言語を見ていると、それぞれ異なる場所でトレードオフを選んでいます。
Goは言語やツールをシンプルに保ち、軽量な並行処理を強力にサポートしています。一方で、伝統的なクラスベースOOPとは異なる設計を採用しています。
Javaは成熟したエコシステム・OOPを持っていますがJVMを必要とします。
Rustは非常に強い制御性と高い性能を提供しますが、所有権や借用についてプログラマが明示的に考える必要があります。
Willowでは、それらとは少し違う組み合わせを試しています。
- ネイティブAOTコンパイル
- GCによる自動メモリ管理
- クラスベースOOP
- パターンマッチ (match)
- Option / Result
- null/nilではなくOption::Noneを使う
- panic / defer
- 軽量な構造化並行処理
これらを、一つの言語とランタイムの中で組み合わせたらどうなるのかを試すプロジェクトです。
これ以前に試したこと
もともと言語の作成には興味がありまして、昔スクリプト言語を作ったことがありました。例えばこのQiitaの記事は6年前の記事ですが、とりあえず動くスクリプト言語を作ったときの記事です。
とりあえず動くスクリプト言語を作るだけでも大変でした。さらにいうと、せっかく作ったのに、ものすごく遅くて使い物になりませんでした。
python3とかはパフォーマンス的にはあまり速い言語ではないと言われることがありますが、僕が当時作った言語はその何十倍も遅い言語でした。
そこでJITを導入しようと思いました。そのときに目をつけたのがCraneliftです。ですが、CraneliftをJITのためにスクリプトに組み込むのはとても大変な作業でした。
せっかくCraneliftを使って時間をかけて作るなら、Webエンジニアとして普段働いている自分が昔から欲しかった、サーバーサイドの開発に使いやすい、ネイティブバイナリにコンパイル可能なGC付き言語をCranelistを使って作ろうと思いました。
(今なら「Goを使えばよくね?」と言われそうですが、Goが一般的になる以前はあまり選択肢がなかったんです…。)
なんでCraneliftか
LLVMのほうがCraneliftよりも高いパフォーマンスをもっているんですが、CraneliftのほうがRustから扱い安いし、またコンパイルもCraneliftのほうが速いです。
Willowがどんな言語か
例えば、enumは値を持つことができ、matchでパターンマッチできます。
enum Shape {
Circle(f64),
Rectangle(f64, f64),
Dot,
}
fn area(shape: Shape) -> f64 {
return match shape {
Shape::Circle(r) => r * r * 3.14159,
Shape::Rectangle(w, h) => w * h,
Shape::Dot => 0.0,
};
}
async fn compute() -> f64 {
await sleep(1);
return area(Shape::Circle(5.0));
}
async fn main() {
let task = compute();
println(await task);
}
クラスとインターフェースも、言語機能として組み込んでいます。
interface Animal {
fn speak(self) -> String;
}
class Dog implements Animal {
pub fn speak(self) -> String {
return "woof";
}
}
fn describe(animal: Animal) {
println(animal.speak());
}
fn main() {
describe(new Dog());
}
matchの例
fn heading(d: Direction) -> String {
return match d {
Direction::North => "north",
Direction::East => "east",
Direction::South => "south",
_ => "west"
};
}
defer, matchの例
async fn scalar(fail: bool) -> i64 {
let mut count = 7;
if true {
defer match recover() {
Some(info) => println("scalar: " + info.message),
None => println("scalar: clean")
}
await sleep(1);
if fail {
panic("no count");
}
count = 100;
}
return count;
}
あとはRust風のResult, Optionとか、nullがなくてかわりにOption::Noneを使わないといけないとか、色々あります。
その他のサンプルをみてみてください。
ランタイム
Willowのランタイムは、Tokioやasync-stdなどのホスト言語側の非同期ランタイムの上に構築しているわけではありません。
Willow自身が、
- タスクスケジューラ
- チャネル
- タスクキャンセル
- select
- GC
などを持っています。
GCは世代別GCです。
Minor GCは現在Stop-the-Worldですが、Major GCのmarkingは並行実行できるようになっています。
また、async関数のフレームはGC管理されており、そのレイアウトはコンパイラがLIR上で行うliveness解析の結果から決定します。
パフォーマンス

処理はそんなに速くもないが、ちょっとだけ使用メモリが少なくて済む

現時点のWillowの性能は、処理内容によってかなり差があります。ですが、現時点ではほとんどすべての分野でGo/Javaに負けています。
- Fibonacci (再帰的な関数呼び出しで40番目のフィボナッチ数の取得)
このような単純なCPU処理では、WillowはすでにGoと競争できる程度の性能が出るケースがあります。 - Object churn
一方、大量のオブジェクト生成を行う処理では、Goの方が数十倍速いケースがあります。
これは現在のWillowのallocationとGC周辺に、まだ大きな改善余地があることを示しています。 - Virtual dispatch
仮想メソッド呼び出しもまだかなり遅いです。
現在のWillowでは、1回の動的ディスパッチに対するコストが大きすぎます。 - Channels
チャネルやselectなどの抽象自体は動作していますが、その下にあるタスクスケジューリングやチャネル処理の経路は、Goなどの成熟したランタイムと比較するとまだ高コストです。
Willowはまだ実験段階です。
標準ライブラリも小さく、GoやJavaより大幅に遅いランタイム処理も存在します。ツール類も未完成で、言語仕様もABIもまだ安定しているとは言えません。
今後やりたいこと
次に特に試してみたいのが、コーディングエージェント向けの機能です。ただ、言語の構文そのものをAI向けにするというよりは「コンパイラが持っている意味情報を、AIや関連開発ツールが読みやすくする」方向がいいのかなと…。結局AIの書いたコードのレビューは人がやるから、人にとってわかりづらくなっては意味が薄いですからね。