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のリポジトリに簡単なサンプルを置いた:

(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) のようにシンボルで記述されるため、これを適当にファイルパスに変換してやる必要がある。

よって、

  1. プログラムを読み取り、依存ライブラリを収集する
  2. 依存ライブラリシンボルをパスに変換し、依存ライブラリの依存関係を再帰的に収集する
  3. ライブラリの依存関係をトポロジカルソートし、パス名をロード順に並び換える
  4. 処理系の load に渡し、ライブラリを実際にロードする

yuniではどの処理系でもyuniのライブラリを直接読み取れるように構文要素を注意深く選んでいる。このため、yuniのソースコードは基本的に処理系の read で直接読み取ることができる。...その後更に load するので2度読みになってしまうが。。依存関係の抽出等の処理は、各処理系に共通する語彙だけを使って共通のSchemeソースとして記述できる。

ただ、処理系の load が直接yuniのR6RS形式ライブラリを読み取れるのは 当然R6RS処理系に限られる ため、これができない処理系では load に相当する手続きを自前で実装している。この 自前で実装した load こそがselfbootの実装のキモ ということになる。

rationale

Schemeアプリケーションを書いている人にとっては、yuniの "ライブラリ書式はR6RS、語彙はR7RS" という組み合せに違和感が有るかもしれない。これには幾つか理由があって、

  • ライブラリシステムは差し替えが難しいため、低機能な方に合わせる方が好ましい。つまり、R7RSの define-library よりもR6RSlibrary のほうがずっと低機能なので実装しやすい。
  • 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

ChezSchemeのselfboot実装はそんなに難しくない。ChezではREPL用に interaction-environment が提供されるため、単にその環境をコピーして library でも何でも eval してしまえば良い。

yuniはR6RS形式のライブラリを採用しているため、Chez上では単純に load すれば良い。Chezの load は第3引数としてハンドラを渡すことができ、その中で eval することになる。

Guile

GNU GuileはChez同様 interaction-environment を備えている。ただ、Guileは標準ライブラリ内部で補助構文を束縛していないため、 export 内で再エクスポートされる場合は取り除く必要がある。このため、 load のような直接ロードは使用できず、フィルタ処理が必要になる。

このようなソースコードの書き換えが必要な処理系では、構文情報が抜けてしまうため行番号情報等が無くなってしまうというデメリットがある。真面目に syntax-case で加工すれば多分何とかなるが。。

Sagittarius

SagittariusでもChez同様に実装できる。

selfbootでは、ライブラリパスの解決は自前の処理で行うことになるが、処理系付属のライブラリは検索する必要がないため事前に除く必要がある。SagittariusR6RS/R7RS Hybrid処理系なため、R6RSライブラリとR7RSライブラリの両方が列挙されることになる。

Racket

これが本当に辛かった。。

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-schemeGauche

chibi-schemeでは、 define-library のようなライブラリ構文は "(meta)" と呼ばれる環境に入っているためこれを environment 手続きで取り出して使う。Gaucheでは current-module 手続きで現在の環境を取り出してそちらを書き換える方向で実装している。

chibi-schemeinclude は何故か絶対パスを処理できないため、単にソースコードread してそのまま quasiquote で直接突っ込みevalしている。...手抜きでGaucheでもまったく同じ実装にしている。

どちらの処理系でも、環境内に library 構文を定義してしまって直接 load するという手法は使えるはずだがまだ検証していない。

残件

  • KawaやIronScheme等他の処理系での実装。
  • ビルドシステムの移行。今はbootstrap schemeとしていくつかのR6RS/R7RS処理系を選び、それらに純粋なR6RS/R7RSで書いたビルドシステムを実行させているが、selfboot処理系に置き換えることでもっと多くの処理系をyuniのビルドに使用できるようになると期待される。
  • R7RSやRacketで構文情報を捨てているのをやめる。
  • OS情報などビルド時定数を何とかサポートできないか検討する。

実際に作ってみると、selfbootは何で今まで無かったんだろうという便利さで、もう毎回ランタイムをビルドしていた生活には戻れそうも無い。多分FFI互換層だけをCMakeでビルドするように替えて、Scheme側はCMakeでビルドするのを止めても良いんじゃないかと思っている。