今まで、自作言語の話は、C言語のコンパイラや、インタプリタの実装が多かった。
一方、2026年現在、LLMの発達のおかげで、自由な文法の言語も自作するハードルが非常に低くなっていきている。
そこで、実用的な言語を目指して自作プログラミング言語を開発することにした。
このプロジェクトは arukellt と名付けていて、GitHub で公開している。
このプロジェクトで特にこだわっているのは、「Wasm-first、LLM-friendly を目指す静的型付け言語」という点だ。
Wasmとは?
まず、前提として Wasm を知らない人向けに前提だけ整理する。
WebAssembly は stack-based virtual machine 向けのバイナリ命令形式で、プログラミング言語の portable なコンパイル先として設計されている。
ブラウザだけの技術ではなく、sandboxed な実行環境を持ち、非 Web 環境でも使える。
MDN も、Wasm を低レベルでコンパクトなバイナリ形式を持つ実行コードとして説明していて、C/C++、C#、Rust などにとってのコンパイル先になると整理している。
WASI はその Wasm 向けの標準 API 群で、ブラウザ、クラウド、組み込みまで含む実行先で安全な共通インターフェースを与える。
よって以下のような関係になる。
- ランタイム: Wasmの実行、ホストとwasmの橋渡しを担う
- WASM: portable なバイナリ命令形式、言語のコンパイル先
- WASI: Wasm向けの標準API、ホストとWasmの共通インターフェース
なぜWasmをターゲットにしたのか
Wasmをターゲットとしてコンパイルできる言語はもう珍しくない。
例えば、C, C++, Rust, Go, Python, Java, .NET などはすでに Wasm にコンパイルできる。
では、なぜWasmをターゲットとした新しく言語を作るのか。
それは、Wasmの機能を最大限生かすことで、バイナリサイズを小さく、実行速度を速く、そしてホストとのインターフェースを自然にすることができるからだ。
だから新しく言語を作るなら、「最終的に Wasm にも落とせます」では弱い。
Wasm の制約や強みを、最初から言語設計の中心に置く必要がある。
arukellt がやっているのはそこだ。
compile の主経路は Lexer → Parser → Resolver → TypeChecker → MIR → Wasm、check では CoreHIR を挟む経路もあり、wasm32-wasi-p1 は互換パス、wasm32-wasi-p2 は canonical な GC-native パスとして分けている。
さらに --emit component と --emit wit まで wasm32-wasi-p2 側に載せている。
これは「Wasm にコンパイルする」のではなく、「Wasm を主戦場にした言語」を作ろうとしている設計だ。
(GitHub)
Wasm-first が効くのは、単に配布しやすいからではない。
WebAssembly の GC extension は、高級言語を効率よく支えること、より速い実行やより小さいモジュール、既存 VM の強い GC を利用することを動機にしている。
しかも方針として、GC データは linear memory とは独立した低レベル表現に置く。
arukellt の wasm32-wasi-p2 はこの方向をかなり正面から採っていて、2026-03-27 時点で T3 emitter は fully GC-native、値表現は struct.new、array.new、ref.cast、br_on_cast のような Wasm GC 命令を使い、linear memory は WASI I/O の marshal 用に 64KB だけ残すとしている。
String は GC byte array、Vec<T> は GC struct、Option / Result は subtype hierarchy と br_on_cast で表現する。
ここまで行くと、Wasm は単なる最終出力ではなく、ランタイムモデルそのものだ。
(GitHub)
実用するためのツール
ツール面も、 run だけではない。
今の arukellt CLI には compile, run, check, build, fmt, test, lint, targets, analyze, init, script, doc, lsp, debug-adapter, compose などを実装している。
rust toolchain でいうところの cargo に近い形を目指していて、単に「言語で遊ぶ」だけでなく、実際に小さなプロジェクトを作って動かすところまで見据えている。
(GitHub)
コード例
現在動かせるコードも、単純な Hello World から、ファイル入出力、HTTP クライアント、さらには Markdown parser のような少し大きめのコードまである。
最小の Hello World はこうなっている。
use std::host::stdio
fn main() {
stdio::print("Hello, world!")
}
これを arukellt run hello.ark で動かす。
host I/O を std::host::stdio から明示 import している時点で、実行環境との境界を曖昧にしていない。
これは、WasmはWASIのようなホストAPIと組み合わせて使うことが前提の技術であるため、言語設計の段階からホストとのインターフェースを明確にすることが重要だと考えているからだ。
(GitHub)
Vec と Option の扱いも実用寄りだ。
get(v, i) は Option<T> を返し、必ず要素があるときだけ get_unchecked を使うようにガイドしている。
use std::host::stdio
fn main() {
let v: Vec<i32> = Vec_new_i32()
push(v, 10)
push(v, 20)
let first: Option<i32> = get(v, 0)
match first {
Some(x) => stdio::println(to_string(x)),
None => stdio::println(String_from("empty")),
}
stdio::println(to_string(len(v)))
}
注記: v[0] のようなインデックスアクセスは、失敗したときに panic する形で実装する予定だが、panicの扱いをまだ定めていないため、まだ実装していない。
Result と ? もすでに入っている。
これで失敗を値として流せるので、host API と相性がいい。
use std::host::stdio
fn parse_positive(s: String) -> Result<i32, String> {
let n = parse_i32(s)?
if n < 0 {
return Err(String_from("negative value"))
}
Ok(n)
}
fn main() {
match parse_positive(String_from("42")) {
Ok(n) => stdio::println(to_string(n)),
Err(e) => stdio::println(e),
}
}
Wasm 上のプログラムは host 境界が多い。
だから Result が最初から自然に書けることはかなり重要だ。
(GitHub)
高階関数も動く。
docs/examples/closure.ark には fn(i32) -> i32 を受け取る関数とラムダの例がある。
use std::host::stdio
fn apply(f: fn(i32) -> i32, x: i32) -> i32 {
f(x)
}
fn main() {
let double = |x: i32| -> i32 { x * 2 }
let result = apply(double, 21)
stdio::println(i32_to_string(result))
}
current-state では closure の表現を「parameter-passing captures; call_indirect for HOF dispatch」と明示している。
つまり高階関数が表面 syntax だけでなく backend の実装方針まで固まっている。
(GitHub)
host I/O もすでに具体化されている。
ファイル入出力は std::host::fs から使う。
use std::host::fs
use std::host::stdio
fn main() {
let r = fs::read_to_string("input.txt")
match r {
Ok(content) => stdio::print(content),
Err(e) => stdio::println(e),
}
}
fs::read_to_string(path: String) -> Result<String, String> と fs::write_string(path: String, content: String) -> Result<(), String> まで quickstart に出ている。
(GitHub)
docs/sample/parser.arkというサンプルは 1,458 行、51.7 KB ある。
中身も単なる文法デモではなく、front matter の抽出、警告構造体、位置計算、イベント列の構築まで含む Markdown parser 移植だ。
たとえば front matter 抽出は、改行コード差分まで見て Option<FmResult> を返している。
fn extract_front_matter(text: String) -> Option<FmResult> {
if !starts_with(text, "---\n") {
if !starts_with(text, "---\r\n") {
return None
}
}
let after_open = unwrap(str_find(text, "\n")) + 1
let rest = substring(text, after_open, len(text))
let close_pos = str_find(rest, "\n---\n")
let close = match close_pos {
Some(p) => Some(FmBounds { fm_end: p, content_start: p + 5 }),
None => {
let close2 = str_find(rest, "\n---\r\n")
match close2 {
Some(p) => Some(FmBounds { fm_end: p, content_start: p + 6 }),
None => None,
}
},
}
}
警告や位置情報のような、実用では避けられない構造も普通に書かれている。
Warning は code, message, source, line, col を持ち、ParseCtx は入力文字列と line_starts を持って offset を行列位置に戻す。
こういうコードが自然に書けるなら、少なくとも「式計算しかできない小言語」ではない。
(GitHub)
未完成な部分
native backend は scaffold、wasm32-freestanding と wasm32-wasi-p3 は未着手、std::host::http は T1/T3 両対応まで来たが HTTP/1.1 の TCP 実装で、HTTPS はまだ未対応だ。
self-hosting も Stage 1 までは到達していて、arukellt-s1.wasm が自身の 9/9 ファイルをコンパイルできる一方、Stage 2 の fixpoint は未達で、原因は multi-file module loading 未実装と cross-module call が stub に落ちる点だと current-state に明記されている。
(GitHub)
結論
arukellt の価値は「Wasm を主戦場にしたとき、言語と runtime と toolchain をどこまで一体で設計できるか」を実装で見せているところにある。
そして、実は Wasm が単なる配布形式ではなく、host API、component、GC、sandbox まで含むリッチな実行基盤だということだ。
そして arukellt は、その基盤に合わせて最初から言語を組み立てようとしている。
今の時点でも、これは十分に「具体的に動くプログラミング言語」の形になっている。
(wasi.dev)