- Lone Lispは、
libcなしでLinuxのシステムコール上で直接動作するLispで、Matheus Moreiraがfreestanding Cだけで完全な言語とユーザー空間プログラムを作れるか確かめるために始めた - 安定したLinuxユーザー空間ABIを直接利用し、
errno、ロケール、暗黙のファイルバッファリングといったCライブラリのグローバル状態やレガシーAPIを取り除き、ランタイム・メモリアロケータ・テストツールを自作した - FEXPRと適切な末尾呼び出し最適化に加え、ジェネレータ、区切られた継続、再開可能なエラー処理をサポートし、ベクタ・テーブル・継続体も同じ関数呼び出し方式で扱う
- 起動は速いが、暫定ベンチマークではPythonより10〜100倍遅いリストレベルのインタプリタであり、Cインタプリタをブートストラップ基準として残しつつ、長期的にはLone自身にJITコンパイラを実装する計画
- MoreiraはClaudeをコードレビューとプロジェクト管理に活用する一方、Loneのコードは自ら書いてレビューし、静的サイトジェネレータ・シェル・ユーティリティを作って従来のLinuxユーザー空間を再構築しようとしている
ゲームからC++・Ruby・Lispへ
- プログラミングに興味を持ったきっかけはMega Man Battle Networkで、関連フォーラムで交流するために英語を覚え、最初の言語であるC++も学んだ
- ブラジルの技術高校で、13〜14歳ごろに情報学コースを始めた
- Dev-C++とcplusplus.comのチュートリアルを使った
- 6か月で学校のカリキュラムをすべて身につけた後、他の学生を助けるティーチングアシスタントとして採用された
- ゲーム制作にも挑戦したが、必要な物理の知識が足りず成功しなかった
- C++の後、Java、Ruby、Pythonを経てLispとSchemeに触れ、Schemeの言語としての優雅さに惹かれた
- SunのJavaチュートリアルをほぼ全部読み、オブジェクト指向プログラミングを学んだ
- Java SwingとNimbusデザインが好きで、数学の教授向けに円と弧を描くアプリも作った
- Rubyでは、Git風のコマンドベース引数パーサーAcclaimを含むgemを書いた
- さまざまな言語を探る原動力は学校のプロジェクトから始まったが、その後は好奇心と、自分に合う正しい言語を見つけたいという欲求が中心になった
Rubyの表現力とCの透明性
- Rubyはメソッド名を推測しても当たることが多く、単数・複数形や同義語もサポートしていて、自分の思考に合っていた
items.each.with_index.mapのように、シェルのパイプラインに近い組み合わせで、少ないコードで多くの処理を行える- 今でもRubyは常にインストールしており、最近はRailsプロジェクトも始めた
- Cは関数がELFシンボルとアセンブリコードに直接対応していて、コンパイル結果を理解しやすかった
- 現代のC++は昔とは実質的に別の言語になったと考えており、もう自分がC++を知っているとは言わない
- Rustへの移行も試みたが定着せず、速度とABIを理由にCで書き直すZigとその作者にも関心を持っている
- 十代のころにCRuby VMのソースを読み、Stack Overflowの「なぜRubyはこう動くのか」という問いに実装から答えを探した
- その後、CRuby、CPython、JVM、Guile、V8のコードを調べ、特にLinuxカーネルソースをプロ向けCプログラミングの基準として評価している
他人のコードを読むやり方
- ライブラリを使うだけでなく、内部実装、ドライバとハードウェアの通信、隠れた層を理解するために継続的にソースを読んでいる
- 実際のアプリケーションを作っていても、基盤技術の探究に逸れてしまうことが多く、すでに解かれた問題をもっと良く作り直したい傾向も生まれた
- こうした傾向は一般的なソフトウェア開発のキャリアに合わないと判断し、プログラミングは職業にせず趣味として維持している
- 最初からドライバコードを理解できたわけではなく、挫折しながらも読み続けたことで、動作の一部を把握できるようになった
RGBキーボードのリバースエンジニアリング
- ClevoベアボーンノートPCのRGBキーボード制御用Windowsアプリが、ウィンドウを開くのに1分以上かかるほど遅かったため、自分で置き換えた
- Wiresharkでアプリがキーボードに送るUSBパケットをキャプチャし、その構造を分析してLinuxユーザー空間Cドライバを書いた
- GitHubに公開した後は忘れていたが、実際に使うユーザーが現れ、あるユーザーはGUIまで作った
- その後、ACPI電源管理などノートPCの他の機能もリバースエンジニアリングしたが、すべての機能を解明できたわけではない
libcを迂回したliblinux
- Linuxのシステムコールは通常の関数呼び出しに近いほどコードが単純で、
libcを通さずにLinuxを直接対象にできる - Linuxはユーザー空間ABIの安定性で知られる一方、ユーザー空間コンポーネントはそうではないと考えている
- マニュアルで、一部のシステムコールをglibcがサポートしていないという記述を見つけた
getrandomがglibcに入るまで約2年かかったというLWNの記事も影響した
- システムコールだけを収めた
liblinux.aを作り、-llinuxでリンクしようという発想からliblinuxを開発した- これはfreestanding C向けのLinuxシステムコールラッパー集だった
- Cランタイム(CRT)はglibc、musl、uclibcのような標準Cライブラリを指す
Freestanding Cで消える問題
- Linuxはエラーを通常の戻り値として返すが、Cライブラリはそれをグローバル変数
errnoに保存する libcを取り除けば、複数のグローバル状態や古いAPIも一緒に避けられるerrno、rand/srand、ロケール状態- 再入可能でない
strtok - 思わぬ問題を起こし得る暗黙のファイルバッファリング
gets、atexit、signal、malloc- NaNや
errnoと絡んで複雑化するmath.h
- 一方で、整数昇格規則やオーバーフロー検査の欠如はC自体の問題なので、そのまま残る
- オーバーフローにはコンパイラ組み込み機能を使う
- 自分が新たな欠陥を作ってしまう可能性にも警戒している
- 標準ライブラリなしでは、バイトコピーから整数→文字列変換まで自分で書く必要があったが、思ったほど不可能な作業ではなかった
- glibcのリポジトリは読み解きにくかったが、
musl libcのソースは整っていて探索しやすく、大いに助けになった - Cライブラリも動的メモリアロケーションも使わず、静的メモリだけを使う自動テストスイートもLoneで実装した
- glibcのリポジトリは読み解きにくかったが、
nolibcの発見とLone Lispの始まり
- Greg Kroah-HartmanにLinuxに公式Cライブラリがない理由を尋ねたところ、過去にklibcプロジェクトがあったと教えられた
- その後、Linuxカーネル開発者が内部ツール向けにnolibc.hを作っていたことを知った
- 自分のliblinuxより完成度も使い勝手も上だと判断し、プロジェクトはアーカイブした
- 現在の
nolibc.hはカーネルツリー内で一時的なlibcに近いディレクトリへと大きくなっているが、外部向けの安定性保証は明確ではない
- 次のプロジェクトとしてLone Lispを始めた
- Linux上で直接動くfreestanding Lispが作れるなら、どんなプログラムも同じやり方で書けると考えた
- 複雑な現代のLinuxユーザー空間を取り払い、安定したシステムコールのバイナリインターフェースの上でやり直そうという実験である
- Cosmopolitanにも浮動小数点パース実装などのためのパッチを送った
- 複数プラットフォームの実行ファイル形式として同時に解釈されるAPE形式や、実行ファイル内の埋め込みアーカイブ機能を高く評価している
エラー処理と区切られた継続
- 初期のLoneでは、すべてのエラー経路がインタプリタで
exit(255)していたが、Common Lisp系の再開可能な条件システムを実装した - エラー処理の基盤は区切られた継続である
- 継続を、スタックフレームのコピーと別の場所での再生として理解している
- 例外も、値をキャプチャしない継続の一形態と見なしている
- エラー種別を
'not-foundのようなシンボルからlambdaへマッピングする- 引数1つの
(lambda (error) ...)は一般的な例外ハンドラのように動く - 引数2つの
(lambda (error continuation) ...)は継続を呼び出し、エラーになったコードを値付きで再開できる
- 引数1つの
- Common Lispの
use-valueのように、シグナルを出すコード側が再開選択肢を列挙するrestart APIは意図的に採用していないsignal、throw、transferが単に値を返したかのように振る舞う、評価器内部の基本機構として構成している
FEXPRと呼び出し可能な言語要素
- 従来のLisp評価器が
ifのような特殊形式を直接処理する方式も避けている - Loneは評価器プラグインに相当するFEXPRをサポートし、特殊形式を評価器の外へ切り出す
- この設計により純粋なLisp関数の末尾呼び出し最適化は先に可能になったが、primitiveにはしばらくバグがあった
- primitiveが末尾位置で返せるよう、別の仕組みを追加して解決した
- ベクタやテーブルも呼び出し可能である
([1 2 3] 0)は1を返す([1 2 3] 0 4)はベクタを[4 2 3]に変更する
- 継続やジェネレータも同じ呼び出し規約に従う
controlは継続をサポートするbeginに近いtransferは最も近いcontrolブロックを抜け、その中断地点を表す呼び出し可能な継続を返す- その継続を値付きで呼び出すと、中断された計算を別の戻り値で再開できる
システムコールと実行構造
- Lone起動前に親プロセスが
execveを呼び出し、Loneは内部メモリ管理のために約12回mmapを実行する - 標準入力からスクリプトを読んで実行した後に終了し、実行中には
mmap、munmap、mremapが追加で発生することがある - モジュールシステムは
openatとcloseを使う - Linuxの300超のシステムコールのうち、自前で使っているのは一部だけだが、Lispレベルにsystem-call primitiveを提供しているため、プログラムから残りも呼び出せる
ioctlのようにカーネルC構造体が必要な呼び出しのため、構造体の生成・操作機能を追加する必要がある- 端末サイズの取得もそうした支援が必要な例である
Loneで作ろうとしているユーザー空間
- 言語レベルでは、エラー処理、適切な末尾呼び出し最適化、ジェネレータ、区切られた継続を実装しており、長期的にはErlang/BEAMのアイデアも取り入れたいと考えている
- 複数のインタプリタがLispコードを並列実行できるよう、構造を準備している
- Loneで作る最初のプログラムは、現在使っているPugJSフォークベースの静的サイトジェネレータの置き換えである
- その後は独自のシェルやユーティリティを書き、従来のLinuxユーザー空間を自分のやり方で作り直す計画だ
envをLone Lispのテーブルとして公開する実装も作っており、当時は走査機能がなかったが、現在はジェネレータをサポートしている- インタプリタが特殊なELFセグメント内のLispコードを読んで実行する機能もある
- インタプリタのバイナリをコピーしてLispコードを挿入すれば、独立した実行プログラムを作れる
Claudeによるレビューとプロジェクト管理
- 総作業量に上限はないが、言語自体の機能セットは完成に近づいていると見ている
- Claudeを一種のマネージャとして使っており、コードレビューとプロジェクト管理で最も助けられている
- コードベース全体を対象に、問題点、未開発領域、今後の方向性を厳しく洗い出すよう依頼している
- 却下したアプローチや後回しにする機能は、文書とプロジェクトメモリで管理している
- ガーベジコレクタとメモリアロケータは、Claudeがより良いアルゴリズムを示したことで大きく改善した
- Loneに入るすべての行を自分でレビューしており、コードを書くこと自体も学習になると考えているため、AIにLoneのコードを代わりに書かせることは信頼していない
- 一方で、JavaScriptの静的サイトジェネレータではClaudeに機能全体を書かせたことがある
- 予定されている機能には、GCC/ClangコンパイラAPIとスタック保護関数のサポートがある
RubyとSchemeから持ち込んだ構文
- Rubyの表現力とSchemeの簡潔さを組み合わせようとし、Lispのパーサは単純なので自分で書いたうえで、ネストしたリストをそのまま扱えるようにした
- Lispを本当に理解したきっかけは、関数が評価済みの値ではなくリストそのものを受け取るFEXPRを実装したときだった
- 配列は
[1 2 3]、辞書は{k v}構文を使う- ClojureやnewLispだけでなく、Ruby、Python、JavaScriptのユーザーにもなじみがあり、不必要な参入障壁を下げられる
- CのコーディングスタイルはLinuxカーネルスタイルを調整して使い、より整列や装飾を好んでいる
- Rubyでは
method('arg')よりmethod 'arg'のように句読点を減らし、英語の文に近く書く方式を好む - Lispでも構文の入れ子を減らしている
- Loneの
letは(let (a b c d ...) body...)の形である - 従来の
(let ((a b) (c d) ...) body...)より、名前と値が順番に並ぶ形のほうが関数適用の方式として自然だと判断した
- Loneの
性能とブートストラップ計画
- Loneが優位なベンチマークは起動速度で、RubyがRubygemsを読み込む時間より速くプログラムを起動して終了できる
- 暫定測定ではPythonより約10〜100倍遅い
- LoneはSICP 5.4章に近い形のリストレベル・インタプリタである
- PythonやRubyはバイトコードインタプリタなので、性能の分類が異なる
- リストをバイトコードに変えるとLispの本質を失うと感じており、バイトコードコンパイルは避けている
- 長期的には、Cインタプリタは最終的なブートストラップ基準として残しつつ、Lone内部でJITコンパイラを書く計画である
- 実行可能なページを
mmapで確保し、その上に命令を生成する方式だ
- 実行可能なページを
- Bashとの再帰的フィボナッチの暫定比較ではLoneのほうがはるかに速かったが、Bashが反復ごとにプロセスを生成していた可能性など、測定の歪みは認めている
- スクリプトでは起動時間が重要なので、Loneの高速なブートストラップは有用だと考えている
- ハッシュテーブルをビルド段階で事前計算してCの初期化データにすれば、実行時の初期化コストをさらに下げられる
メモリと最小環境
mmapはカーネルにメモリページを要求し、一般的なページサイズは4,096バイトである- 読み取り・書き込み・実行権限を指定でき、JITは実行可能ページを使う
- 16バイトしか必要なくても4KiBを要求すれば無駄が大きいため、一般的なアロケータは128MBのような大きな領域を受け取り、それを小片に分割する
- Loneのトップレベル環境には
importとexportの2つのシンボルしかない- リスト、リテラル、関数呼び出し機構以外は、
ifやquoteまですべてインポートしなければならない - ユーザーが環境内のすべての名前を自分で直接制御できるようにした設計である
- リスト、リテラル、関数呼び出し機構以外は、
- 現在の関数は約50個で、
if、let、begin、control/transfer、intercept/signal、flattenなどが中心である
標準ライブラリと文書化
- 言語ドキュメントはまだ不完全だが、言語機能より標準ライブラリの作業のほうが完成に近づいている
- Rubyの標準ライブラリとSchemeの命名方式を参考にする予定である
- 次の作業は反復プロトコルである
(for (each collection) (lambda (item) ...))の形を検討している- 既存の
each関数はすべてジェネレータを返すように再設計する予定である
- その後は文字列操作と、効率的な連結のための文字列ビルダを実装したいと考えている
- 反復と文字列機能が完成すれば、Hiccup風のWebフレームワークも書けると見ている
- 低レベル機能が終わり、反復的な標準ライブラリ作業へ移ると集中力が落ちる可能性を心配しており、Claudeの管理支援を活用するつもりだ
スタック全体を自分で所有するという目標
- 継続のように神秘的に見える機能も、スタックフレームのコピーとして理解すれば普通の実装問題になり、Loneには他の人にも自分のシステムを作り、探究してほしいという目的がある
- ライブラリの利用者にとどまらず、ライブラリを作る側になろうとしており、一般開発者には禁じられているかのように扱われるシステムコールも自分で使おうとしている
- 自分のシステムを持つなら、壊れたときは自分で直さなければならないが、より多くの人がその責任と報酬を選んでほしいと願っている
- コンピューティングに蓄積された古いコードを恐れず、不要な層は取り除けると考えている
- Linuxユーザー空間を作り直す主体は自分ではなく、RustやZigの開発者かもしれない
- Loneが成功しなくても、こうした試みを後押しできるなら満足できる
自由ソフトウェアとAndroid
- 自由ソフトウェアが目指すユーザー権限の強化を重視しており、汎用コンピュータが企業や政府に管理される家電のような存在へ縮小していく状況を懸念している
- systemdは重厚なアプローチを取った面があっても、従来のLinuxユーザー空間を再想像し再構築した成功例だと評価している
- すべての層を自分で作るのではなく、戦う場所を選ぶべきだと考えている
- LoneをベアメタルLispとして作っていたら、ブート段階にすら到達できなかったかもしれない
- 512バイト未満のブートセクタLispであるjartのSector Lispを例に挙げている
- LoneはTermuxの中で始まり、かなりの部分がスマートフォン上で書かれた
- Androidは
statxを含む一部システムコールをブロックするが、LoneはAndroid上で動作し、テストスイートも100%通過している
医師でありプロではないプログラマ
- Moreiraは2019年に医学部を卒業した医師である
- 高校時代にニューラルネットワークのプログラミングを読んだことから生物学に興味を持った
- ある日は10〜14時間診療し、別の日は休むという日程でプログラミング時間を確保している
- 電子カルテを使う病院では、テンプレートとタイピング能力で業務時間を大きく短縮している
- 患者と目を合わせたまま記録できる
- 最近始めたRailsプロジェクトは、自分のニーズを解決するための電子患者記録システムである
- プロの開発者が個人的な問題を自分で解決しない理由の一つとして、コンピュータそのものへの関心の薄さを挙げている
- 結果だけを得ようとする姿勢ではなく、好奇心があるからこそ実装の内部に踏み込めるのであり、その好奇心をどう人々に呼び起こすかはまだ分からない
1件のコメント
Lobste.rs のコメント
このインタビューシリーズは欠かさず読むほど好きで、それぞれがコンピューティングと築いてきた個人的な関係に触れられるのがとても面白い
GBE+ GBA エミュレーターの作者 Shonumi も私と同じく Mega Man Battle Network のゲームに影響を受けており、いつか彼のインタビューも読んでみたい
再開可能な例外に第一級継続(first-class continuation)が必ず必要というわけではない
Common Lisp は継続がなくても再開可能な例外を提供しており、要点はハンドラーを呼び出す前にスタックを巻き戻さないことにある
そのため、例外が発生したスタックと状態がそのまま維持され、ハンドラーが正常に返ることも、Common Lisp のようにスタック上のどこかにある再開ハンドラーへジャンプすることもできる
ただし Lone Lisp では、返された継続を複数回呼び出せ、シグナルハンドラーの外へ脱出できるため、実際に必要になる ジェネレーターは反復処理の基盤なので性能がより重要であり、限定継続の代わりに別スタックを使ってコピーをなくした
エラー処理は相対的にまれな経路なので、機能を完全に提供することにした
多くの人にとって、最も重要な作業やプロジェクトは、余暇に無報酬で楽しみとしてやることなのかもしれない
私の医師も、人知れず自分だけの Lisp 言語を作る二重生活を送っているかもしれない、と想像してしまう
Dr. Cameron Kaiser も大きなプロジェクトを進めており、機械の所有権に関する哲学まで共有しながら、完全オープンな POWER9 ワークステーションを日常的に使っている
Lone Lisp は今回初めて知ったが、素晴らしいプロジェクトに見える
私も13〜14歳ごろ、情報の授業で古い Dev-C++ を使って C++ を学び始めたので、本当に久しぶりに思い出して郷愁を誘われた
医師だとはまったく予想しておらず、Con Kolivas を思い出した
こういう医師兼開発者は、意外とかなり多いようだ
https://en.wikipedia.org/wiki/Con_Kolivas