Schemeライブラリ互換レイヤの制作 - Racket stub library編

Teslawire用の組み込みインタプリタを作るにあたって、pFFIとその他の細かいユーティリティに移植性を持たせて色々なR6RS/R7RS実装で使いまわそうという方針にしている。良いScheme APIを作るための方針を、様々な処理系を(無理矢理)触ることで得られるのではないかな。と。
今のところ、nmosh、Racket(as R6RS reference impl)、chibi-scheme(as R7RS reference impl)、Gauche(as API reference impl)でpFFIを含めた全ての機能をサポートすることを目標にしている。chibi-schemeはゲームを駆動するには遅すぎるかもしれないが、それ以外の実装は真っ当な速度で動くことが期待できる。
ChickenやGambitのようなコンパイラ型実装もサポートしたいけど今のところ良い実装方針が見えていない(そもそもゲーム内REPLとして使うのでコンパイラ型の実装はあまり恩恵が無い)。psyntax系のR6RSであるVicare、IronSchemeと、nmoshに近いimport挙動を持つSagittariusはnmosh用のコードがほぼそのまま動くことを期待している。
というわけで、今後は個々の処理系についてstub library編とpFFI編のそれぞれを実施していくことになる。stub library編は、今回策定したR6RS-light形式のライブラリをincludeしてくるための"glue"ライブラリの実装を行う。
pFFI(Pseudo FFI)は、DLL/.so/.dyldを開いて内部のC言語関数を呼び出すためのインターフェースで、API記述(UCID)から生成したSchemeスタブのランタイムとなる。nmosh以外の処理系については処理系付属のFFIのwrapperライブラリを作成する(Racket)か、C拡張APIを使用してdyncall( http://www.dyncall.org/ )のバインディングを書き、その上に構築するか(chibi-scheme/Gauche)の2択を考えている。

R6RS-light library

ライブラリは独自に定めた"R6RS-light"形式で記述する。従来のnmoshで使用していたR7RS-bridge形式と異なり、import直下のbeginを要求しない。基本的にはR6RSライブラリから meta または for 指定によるexplicit-phasingを抜いたもので、nmoshやpsyntax系の実装ではそのまま使用できる。
R6RS-light形式のライブラリは、単一コンポーネントのライブラリ名を許可しない。ライブラリ名(yuni)は予約であり、常に(yuni core)のような2コンポーネント以上を必要とする。単一コンポーネント名のライブラリは、合成ライブラリに使用されることが多く、命名規則に処理系間で互換性がないためR6RS-lightでは使用不可としている。
R6RS-lightライブラリはR6RSライブラリの厳密なサブセットとなるため、見た目はR6RSのライブラリに見える。

(library (check testlib)
         (export hoge)
         (import (yuni scheme))
(define hoge #f)
         :
)

また、

  • ライブラリは1ファイル1ライブラリでなければならない
  • (scheme base)や(rnrs)のような標準ライブラリをインポートしてはいけない。Schemeライブラリとしては(yuni scheme)を提供する(R7RS base + αを提供する)
  • ライブラリはバージョンを持てない
  • ライブラリはload-hookを持つことができる。プログラムの実行時に高々一度実行されることが保証される。
  • ライブラリはload-hook以外の場所で副作用を起こしてはいけない。ロードのセマンティクスは処理系間で互換性が無いため。
  • シンボル 'library' をexportできない。

ちなみにアプリケーションは専用のフロントエンドで一旦expandしてから実行することを想定している。アプリケーションをライブラリの集合として実装することは常套手段だが、これを複数の処理系で共有するのはかなり難しい。

Import stubs

R6RS-light libraryは、RacketやR7RS処理系ではそのままロードすることはできない。RacketでR6RSライブラリをロードするためには、 #!r6rs を指定する必要がある。R7RS処理系では、そもそも library はライブラリの開始として認識されない。
このため、ライブラリを include するためのimport stubライブラリを(ライブラリ×処理系)毎に生成する。ライブラリ本体は処理系間で共有するが、stubライブラリは処理系固有のconstructを含むことがあるため共有できない。
Import stubを作るのではなく、R6RS-light形式のライブラリを直接R7RS形式に変換するなり先頭に#!r6rsを付けるなりの方法で各種処理系に適応させるという方向も考えられる(R7RS-bridgeは実際にそれを目指していた)。しかし、Import stub方式ならexportセットが変化しない限りはstubの再生成は不要になるというメリットがある。
Import stub方式の重要なデメリットは、R6RS-lightで書かれるライブラリは個々の処理系のS式リーダの共通部分しか使用できないという点になる。これはbytevectorやstringで問題になる。例えば、bytevectorリテラルは#vu8()で始めるR6RS流儀と#u8()で始めるR7RS流儀の両方がある。nmoshは両方を読めるようにしているが、そのような処理系ばかりではない。
Import stubは、更に2種類のstubに分けることができる。

  • Include stub。R6RS Light形式のライブラリをincludeし、適当なマクロで変換して処理系ネイティブのマクロとして認識させるためのstub。
  • Alias stub。他のInclude stubをimportし、そのままexportするstub。

Alias stubの存在意義は自明で無い: 後述するように(scheme base)のようなscheme標準ライブラリを定義できない処理系が存在するなどの理由で、ライブラリ名の命名には制約がある。yuniでは標準ライブラリも一旦 impl ライブラリに定義してからrenameする方針としている。この方針を取ることで、多くの場合では標準ライブラリをimportせずにライブラリを作成することができる。
例えば、Racket用のinclude stubは:

#!r6rs
(library
  (r7b-util u8-ready)
  (export u8-ready?)
  (import (rnrs) (yuni-runtime racket))
  (%%internal-paste
    "lib-r6rs/r7b-util/u8-ready.sls"))

のように、%%internal-paste構文を単に適用しているだけのライブラリとなる。
include stubのimport/exportは、includeするライブラリからそのまま持ってきて出力する。%%internal-paste構文は、Light R6RS形式ライブラリのソースコードからライブラリ名とimport/export部を除いた構文オブジェクトを返す。

#!r6rs
(library (yuni-runtime racket)
         (export %%internal-paste)
         (import (rnrs)
                 (only (racket) read-syntax syntax->list))

(define-syntax %%internal-paste
  (lambda (x)
    (syntax-case x ()
      ((here pth)
       (let ((pathname (syntax->datum #'pth)))
         (call-with-input-file
           pathname
           (lambda (p)
             ;; FIXME: It strips syntactic information....
             (let ((stx (syntax->datum (read-syntax pathname p))))
               #`(begin #,@(datum->syntax #'here (cddddr stx)))))))))))
)

FIXMEにあるように、この方法でincludeを実装すると行番号情報がオチてしまう。これを書いた当時はRacketにおける#!r6rsのリーダマクロ的な動作に気づいていなかったので、今ならちゃんと書けるかもしれない。

Racket固有の問題

RacketはRacket言語とR6RSの相互運用をサポートしている。yuniのstubライブラリはRacket言語ではなくR6RSで書かれるので、Racketのライブラリ(collection)がR6RSからどのように使用できるのかを知る必要がある。

  • 使えないライブラリ名がある

Racketのマニュアルには、RacketライブラリとR6RSライブラリの命名規則の対応表がある( http://docs.racket-lang.org/r6rs/libpaths.html )。しかし、Racketライブラリ名の一部はR6RSライブラリ名として記述できないことが有る。
例えば、SRFI-9ライブラリはRacketでは srfi/9 となっているが、これを (srfi 9) として参照することは出来ない。R6RSは、ライブラリ名のコンポーネントはシンボルであることを要求しているため、数値である 9 をライブラリ名の一部に使用することができない。もっとも、Racket上でサポートされているSRFIの場合は、標準の(srfi :9)のようにコロンを含む表記でアクセスすることができる。(R7RSからは(srfi 9)のようなライブラリ名も合法になった)

  • 標準ライブラリ名が被っている

Racketには、scheme/baseやscheme/fileのようなライブラリが標準でインストールされている。これらのライブラリはR7RSの(scheme base)や(scheme file)とは関係が無いライブラリで、R6RS環境にはimportすらできないため、RacketではnmoshやSaggitariusのようにR6RS/R7RS混在コードを書くことができないことになる。
yuniでは、R6RS/R7RS標準ライブラリの直接的なimportは禁止することでこの問題に対処している。stubライブラリジェネレータは、(scheme base)を(scheme base0)と読み替えてstub libraryを生成する。

  • #!r6rs はライブラリを変換する

Racketにおける#!r6rsはある種のリーダマクロとして実装されているようで、#!r6rsの後に来た library 構文は自動的に RacketのModuleに変換されるらしい。このため、#!r6rsをかなり厳密に運用する必要がある。