yuniをR5RSに対応させる
今までyuniはR6RS/R7RSの互換ライブラリということにしていたが、これにR5RSを加えられないか検討することにした。
- 本家Schemeであるところのmit-schemeがR5RS
- Bones(amd64のアセンブリを吐くコンパイラ https://bitbucket.org/bunny351/bones/ )とかBiglooのような興味深い処理系が有る
- Alexpanderを使えば、BiwaSchemeとかtinyschemeのようなdefine-syntaxマクロの無い処理系にも対応できる
で、実際に試してみたらそれなりに動いたので、とりあえずサポートに加えてみて実際のアプリケーションで検証を進めていく。
ライブラリ機構が無い → ソースコード連結器を書く
R5RSの言語自体にはライブラリ機構が存在しない。というわけで、既存のPicrin用のライブラリローダを流用して適当なローダーを用意した。https://github.com/okuoku/yuni/blob/adccf4a840243a9111b88ded27e3313e26906e7c/yuniloader/yuniloader-fake.scm
このローダーはプログラムのimport節を読んで必要なライブラリファイルをロードし、それぞれのライブラリファイルのimport節も再帰的に処理してプログラムに必要なライブラリを全部連結したS式を出力する。
S式の出力過程で、ライブラリは多少変換される。基本方針は過去のにっきに書いた( http://d.hatena.ne.jp/mjt/20150601/p2 )もので、
- 基本的にライブラリは全体をletで囲んで定義が外に出ないようにする
- exportしている変数は適当にrenameしてletの中からset!する
- define-syntaxした定義をexportしている場合はtop-levelにご招待
- define-syntaxした定義をexportしているライブラリがimportしているライブラリもtop-levelにご招待
- マクロのrenameは禁止
というルール。
(library (a) ;; マクロをエクスポートしないライブラリ(letで囲める) (export a) (import ...) (define a 10)) (library (b) ;; (a)をimportする普通のライブラリ(これもletで囲める) (export b) (import (a) ...) (define b 10)) (library (c) ;; マクロをエクスポートするライブラリ(toplevelにご招待) (export c) (import (d) ...) (define-syntax c ...)) (library (d) ;; toplevelのライブラリからインポートされているライブラリ(letで囲めそうだけどtoplevel) (export d) (import ...) (define d 10)) ↓yuniloader: 別々のファイルにあるものを集めて1つのR5RSプログラムに合成して出力 ;; ライブラリ(a) (define %%a #f) ;; 他に a という名前のシンボルが有った場合に共存できるようにrename (let () (define a 10) ;; ★ライブラリの元コード (set! %%a a)) ;; エクスポート ;; ライブラリ(b) (define %%b #f) (let ((a %%a)) ;; インポート (define b 10) ;; ★ライブラリの元コード (set! %%b b)) ;; ライブラリ(d) (define %%d #f) (let () (define d 10) ;; ★ライブラリの元コード (set! %%d d)) ;; ライブラリ(c) (define d #f) (set! d %%d) ;; マクロから依存されているのでtoplevelにインポート (define-syntax c ...) ;; ★ライブラリの元コード
実際のyuniのCIテストを変換してみたもの: https://gist.github.com/okuoku/9152a66a1051a68b2677bfbe7b3650b0 (かなり下の方にスクロールしないとimportが有るライブラリに辿りつかない)
変数のエクスポート処理はset!で行う。マクロはtop-levelにdefine-syntaxするしか無いので、特にエクスポート処理やインポート処理は無い。letで囲めるライブラリの場合はインポート処理はletのバインディングとして行えるが、toplevelにご招待されたライブラリは、エクスポートと同様のdefine - set!ペアでインポート処理を行う。R6RS/R7RSのライブラリ機構と違い、toplevelにご招待されたライブラリのグローバルシンボルは後続のライブラリのグローバル環境からも見えてしまう。
実際のアプリケーションでは、ライブラリからマクロをexportするケースはかなり稀で、仮にマクロをexportしたとしても、多くのライブラリをimportしていることはほぼ無いため、これらの規制が有ったとしても特に不都合は無さそう。
ローダはライブラリをreadし、トップレベルのシーケンスをスキャンしてdefine-syntaxを探し、トップレベルでdefine-syntaxしているものはマクロであると認識する。define-syntaxに展開されるマクロがexportされているケースが有ったらどうすんの(= 字面からだけではマクロと認識できないケース)という問題は有るが、手元のコードではそのようなマクロは2例しか無かった。
yuniは今でも補助構文(...とか=>やelse)のrenameは禁止している。これは補助構文がboundでない処理系がR6RS/R7RSにも普通に存在し(ChickenやGuile)、互換性の確保が難しいため。R5RS環境を勘定に入れるなら、補助構文だけでなくマクロのrenameも禁止になる。
"toplevelにご招待"とは、単にlibraryで囲まれた部分をそのまま出力するだけとなることを指す。このため、マクロをexportしているライブラリ内部のdefineは全部グローバル変数になる。ただ、微妙に盲点だったのは、マクロをexportしているライブラリに直接importされる関係に有るライブラリもtoplevelにご招待される点。このためライブラリを設計するときにマクロを別ライブラリに分離するといった配慮が必要になる。
defineのセマンティクスが違う → あきらめる
個人的にSchemeを本格的に使い出したのはR6RSからなのでR5RSの仕様にはあまり詳しくない。が、今回初めてガッツリとR5RSを書いてみて一番壁になったのはdefineの仕様がletrec*でなくletrecである点。これはもうどうしようもない。
R6RS/R7RSでは、internal defineはletrec*で定義される。このためdefineの定義は上から順番に確定すると考えることができる。
(define a 10) (define b (+ a 10) ;; => 20 ↓ (letrec* ((a 10) (b (+ a 10)) ;; R6RSではletrec*なので、ここでaの値を使える ...)
R5RSでは基本的にletrecの挙動が採用されている。なので、
(define a 10) (define b (+ a 10)) ;; => エラー(未定義) ↓ (letrec ((a 10) (b (+ a 10)) ;; R5RSではこのタイミングではaが確定していない ...)
この挙動はyuniの方々で依存しているところがあったので、ライブラリを分割する等して対応した。
(library (a) (export a) (import ...) (define a 10)) (library (b) (export b) (import (a) ...) (define b (+ a 10)) ;; (import (a))しているので、ここではaは確定している
psyntax-moshからnmoshに移植するときに、同じような理由でライブラリを分割する必要があったのを思い出すな。。
define-valuesが実装できない → あきらめる
特にこのletrec*とletrecの差で困るのは、この仕様によりR5RSではdefine-values構文をsyntax-rulesで実装する方法が全く思いつかないこと。
yuniのR6RS向けR7RS互換レイヤでは、define-valuesを以下のような変換で実装している。
(define-values (a b) (values 1 2)) ↓ (define a #f) (define b #f) (define dummy (begin (call-with-values (lambda () (values 1 2)) (lambda (tmp0 tmp1) (set! a tmp0) (set! b tmp1)))))
しかし、これはR5RSでは使えない。何故ならinternal defineはletrecに展開されるので、define同士の実行順を規定することができないため。