各処理系向けのアプリケーション実行環境生成

今のところyuniのアプリケーションはCMakeのスクリプトで書かれたランチャ経由で起動される。単体で動作させるためには、これをなんとかScheme側に持ってくる必要がある。

直接起動を後回しにする

今回は直接起動を後回しにすることにした。今yuniがサポートしている15処理系向けそれぞれに(C標準におけるsystem()関数のような)プロセス起動のサポートを実装する必要があり、これが意外と仕様面で面倒なため; たとえば、プロセス間のパイプをサポートするか否かや、実行時のカレントディレクトリの変更をサポートするかどうか等が問題になる。これらはsystem()関数ではサポートされていないため、実装できない処理系が出てきても不思議ではない。
直接起動を後回しにするということは、今回の"処理系向けアプリケーション環境生成"は、単に"Schemeプログラムを読みとり、必要なライブラリファイルを生成し、起動用のバッチファイル/シェルスクリプトを出力する処理"と要約できる。
(これ書いてるときに結局chmodは必要ということに気付いてしまったが後で考える。Windowsでは困らないし。)

アプリケーション起動処理

yuniのアプリケーションはR6RSライブラリ構文とR7RS-smallのサブセットおよび処理系毎に用意された互換層で書かれる。いいかえると、yuniのアプリケーションを直接実行できる処理系は存在しない(nmoshではサポートする積りだけど)。
このため、yuniのアプリケーションを実行するためには、アプリケーションをそれぞれの処理系ネイティブの形に一旦変換する必要がある。変換するといっても、だいたいは R6RS: ライブラリパスを追加するだけ、または、R7RS: stubライブラリと呼んでいるライブラリを出力するだけで済むため、ライブラリからexportするsymbolやimportするライブラリが変わらない限りは再変換する必要はない。
アプリケーションの起動/実行処理は以下の3フェーズに分割できる:

Phase 入力 出力
1 Generate Schemeプログラム stubライブラリ、loadstub、CompileとExecuteのためのシェルスクリプト
2 Compile Schemeプログラム または loadstub 処理系依存
3 Execute 処理系依存

Generateフェーズは、プログラムを読み取って依存ライブラリを抽出し、依存ライブラリについてstubライブラリを必要に応じて生成する。compileとexecuteの各フェーズを実行するためのシェルスクリプトも生成する。autotoolsにおけるconfigureや、CMakeでのプロジェクト生成に相当する操作と言える。
Compileフェーズは、処理系の事前コンパイル機能を使ってコンパイル処理を行う。このフェーズが存在しない処理系(Chibi-scheme等)も有る。
Executeフェーズは、Compile結果を使用して実際にアプリケーションを実行する。
このため、1つのアプリケーションを実行するために都合3回処理系を起動する可能性がある。どこかのタイミングで真面目に統合する必要がありそうだが後回し。
アプリケーションを変更していない場合はGenerateとCompileは省略できる。が、アプリケーションや依存するライブラリに変更が加わっている場合は未定義の挙動となる(安全に停止しない)。先述のように、一般には、exportとimportの内容が変わらない限りはアプリケーションを変更して単にExecuteするだけでも安全な事が多い。また、stubライブラリとloadstubが不要な処理系(= Chez等大半のR6RS処理系)では事実上GenerateとCompileがno-opとなるため、これらに変更が加わっても問題なく実行できる。

Generate: stub生成

ここ半年ほどの(ChickenやGaucheにおける)R7RS対応の進展で、yuniのランタイムに要求される変換は大分集約されてきた。しかし、現状でも処理系毎に細かい処理の違いは存在する。

処理系 標準 stub必要? loadstub必要? exportフィルタ 特殊構文 コンパイルできる? 自動バイトコードキャッシュ?
Gauche R7RS Yes キーワード構文除去
Chibi-scheme R7RS Yes includeにライブラリパスを使用
Chicken R7RS Yes Yes 標準補助構文除去 ライブラリパス機能が無い Yes
Kawa R7RS Yes Yes ライブラリパス機能が無い できるけどエラー
Sagittarius R6RS/R7RS Hybrid Yes キーワード構文除去 Yes
Racket R6RS Yes #!r6rs必要 Yes
Guile R6RS Yes 標準補助構文除去 ライブラリ拡張子として.slsが使えない Yes
Larceny R6RS/R7RS Hybrid Yes
IronScheme R6RS Yes できるけどエラー
Chez R6RS Yes
Vicare R6RS できるけどエラー
nmosh R6RS/R7RS Hybrid Yes
Gambit R5RS Yes マクロ除去 Alexpanderで事前展開 Yes
MIT/GNU Scheme R5RS Yes マクロ除去 Alexpanderで事前展開 Yes

