llrlの言語デザイン上の選択
S式を採用する
シンタックスを考えると無限に時間が吸われるため、またマクロに都合が良いため。モジュール間のqualified importに際して module-name/definition-name のような単なる識別子による束縛ができたりと、名前修飾周りの言語仕様の簡略化にも寄与する。 _ をワイルドカードにしたimportもおおよそ機能する (hiding importを実装しなかったことでしばしば不便があった)。
モジュールシステムをサポートする
1ファイル = 1モジュールとして、ファイルとディレクトリからなるプログラムの構造化をサポートする。
セルフホスティングに必須ではないが、扱うプログラムのサイズからすると直列に繋げて処理するだけでは厳しい。より小さいwonderlispの実装でも何かしらのモジュールシステム相当のものが無いとスコープが厳しいということを実感した。
モジュールの循環importを禁止する
意味解析を簡略化するため。この制約によって依存モジュールは全て構築済みであるという前提を設けることができ、import/exportの不動点を見つけたりが必要なくなる。またマクロのJIT Executionにも都合が良い。
モジュール内の定義の相互参照をサポートする
モジュール同士は循環を許さないのに対し、モジュール内のトップレベルの定義同士は常に相互参照できる。
トップダウンにコードを書いていきたく、OCamlやF#を書いてていやーやっぱりここは欲しいなと。
この決定は、後述のマクロについての制限や採用した型システムにも関わってくる大きなトレードオフがある。
マクロを搭載する
遊び心 (1)。現代のプログラミング言語には使い心地を改善する細かい言語機能が多々あるが、自作言語でこのような対応を逐一行っていくのは非常に大変。マクロはこのような細かい言語機能の不足をカバーするのに有効な機能である。
マクロは依存する別モジュールで定義されることを必須とする
TemplateHaskellにもこの制約があったかと思う。マクロの展開はトップレベルの宣言時を含むプログラムの大部分の場所でサポートしたく、一方前述のようにモジュール内では相互参照をサポートするため、この制約を導入しないと意味解析とマクロ展開の順序が複雑になる。
例外機構をサポートしない
catch 可能な、制御フローとして用いることができるような例外を言語機能として含めない。
例外が控えめに言って嫌いであるため。また、スタックのunwindingや例外のRTTIなど実現までに取り組む課題も多い。
ビルトインで定義される型として (Result T E) (Option A) をサポートし、そのための構文糖衣もサポートする。
強い静的型付け言語にする
好みであるため。しかし、そもそも例外を載せないという決定をすると動的型付け言語にするのは困難ではないか?
Hindley-Milner型推論 + 型クラスを搭載する
よく枯れた手頃な型システムのため。とはいえ、モジュール内の定義の相互参照をサポートしていることで、依存性解析を行って順に型変数を振って多相化して、みたいな手間は増える。
型クラスは遊び心 (2)。セルフホスティングに明らかに必要無いしそこそこ意味解析やコード生成の手間が増える。
型システムもシンタックスと同じく考えると無限に時間が吸われるため、マクロがあると考えて素朴なものとしたい。
ただ実際に標準ライブラリを書いてみると...fundepsあるいは関連型が非常に欲しい。
暗黙の型変換を行わない
好みであるため。言語仕様が複雑になるため。
システムプログラミングを意識しない / Safetyを重要視しない
これに目を向けると、おそらく無限に劣化Rustを実装し続けることになる。セルフホスティングに集中したい。
例えばクロージャの変数のキャプチャ + boxingとかを普通にやってしまう。
左辺値・右辺値を持たない
上の割り切りの一環。極力immutableなデータ構造を扱い、mutabilityはOCamlのように ref や array 型を通して扱う。
x64, Linuxをターゲットとする
現在の自分のプログラミング環境のため。互換性について考慮されるべきことは多々あり大変なので、他は忘れてしまう。
代数的データ型をサポートする
丁寧にやるならstruct (record), union, enumと用意していきたいが、言語仕様が膨らんで手間がかかる。Haskellライクな data とそのコンストラクタとパターンマッチ (デコンストラクト) だけサポートする。マクロで扱うS式もこの data で定義され、 syntax-case や quasiquote のような機能はこのデータ構造の操作に変換されるマクロとして定義される。
実際にセルフホスティングしてみるとレコード型相当の機能はやはり欲しかった。正確には、フィールドアクセスに相当する関数の生成はマクロで簡単だが名前の衝突が厳しいので、シンボル型 'foo 、phantomによる構文糖衣 @foo → (phantom 'foo)、式用の構文糖衣 ->foo → get @foo、および型クラス (class (Get K A B)) に K A -> B 相当のfundepsがあれば (Get 'foo A B) のようなインスタンス定義で解決できる。
言語レベルでユニコード文字列をサポートする
現在のプログラミング環境ではもはや必須と言える。またコンパイラの入力のソースコードはUTF-8で記述されたものとする。
パッケージ管理を含めない
現代的な汎用プログラミング言語ではありふれた存在だが、llrlの目的では取り組むべき課題ではない。
コードからのドキュメント生成を行わない
現代的な汎用プログラミング言語ではありふれた存在だが、llrlの目的では取り組むべき課題ではない。
型エイリアスをサポートしない
静的型付け言語としてはかなり不便である。が、型推論の前に展開が終了するのを検査するのが面倒で...
S式を採用したLispの体をした言語なので、型はそんなに書くことがないという体で行きたい (型システムも型を明記しないプログラムとまあまあ相性がいい)。
しかし実際にセルフホスティング作業や標準ライブラリの実装作業を行ってみると、この言語機能はかなり欲しいものだった。実装していない中で一番欲しかった機能かもしれない。
分割コンパイル・ライブラリ生成をサポートしない
コンパイラの出力は常に実行バイナリとする。入力は、標準ライブラリ (std) も含めてまとめて構文解析・意味解析・コード生成を行ってしまう。もちろん理想的には言語の意味解析情報を全て含んだ中間ファイルを設けたいが、セルフホスティングコンパイラは標準ライブラリも含めて一括で十分コンパイルできるサイズである。
参考