etolisp 進捗 (14) 〜 macro expansion
で,結局こうなった。context frame とか lexical context とか,正確な定義をしないまま変数名に使っているので,この辺は,間違っていたら,おいおい修正しようと思う。進捗(8) の方針をそのまま実装した。defun は lambda を setf したのとほぼ等価な実装で,defmacro は「ほぼ lamda」を setf したのと等価。実装は lambda の導出クラスという形にした。
(defun foo (...) ...) (foo ...)
が評価されたときに呼ばれる関数が
auto_ptr<const Value> ValueLambda::asFunction(RunTime *pE, const Value *pValue) const { RunTime::SymbolTable localTable; initContextFrame(pE, pValue, &localTable, cfuncEval()); { AutoContextFrame proxy(*pE, localTable); return value()->eval(pE); } }
なのに対して
(demacro bar (...) ...) (bar ...)
が評価されたときに呼ばれる関数は
auto_ptr<const Value> ValueMacro::asFunction(RunTime *pE, const Value *pValue) const { RunTime::SymbolTable localTable; initContextFrame(pE, pValue, &localTable, cfuncClone()); auto_ptr<const Value> pExpanded; { AutoContextFrame proxy(*pE, localTable); pExpanded = value()->asCons()->car()->eval(pE); } return pExpanded->eval(pE); }
のような感じになった。pE はランタイムのポインタ,pValue は関数の cdr の cons を参照している Value で,nil でない場合は cons であることが保証されている(nil の場合は asCons() で例外が飛ぶ)。value() が Lisp の関数の定義に相当する。localTable がローカル変数用のシンボルテーブルで,initContextFrame() にて,それに変数をセットする。AutoContextFrame が,ランタイムのスタックにシンボルテーブルを push/pop する。lambda はそれ自身を評価する前に引数を評価するから,cfuncEval,一方,macro は展開する前には評価しないので cfuncClone を呼ぶといった雰囲気。内部で仮想関数を呼ぶ実装にしないで,わざわざ関数オブジェクトを渡しているのは,両者の呼び出し方の違いを分かり易くするためで,切実な意味はない*1。
みそは進捗(8)の構想に加えて,macro のとき,macro expansion する文脈と,その結果を eval するときの文脈が異なるというところ。今のところ,マクロが呼ばれるたびに毎回展開されるという,悲しいことになっているが,改善は今後の楽しみとしてとっておく。
この辺りは,なんとなく思いうかべたことをダイレクトにコード化できてる感がある。とりあえず ANSI Common LISP (Prentice Hall Series in Artificial Intelligence) の 10 章のコードがちゃんと動くかどうか調べてみよう。
追記:
(defmacro for (var start stop &body body) (let ((gstop (gensym))) `(do ((,var ,start (1+ ,var)) (,gstop ,stop)) ((> ,var ,gstop)) ,@body))) (for x 1 8 (princ x))
これが動いたのは,けっこう感動。インタプリタの方が私より賢い(笑)。Lisp 難しすぎるぞっ!
*1:本当は「実装の共有には必ずテンプレートを使う」という強力なポリシーを掲げてもいいのかも知れない…