Gauche:include
Shiro(2009/12/30 01:30:05 PST): Gaucheへのinclude導入についてのメモ。
includeとは
いくつかのScheme処理系にはincludeというプリミティブがある(x:include)。 loadと違って、指定されたファイルの内容をあたかもその位置に挿入されたかのように して評価する。従って例えばletの内部にincludeすることなんかもできる。 意味的には、loadがevalの親戚で、外から来たS式の評価という実行時の セマンティクスなのに対し、includeは親フォームのコンパイル時に既に展開される、 一種のマクロ展開時のセマンティクスと言える。
展開時セマンティクスということではGaucheのrequire/use、あるいはR6RSのimportの 仲間なんだが、requireやR6RS importが「独立した別のユニット」の参照であるのに 対し、includeはその場に埋め込んでしまう、従って呼び出し元のコンテキストの 影響を受ける、というところが違う。
通常のアプリケーションではユニット間の独立性は高い方が何かと便利なので、 includeが必要になる場面は滅多にないと思うが、たまに欲しい時もある。 Gaucheのコンテキストでは例えば:
- ソースファイルは分割して管理したいが、コンパイルユニットとしてはひとつに なっていて欲しい、という場合。コンパイルユニットを分ける (モジュールを分ける) と、インタフェースをちゃんと決めてimport/exportしないとならないんだけれど、 ソース間が密接に関連しあっていてモジュールの綺麗な階層構造が作れない場合もある。
- サードパーティのソースを隔離された環境に読み込みたい、という場合。 Gauche的にはモジュール関連のフォームをつけたせばある程度隔離することができるけれど、 元ソースに手をつけたくない場合もある。includeがあれば、別ファイルにdefine-moduleなど Gauche特有のフォームを書き、そこから元ソースをincludeすればいい。 loadでもできなくはないが、loadは実行時動作なのでプリコンパイルしたい場合に 困る。
で、R7RSの議論でもincludeの話が出てきたし、あっても良いかなと思って手をつけて みたんだが意外なトラップがあった。
相対パスの解釈
includeされるファイル名が相対パスで指定されていた場合、どう解釈すべきか。 これについては処理系間でも仕様が割れているようだ。
loadやrequireと同列と考えるなら、素直にload-pathから探してしまえば良いのだが、 includeではincludeするファイルとされるファイルの結びつきが、 loadやrequireのそれよりもずっと強いように思える。 外から見ればincludeするファイルとされるファイルは一体で、 たまたま便宜上分割されているにすぎない、という感覚だ。
だとすれば、相対パスはincludeしているファイルのあるディレクトリからの 相対とまずは解釈するのが自然なように思える (そこに見つからなければload-pathから探す)。 例えばfoo.scmがsub/bar.scmをincludeし、sub/bar.scmがbaz.scmをincludeしていたとすると、
-+- foo.scm +- sub/bar.scm +- sub/baz.scm
というディレクトリ階層になっていれば、これがどこに置かれようがfoo.scmは隣のsubディレクトリ 下のbar.scmをincludeし、bar.scmは隣のbaz.scmをincludeすることになる。
で、現在のファイルからの相対で探すように実装してみたのだが、includeがネスト した場合にうまく動かない。
理由はこうだ。
- foo.scm中のincludeは、その位置にあたかも sub/bar.scm中のフォームすべてがbeginで囲まれて存在するかのように 展開される。この時点で、bar.scm中のincludeは展開されない。 bar.scmの展開がすべて終わったらbar.scm自体は閉じられる。
- foo.scmは展開されたフォームのコンパイルを続け、 (元々bar.scm中にあった)includeフォームを見つけてbaz.scmを 読もうとする。この時、そのincludeがsub/bar.scmからの相対であるという 情報は既に失われているので、foo.scmは自身と同じディレクトリにbaz.scmを探しに行ってしまう。
includeは呼び出し元のコンテキストの影響を受けるのが特徴であった。 ということは、includeに指定するパスも呼び出し元のコンテキストの影響を受けてしまう ということだ。
includeされたフォームをsubtreeとして直接pass1をかけられるなら、 そのダイナミックスコープの間だけincluderの情報を持たせればいいんで無問題なんだけど、 includeがinternal defineの連なりの一部を構成する場合があるので、include以下を 独立したsubtreeと考えるわけにはいかないのだ。
(let () (define x 0) (include "foo.scm") ;; この中に(define (foo a) (bar a))みたいなinternal defineがある可能性あり (define (bar b) b) (foo x))
internal defineの終わりはincludeを越えてフォームを見て行かないと決定できない (上の例ではincludeフォームの後のbarの定義もinternal defineになる)。
他の処理系でどうやってるのかなと思ってみてみると、ChezとChickenは そもそもincluderのパスとは無関係のようだ (まずカレントディレクトリ、次いで 指定されたパス)。まずカレントディレクトリを探すというのはコンパイルonlyの 処理系じゃないと恐ろしくて使えない。
PLTはincluderのパスを見るようだが、ネストした場合の動作はどうなるだろう。 ---ネストした場合、直接のincluderからの相対を探してる、つまり上のfoo.scmなどの例 で示したような動作になってる。やっぱりこれがわかりやすいよな。 しかしincluderをトラックしてるってことはincludeを完全に展開してから コンパイルしてるわけじゃないってことかな。マクロで生成されるincludeも ちゃんと認識してるからなあ。
ああなるほど。PLTはsyntax objectに付随するソースコード情報から includerのパス名を取り出してるのか。うーむ… Gaucheもソースコード情報は保持してるけど、マクロ展開で挿入される コードにはついてる保証がないからあてにできない。
要はメタ情報をどうやってコンパイラの中で受け渡して行くかって話だから、 メタ情報を受け渡すダミーフォームみたいなやつを使うって手はあるな。
結局、internal defineを扱うpass1/body内だけソースコード情報を 陽に扱い、それをcenvを通じてleafに伝える形で実装した。 internal defineは実装者泣かせだなあ。