yuniのselfboot
もう当分Racket触りたくねェ... (後述)
nmoshの互換ライブラリ層を分離した yuni
は、どんなScheme実装でもR6RS風のライブラリシステムとR7RS-smallのサブセットを提供することを目的としている。で、従来はCMakeを使ったビルドシステムで一旦ビルドしないと使えないという微妙に面倒な仕組みになっていたが、"selfboot"と呼んでいる新しいbootstrapによって リポジトリをチェックアウトするだけで使える 状況を目指している。
もっとも、selfbootはyuniのbootstrap -- 普通のライブラリで言うところの"ビルド" -- と、簡単なテストのために使うことを想定している。特に、 selfbootはFFI互換層を提供しない ため、(yuni自体のビルドを除いた)実用的なアプリケーションをselfbootで実行することはスコープにない。例外的に、BiwaSchemeとs7ではselfbootが今のところ唯一のyuni環境になっている。
selfbootでできること
yuniのリポジトリに簡単なサンプルを置いた:
- https://github.com/okuoku/yuni/tree/6d968de0df1541ad63319be73aa11ccf8e450daf/samples/hellolib -- 簡単なサンプルプログラム
(library (A) (export A stxA) (import (yuni scheme)) (define-syntax stxA (syntax-rules () ((_ sym) (begin (display "stxA: Symbol ") (display 'sym) (display " value is: ") (write sym) (display "\n"))))) (define (test) (display "This is `test` in library (A)\n") 'A) ... )
のようなライブラリを使ったプログラムが、非R6RS処理系や syntax-rules
の無い処理系でも同様のコマンドラインで実行できる:
- (Chibi-scheme - R7RS)
chibi-scheme ../../lib-runtime/selfboot/chibi-scheme/selfboot-entry.scm -LIBPATH . app.sps
- (Gauche - R7RS)
gosh ../../lib-runtime/selfboot/gauche/selfboot-entry.scm -LIBPATH . app.sps
- (Sagittarius - R7RS/R6RS)
sagittarius ../../lib-runtime/selfboot/sagittarius/selfboot-entry.sps -LIBPATH . app.sps
- (ChezScheme - R6RS)
scheme --program ../../lib-runtime/selfboot/chez/selfboot-entry.sps -LIBPATH . app.sps
- (s7 - Generic scheme)
s7yuni ../../lib-runtime/selfboot/s7/selfboot-entry.scm -LIBPATH . app.sps
- (Racket)
racket ../../lib-runtime/selfboot/racket/selfboot-entry.rkt -LIBPATH . app.sps
(s7は単体の処理系が存在しないので、自前の s7yuni
がyuniのリポジトリに収録されている。)
selfbootによって可能になるのは、yuniの基本ライブラリで構成された、R6RS風アプリケーションを適当なSchemeで起動できること。 ...たったコレだけでは有るけど、各処理系で微妙に異なるライブラリ構成や構文を無視して各種処理系で一発動作するのは中々に気持が良い。特に、s7のような素のSchemeからR6RS、R7RSまでをカバーしているローダーは多分他に無いんじゃないだろうか。SLIBとか他のライブラリと一緒になった奴は有るけど、インストール不要ではない。
しくみ
selfbootなyuniは、 load 呼び出し生成器 と要約できる。通常のScheme処理系は load
手続きでファイル名を指定すればライブラリをロードできるが、R6RSプログラムではライブラリ名は (rnrs)
とか (yuni scheme)
のようにシンボルで記述されるため、これを適当にファイルパスに変換してやる必要がある。
よって、
- プログラムを読み取り、依存ライブラリを収集する
- 依存ライブラリシンボルをパスに変換し、依存ライブラリの依存関係を再帰的に収集する
- ライブラリの依存関係をトポロジカルソートし、パス名をロード順に並び換える
- 処理系の
load
に渡し、ライブラリを実際にロードする
yuniではどの処理系でもyuniのライブラリを直接読み取れるように構文要素を注意深く選んでいる。このため、yuniのソースコードは基本的に処理系の read
で直接読み取ることができる。...その後更に load
するので2度読みになってしまうが。。依存関係の抽出等の処理は、各処理系に共通する語彙だけを使って共通のSchemeソースとして記述できる。
ただ、処理系の load
が直接yuniのR6RS形式ライブラリを読み取れるのは 当然R6RS処理系に限られる ため、これができない処理系では load
に相当する手続きを自前で実装している。この 自前で実装した load
こそがselfbootの実装のキモ ということになる。
rationale
Schemeアプリケーションを書いている人にとっては、yuniの "ライブラリ書式はR6RS、語彙はR7RS" という組み合せに違和感が有るかもしれない。これには幾つか理由があって、
- ライブラリシステムは差し替えが難しいため、低機能な方に合わせる方が好ましい。つまり、R7RSの
define-library
よりもR6RSのlibrary
のほうがずっと低機能なので実装しやすい。 define-library
は実装によって挙動がまちまちで使いづらい。例えばinclude
フォームがライブラリパスを尊重するかどうかに標準が無い。このため、define-library
はライブラリ定義と実装の分離を念頭にデザインされているものの、ライブラリ定義とライブラリファイルを同じディレクトリに置かないとうまく働かない。- R6RSの標準ライブラリよりもR7RSの標準ライブラリの方が普及している実装に合っている(既存のSRFIに素直な仕様になっていることが多い)。
selfbootを実装するまでは、R6RS処理系の一部(Chezやnmosh)だけがyuniのアプリケーションを直接起動することができた。これらの処理系では、単にライブラリパスにyuniのライブラリを追加するだけで直接R6RSプログラムとしてyuniのアプリケーションを処理できた。R6RS処理系全部をサポートできないのは、Racketのように #!r6rs
を付けないとライブラリがR6RSとして認識されなかったり、Guileのように補助構文が束縛されていなかったりといった細かい違いによる。
selfbootの実装で、yuniをいちいちビルドしなくてもyuniアプリケーションが起動できるようになるので、もうちょっとyuni自体の開発効率が上がるんじゃないかという気がしている。
各種処理系でのselfboot実装
各Scheme処理系でselfbootを実装する方法は、処理系毎に異なる。...要するに、R6RSとかR7RS標準だけではyuniのselfbootは実装できないため、処理系固有の実装がどうしても必要になってしまう。。
selfbootの実装に必要なのは:
library
に相当する構文をeval
で処理すること- 後続の
eval
で、事前にevalされたライブラリをimport
できること
の2点で、おおざっぱに言えばREPL上で module
とか library
を定義し、使用するのと大して変わらない。
ChezScheme
- https://github.com/okuoku/yuni/blob/6d968de0df1541ad63319be73aa11ccf8e450daf/lib-runtime/selfboot/chez/selfboot-entry.sps#L91
--
interaction-environment
による環境定義 - https://github.com/okuoku/yuni/blob/6d968de0df1541ad63319be73aa11ccf8e450daf/lib-runtime/selfboot/chez/selfboot-entry.sps#L94
--
load
によるファイルのロード
ChezSchemeのselfboot実装はそんなに難しくない。ChezではREPL用に interaction-environment
が提供されるため、単にその環境をコピーして library
でも何でも eval
してしまえば良い。
yuniはR6RS形式のライブラリを採用しているため、Chez上では単純に load
すれば良い。Chezの load
は第3引数としてハンドラを渡すことができ、その中で eval
することになる。
Guile
- https://github.com/okuoku/yuni/blob/6d968de0df1541ad63319be73aa11ccf8e450daf/lib-runtime/selfboot/guile/selfboot-entry.sps#L92
--
interaction-environment
による環境定義 - https://github.com/okuoku/yuni/blob/6d968de0df1541ad63319be73aa11ccf8e450daf/lib-runtime/selfboot/guile/selfboot-entry.sps#L108
--
export
のフィルタ処理
GNU GuileはChez同様 interaction-environment
を備えている。ただ、Guileは標準ライブラリ内部で補助構文を束縛していないため、 export
内で再エクスポートされる場合は取り除く必要がある。このため、 load
のような直接ロードは使用できず、フィルタ処理が必要になる。
このようなソースコードの書き換えが必要な処理系では、構文情報が抜けてしまうため行番号情報等が無くなってしまうというデメリットがある。真面目に syntax-case
で加工すれば多分何とかなるが。。
Sagittarius
- https://github.com/okuoku/yuni/blob/6d968de0df1541ad63319be73aa11ccf8e450daf/lib-runtime/selfboot/sagittarius/selfboot-entry.sps#L95
--
interaction-environment
へのロード - https://github.com/okuoku/yuni/blob/6d968de0df1541ad63319be73aa11ccf8e450daf/lib-runtime/selfboot/sagittarius/selfboot-entry.sps#L106 -- 処理系付属ライブラリの定義
SagittariusでもChez同様に実装できる。
selfbootでは、ライブラリパスの解決は自前の処理で行うことになるが、処理系付属のライブラリは検索する必要がないため事前に除く必要がある。SagittariusはR6RS/R7RS Hybrid処理系なため、R6RSライブラリとR7RSライブラリの両方が列挙されることになる。
Racket
これが本当に辛かった。。
- https://github.com/okuoku/yuni/blob/6d968de0df1541ad63319be73aa11ccf8e450daf/lib-runtime/selfboot/racket/selfboot-entry.rkt#L114 -- R6RS read処理
- https://github.com/okuoku/yuni/blob/6d968de0df1541ad63319be73aa11ccf8e450daf/lib-runtime/selfboot/racket/selfboot-entry.rkt#L200
--
module
構文への書き換え - https://github.com/okuoku/yuni/blob/6d968de0df1541ad63319be73aa11ccf8e450daf/lib-runtime/selfboot/racket/selfboot-entry.rkt#L146 -- R6RSのmutable pairからRacketのimmutable pairへの載せ替え
- https://github.com/okuoku/yuni/blob/6d968de0df1541ad63319be73aa11ccf8e450daf/lib-runtime/selfboot/racket/selfboot-entry.rkt#L123 -- ライブラリlookupの実装
Racketは言語処理系の実装フレームワークとも言うべく充実を誇っているが、この手のhackを実装するのは本当に超クッソ激烈に大変でゼロから実装した方が早いんじゃないかという気がしてくる。
標準のR6RS実装はRacketの言語として実装されており、単体で流用することをあまり想定していないように見える。selfbootではreaderだけ流用し、Racketネイティブの module
への変換は自前のものを実装している。このとき、何故か (quote id)
形式のライブラリ解決が quote: not a require sub-form
と言われて使用できず( quote
を再exportしているから?)、自前のライブラリlookupも追加で実装している。。
R6RSのreaderを使うには、 (require (prefix-in r6rs: r6rs/lang/reader))
のように r6rs/lang/reader
を直接importし、このライブラリの read-syntax
を使う。これによって読み取ったコードは自動的にRacketの module
構文に開かれるが、この module
はRacketのR6RS実装に固有のものなので直接使い廻すことができない。今回は syntax-case
で中身を取り出し、更に syntax->datum
して構文情報を取り去ってから、改めて module
を組み立てて eval
している。
また地味なポイントとしてRacketでは cons
等の手続きが生成するペアはimmutable pairとなっており set-car!
等が使用できない。RacketのR6RS実装では自動的にmutable pairを使うが、これらには専用の mcar
といった手続きを使う必要があるため、R6RSとRacketでやりとりするには変換が必要になる。ここではイテレータ in-mlist
を使って (for/list ((y (in-mlist x))) y)
で変換を済ませている。
多分Racketのプロなら syntax-case
で直接実装できると思うがどうやってもreaderが作ってくるsyntaxを使いつつ module
構文だけ差し替えるということが出来ず、4時間くらい格闘した挙句諦めてしまった。。Racket標準のR6RS実装は R6RSライブラリがRacket的な意味でCollectionを構成する必要が有り 、yuniのようにR6RS / R7RSの両処理系から読み取れるという要件を同時に達成するのは不可能になっている。
chibi-scheme、Gauche
- https://github.com/okuoku/yuni/blob/6d968de0df1541ad63319be73aa11ccf8e450daf/lib-runtime/selfboot/chibi-scheme/selfboot-entry.scm#L100 -- ライブラリ構文のみの環境定義 (chibi-scheme)
- https://github.com/okuoku/yuni/blob/6d968de0df1541ad63319be73aa11ccf8e450daf/lib-runtime/selfboot/gauche/selfboot-entry.scm#L86 -- 対話環境の取り出し (Gauche)
- https://github.com/okuoku/yuni/blob/6d968de0df1541ad63319be73aa11ccf8e450daf/lib-runtime/selfboot/chibi-scheme/selfboot-entry.scm#L113 -- ライブラリの別名定義(エイリアス)
chibi-schemeでは、 define-library
のようなライブラリ構文は "(meta)" と呼ばれる環境に入っているためこれを environment
手続きで取り出して使う。Gaucheでは current-module
手続きで現在の環境を取り出してそちらを書き換える方向で実装している。
chibi-schemeの include
は何故か絶対パスを処理できないため、単にソースコードを read
してそのまま quasiquote
で直接突っ込みevalしている。...手抜きでGaucheでもまったく同じ実装にしている。
どちらの処理系でも、環境内に library
構文を定義してしまって直接 load
するという手法は使えるはずだがまだ検証していない。
残件
- KawaやIronScheme等他の処理系での実装。
- ビルドシステムの移行。今はbootstrap schemeとしていくつかのR6RS/R7RS処理系を選び、それらに純粋なR6RS/R7RSで書いたビルドシステムを実行させているが、selfboot処理系に置き換えることでもっと多くの処理系をyuniのビルドに使用できるようになると期待される。
- R7RSやRacketで構文情報を捨てているのをやめる。
- OS情報などビルド時定数を何とかサポートできないか検討する。
実際に作ってみると、selfbootは何で今まで無かったんだろうという便利さで、もう毎回ランタイムをビルドしていた生活には戻れそうも無い。多分FFI互換層だけをCMakeでビルドするように替えて、Scheme側はCMakeでビルドするのを止めても良いんじゃないかと思っている。