ここで"stub必要?"欄がYesになっているものは、yuniのR6RS-lite構文で書かれたライブラリを直接ロードすることはできず変換が必要になる。
変換の内容はいくつかある。いずれのR7RS処理系も、ライブラリはR6RSの(library ...)形式ではなく、R7RSの(define-library ...)でなければならないため、define-library → includeの組み合せで読ませている。幸い、libraryというシンボルはR7RSの範囲では予約されていないため、libraryという名前でマクロを定義してしまえば良い。

(define-syntax library
  (syntax-rules ()
    ((_ libname (export ...) (import ...) body ...)
     (begin body ...))))

R7RS処理系では、library構文はexport節とimport節を飛ばして body ... 部分をbeginで囲んで返す。export節とimport節の内容についてはdefine-library側に書く必要があるため、生成処理の際にライブラリをパースしてそれぞれの内容を取得しておく必要がある。
R6RS処理系では基本的に変換は不要なことが多いが、いくつかの処理系ではR7RS処理系同様のフィルタリングが必要だったり、特殊な構文を要求していたりして変換が必要になることがある。
(R5RS処理系はかなり特殊なので割愛 - http://d.hatena.ne.jp/mjt/20160825/p1 )
キーワード構文除去はCommon-lispスタイルのキーワード(コロンで始まるシンボル)をreaderがサポートしている処理系で必要になる。Gaucheはキーワードをsymbolのサブタイプにする予定で、SagittariusはリーダがR7RSモードの場合はキーワードを認識しないのでこの変換は本来不要だが、yuniは処理系ネイティブライブラリとの混在を企図して可能な限り処理系のデフォルト言語で実装するようにしている。
標準補助構文除去は、 ... とか else のような、標準で使用される補助構文をexportから削除する。Guileのように途中から削除されるようなケース https://github.com/okuoku/yuni/issues/29 では微妙だが、基本的にはyuniはこれらの構文がboundであることは要求していない。Gambitのようにそもそも全ての構文がexport不能なケースも存在するため、構文についてはnamespacingを期待していない。
他に処理系固有の事情がいくつかある:
Chibi-schemeでは、include構文の相対パスが"ライブラリ検索パスからの相対"で処理されるため、変換処理が出力したstubライブラリファイルの相対パスにはできない。このため、includeライブラリ構文に指定するパスも他の処理系とは異なる。
ChickenやKawaにはライブラリパスを指定してライブラリを検索する機能が無い。Chickenはeggとして特定の場所に配置する必要があり、Kawaは機能自体はたぶん存在するが上手く動いた試しがない。これらの処理系では、アプリケーションがimportする可能性のあるライブラリファイルを全て事前にloadしておく必要があるためそのためのコード(loadstub)も出力する。
IronSchemeは標準のフロントエンドが異常終了時もゼロを返す等挙動が微妙なのでloadstubを用意している( https://github.com/okuoku/yuni/blob/be01f716ae08f9b49da2de01344221806d455395/yuniloader/yuniloader-ironscheme.scm )。
Racketはライブラリ先頭に#!r6rsを付けないとR6RS言語のライブラリとして認識されない。
Guileはライブラリの拡張子として.slsを使えない。この拡張子自体はグローバル変数で指定できるが、先述のように標準補助構文の除去が必要なのでstubライブラリの生成が結局必要になる。

Generate: コマンドラインの生成

後続のフェーズのために、処理系を起動するためのコマンドラインを含んだバッチファイル/シェルスクリプトを生成する必要がある。
コレが意外と自明ではなく、yuniでは既存のSRFI(SRFI-22およびSRFI-138)には頼らずに自前で起動コマンドラインを生成している。
コマンドラインの生成のために、処理系をtype0 - 2の3種に分類している( https://github.com/okuoku/yuni/blob/29d65697f2f9d1d977b3befe8a9231870159c8cb/doc/PortingNotes/CommandLine.md )。
Type0処理系は、Generateフェーズではloadstubと呼ぶload(またはinclude)の羅列と多少のグルーコードを出力する。Type0処理系では処理系自身の"ライブラリの存在するディレクトリを指定してライブラリを探索する機能"を使用しない。全ての処理系をType0として処理しないのは、アプリケーションがライブラリのコードを実際に実行するまでパースを遅延させる実装が想定されるため。
Type1処理系は、ライブラリパスをオプションとしていくつか指定することができる。これがもっともstraightforwardな実装と言える。
Type2処理系はライブラリパスが1つしか指定できない。このためライブラリパス同士のセパレータがOS毎に異なり、結果的に生成コードがOS依存となる
Type1とType2では、処理系がスクリプトとして実際に実行するものはユーザが記述したスクリプト自身となる。実はR6RSとR7RSで(ライブラリでない)スクリプトの構文には互換性があるため、ライブラリさえ適切に配置すればそのまま実行させることができる。yuniの実行環境は、この"適切に配置"の部分を抽象化したものと言える。(Racketもライブラリには#!r6rsの指定を必要とするものの、プログラムはplt-r6rsで起動する限りは#!r6rsを指定しなくても正常に動作する。)