Mere: 言語を作る
リージョン/ビュー型のメモリモデル、能力受け渡し方式のエフェクト、ブラウザ上でのセルフホスト、4 つのバックエンド(インタプリタ / C / LLVM / Wasm)へのコード生成、型安全な並行処理 —— 小さな ML 系言語 Mere の設計日記。完成品の紹介ではなく、意思決定の記録:何を、どう作り、どんな課題が出て、どう解いたか。
この連載について
Mere は小さな ML 系プログラミング言語です —— Hindley–Milner 型推論、代数的 データ型、パターンマッチ。それを積み上げて、最終的には自分自身をブラウザ上で 型検査・評価・コンパイルでき、4 つのバックエンド(ツリーウォーキング インタプリタ、 C、LLVM IR、WebAssembly)へコードを生成し、そのすべてで型安全な並行処理を走らせる ところまで作りました。
この連載は完成した言語のツアーではありません。意思決定の記録です —— 各要素が 何であり、どう作り、どんな課題が出て、どう解決したか。選択が本当の分岐だったときは、 取らなかった選択肢と、選んだ理由も物語の一部にします —— 「これはまだ作らない」 という答えだったときも含めて。
Mere の背後にある考え
ほぼすべての決定を貫く一つの信念があります:
正しさの検証可能性を最適化し、暗黙を明示にする。
具体的には:
- 暗黙は意図的に取り除く。 実行時の表現や挙動がひそかに変わるものは表に出す —— メモリの所有権、エフェクト、そして(後には)どの値がスレッド境界を越えられるか、 これらすべてが型に見える形になります。
- 妥協は避けられない。だから「どこで」を選ぶ。 どの設計ページも、妥協が無いふりを せず、自分が行っているトレードオフを名指しします。
- 記述量は許容する。 コードは AI の支援で書くので、「打鍵量が多い」ことはかつての ほどのコストではありません。おかげで、人間第一の言語なら簡潔さのために隠すところを、 明示的に設計できます。
これは賢さや速さの話ではありません。プログラムを見て、それが何をするか分かるため のものです。
各要素をどう決めたか
プロジェクト全体を貫き、ほぼ毎回登場するパターンがあります:
- 紙上 trial —— コミットする前に、設計を紙の上で(そしてしばしば使い捨ての プロトタイプで)考え抜く。
- 未決事項を構造化する —— 決めきれない論点は
Q-###として立て、意図的に open のままにする。実装が答えを迫った時点で決める。 - dogfooding —— 実際のプログラムをツールチェインに通し、設計文書では見えない 問題を炙り出す。
全体のアーク
- 土台 —— 思想、母言語に OCaml を選ぶこと、動く最小 ML コア。
- メモリ —— 5 つの戦略を、
Trivial/Drop/withを伴う統一された リージョン/ビューモデルへ蒸留する。 - エフェクト —— 隠れた環境状態ではなく、能力(capability)として受け渡す副作用と、 細分化された借用注釈。
- コード生成 —— 一つの言語、4 つのバックエンドを歩調をそろえて保つ。
- セルフホスト —— Mere 自身の字句解析器・構文解析器・整形器・評価器・型検査器・ コード生成器を Mere で書き、ブラウザで動かす。
- 並行処理 ——
spawn/ チャネル /par_mapをSend/Syncの型安全とともに、 すべてのバックエンドで。 - これから —— そして同じくらい大事な、意図的に作らなかったもの。
どこから読んでも構いませんが、各パートはおおむねこの順で積み上がっています。
公開済みの記事
- Mere は何を最適化するか
- 母言語を選ぶ
- 最小の ML コア
- メモリ:5 つの戦略、1 つの選択
- 二つの概念が一つだったとき:region と arena
- Trivial、そして region が持てない値
- view 型:region が易しくする難問
- region の中のコレクションと、Trivial 階層
- 副作用を値として渡す
- 高階関数、エフェクトを漏らさずに
- エフェクトの粒度:shared write と Logger 問題
- ケイパビリティを束ねる、そしてそれはどこから来るのか
- なぜ一つ以上の backend か、そしてそれらを同一に保つとは
- インタプリタと C backend:クロージャを手で作る
- LLVM backend:コンパイラのための IR へ下ろす
- スケールで完全一致を保つ
- WebAssembly backend:サンドボックスへコンパイルする
- FFI:呼び出すことと、呼び込まれること
- docs site を Mere で書く:アプリ規模の dogfood
- セルフホスト始動:レキサ、パーサ、そして共有された木
- フォーマッタ、そしてセルフホストがセルフホストをパースする
- メタ循環評価器:Mere が Mere を走らせる
- 型検査器:Hindley–Milner を Mere で書く
- コード生成器を Mere で:自分自身を吐くコンパイラ
- ブラウザで動く Mere:すべてが収束する場所
- 名前「Mere」、そしてそれが何であるか
- 公開:使えるものになるという、地味な仕事
- 残りを裏切らずに並行処理を設計する
- Send と Sync:安全性が乗る述語、そしてその一つの穴
- move 追跡:手渡し後の使用を捕まえる
- 多相チャネルと Send 束縛
- 四つの backend で並行を動かす
- par_map:よくある場合を人間的にする
- 何かを作らないという決定
- 次に作るもの:地図の縁
- ほとんどすでにあったパッケージシステム
- 違う痛み方をするよう選ばれた二つ目の dogfood
- 同じソースを、ホストなしでコンパイルする
- trait が与えるものを、trait なしで
- backend を正当化する痛みを探して
- ドキュメントもまた dogfood だった
- 新しい痛みを見つけるにはドメインを変える
- 自分自身を一度も走らせたことのないコンパイラ
- コンパイラにジャンプを教える
- 鏡の中にだけ現れる三つのバグを狩る
- バイトごとに
- 順序のもう半分
- どこで痛むかを選ぶ
- 他のプログラムを走らせる
- 端末に住む
- 自分の出力を読む
- バグはブラウザには無かった
- 修正が利用者を壊すとき
- ワイヤの向こう側になる
- 成長は回収ではない
- 言語を測る 1 行ループ
- interp では動くのに
- 型システムが許したレース
- 返ってくるメモリ
- 単相化こそが辞書
- 一万手の探偵
- wasm も返す
- 既にあった
- 日本語を打ってみる
- 百万要素
- 一つの int、四つの機械
- 本物の ALU
- バイトが入口を通る
- docs を直した絵
- 桁であって符号点ではない
- 十六進が読めるようになる
- 止まれない worker
- 諦める supervisor
- select は要らなかった
- 古典四重奏
- 解凍器を書く
- 影の中の 3 バグ
- bytes ではなかった
- 圧縮器を書く
- 本物の形式
- gzip に並ぶ
- records で領域を書く
- Mere で Scheme を
- 取られていた名前
- 六つ目の名前
- リストを消す
- float を走らせたことのなかった backend
- 64 メガバイトの崖
- ページの上のパリティ
- 千四百八十四の注釈