目次へ

5. インタープリタの Eval メソッド


主要な public クラスの図を前章 2.2 節 から下記に再掲する。 色分けはクラスのソース・ファイルを示す。 赤が Lib_Interp.cs, 緑が Lib_Reader.cs, 青が Lib_Types.cs である。 本章では Lib_Interp.cs に含まれる Lisp インタープリタ本体 Interp の中核的なメソッド Eval と,それを駆動するメソッド Run を述べる。 メソッド LoadNatives については次章で取り上げる。


5.1 スクリプト実行メソッド Run

2.1 節 で述べたように,インタープリタ本体 Interp のインスタンス interp に対してファイル名 file_name の Lisp スクリプトを実行するには interp.Run(Lines.FromFile(file_name)) を行う。 静的メソッド Lines.FromFile については 3.1 節 で述べた。 その戻り値を引数とする Run メソッドを次のように定義する。

    public partial class Interp
    {
        ……
        public object Run(IEnumerable<string> lines) {
            bool interactive = (lines is InteractiveInput);
            using (Reader rr = new Reader (lines)) {
                object result = null;
                for (;;) {
                    try {
                        object x = rr.Read();
                        if (x == LL.S_EOF) {
                            if (interactive)
                                Console.WriteLine("Goodbye");
                            return result;
                        }
                        result = Eval(x);
                        if (interactive)
                            Console.WriteLine(LL.Str(result));
                    } catch (EvalException ex) {
                        if (interactive)
                            Console.WriteLine(ex);
                        else
                            throw;
                    }
                }
            }
        }
        ……
    } // Interp

Reader クラスについては 3.4 節 で述べた。 終了時にファイルを確実に解放するため,Reader のインスタンス rrusing 文で構築する。 引数で与えられた入力行の集合体 lines から rr.Read() で (もしかしたら複数行にまたがっている) 一つの Lisp 式を読み取り,objcet x に格納する。 それが EOF ならば,そこでメソッドを終了し,最後に評価した結果である resultreturn する。 そうでなければ,xEval(x) で評価し,結果を result に格納する。

もしも linesInteractiveInput のインスタンスならば,対話セッションとしてフラグ interactive を立てる。 このフラグにより result が毎格納後に表示されるなど,若干の動作が変更される。 InteractiveInput クラスについては 3.2 節 で述べた。

対話セッションの実行や,文字列として与えられた Lisp プログラムの実行のために,次の二つの便宜メソッドを用意する。

        /// <summary>対話セッションを行う便宜メソッド
        /// </summary>
        public object Run() {
            return Run(new InteractiveInput("> ", " "));
        }

        /// <summary>文字列中の Lisp プログラムを実行する便宜メソッド
        /// </summary>
        public object Run(string text) {
            return Run(Lines.FromString(text));
        }

対話セッションで表示される1次プロンプト 「> 」 と2次プロンプト「  」 は,ここにある文字列定数 "> ""  "に由来する。

> (printf "%12d\n"
          #x40bc84)
     4242564
nil
> 

5.2 Eval メソッド概観

前節で言及した1引数 Eval メソッドの定義を示す。

        /// <summary>オブジェクトとして構成された Lisp 式を評価する
        /// </summary>
        public object Eval(object exp) {
            return Eval(exp, false);
        }

これから分かるように,1引数 Eval メソッドは下記の2引数 Eval メソッドから第2引数を隠した便宜メソッドである。 第2引数は末尾呼出しの最適化のためのフラグとして使われる。詳細は次節で述べる。

        internal object Eval(object x, bool canLoseEnviron) {
            try {
                for (;;) {
                    if (x is Symbol) {
                        return EvalSymbol((Symbol) x);
                    } else if (x is Arg) {
                        return ((Arg) x).GetValue(environ);
                    } else if (x is Cell) {
                        Cell xc = (Cell) x;
                        object fn = xc.car;
                        if (fn == LL.S_QUOTE) {
                            Cell kdr = xc.cdr as Cell;
                            if (kdr == null || kdr.cdr != null)
                                throw new EvalException ("bad quote");
                            return kdr.car;
                        } else if (fn == LL.S_PROGN) {
                            x = EvalProgN(xc);
                        } else if (fn == LL.S_COND) {
                            if (EvalCond(ref x))
                                return x;
                        } else if (fn == LL.S_SETQ) {
                            return EvalSetQ(xc);
                        } else if (fn == LL.S_LAMBDA) {
                            int arity = Compile(ref x);
                            return new Closure (arity, x, environ);
                        } else if (fn == LL.S_MACRO) {
                            if (environ != null)
                                throw new EvalException ("nested macro", x);
                            ReplaceDummyVariables(ref x);
                            int arity = Compile(ref x);
                            return new Macro (arity, x);
                        } else if (fn == LL.S_CATCH) {
                            return EvalCatch(xc);
                        } else if (fn == LL.S_UNWIND_PROTECT) {
                            return EvalUnwindProtect(xc);
                        } else if (fn == LL.S_DELAY) {
                            Cell kdr = xc.cdr as Cell;
                            if (kdr == null || kdr.cdr != null)
                                throw new EvalException ("bad delay");
                            return new Promise (kdr.car, environ, this);
                        } else {
                            ……
                        }
                    } else if (x is Lambda) {
                        Lambda xl = (Lambda) x;
                        return new Closure (xl.arity, xl.body, environ);
                    } else
                        return x;
                }
            } catch (EvalException ex) {
                if (ex.Trace.Count < LL.MAX_EXC_TRACES)
                    ex.Trace.Add(LL.Str(x));
                throw;
            }
        }

これから分かるように2引数 Eval メソッドは無限ループを構成する。 これは末尾式を元の呼出し深さで評価するためである。 例えば,N 個の式 E1 E2 … を順に評価し,末尾式 EN の評価値を結果とするスペシャルフォーム (progn E1 E2EN) の実装は x = EvalProgN(xc) である。 EvalProgN メソッドは末尾式 EN を自分では評価せず,呼出し元の Eval メソッドに x として差し戻す。 無限ループの中で Eval メソッドは差し戻された x を評価する。

        private object EvalProgN(Cell xc) {
            Cell c = xc;
            object body = c.cdr;
            if (body == null)
                return null;
            c = body as Cell;
            if (c == null)
                throw new ProperListExpected (xc);
            for (;;) {
                object d = c.cdr;
                if (d is Cell) {
                    Eval(c.car, false);
                    c = (Cell) d;
                } else {
                    if (d != null)
                        throw new ProperListExpected (xc);
                    return c.car; //  末尾呼出し => 戻った先で評価
                }
            }
        }

条件式 (cond … (TEST E1EN) …) TEST が成立した節の末尾式 EN についても同様である。

ラムダ式 (lambda (ARGS…) BODY…) に対して Compile メソッドは本体 BODY… をコンパイルし,引数 ARGS… の個数 arity とともに返す。 Eval メソッドは arity とコンパイル済みの本体 x と現在の環境 environ からクロージャ Closure のインスタンスを構築して return する。

                        } else if (fn == LL.S_LAMBDA) {
                            int arity = Compile(ref x);
                            return new Closure (arity, x, environ);

environInterp クラスのメンバ変数である。

        internal Cell environ = null;

Compile メソッドは次を行う。

  1. ラムダ式本体中の束縛変数の出現を,環境中のフレームの相対深さとフレーム内での位置を表す Arg インスタンスに置き換える。
  2. ラムダ本体中のマクロを展開し,入れ子のラムダ式を Lambda クラスのインスタンスにコンパイルする。 Lambda はメンバとして環境を持たない点で Closure と異なる。

コンパイルの結果は対話セッションで簡単に確かめられる。 下記の一つ目の結果で (1) は,(1 . nil) つまりクロージャの引数の個数が 1 で,環境が nil であることを示す。 二つ目の結果で #0:1:b は,Arg インスタンスの ToString() 値であり, 現在の環境で (0 から数えて) 0 番目のフレームの,(0 から数えて) 1 番目の位置の,コンパイル前の名前が b である束縛変数を表す。 三つ目の結果で,入れ子のラムダ式からみて自由な変数である a は,一つ外側の環境に相当する一つだけ深い (つまり,0 から数えて 1 番目の) フレームの 0 番目の位置の Arg インスタンス #1:0:a へとコンパイルされている。

> (lambda (a) (+ a b))
(#<closure> (1) (+ #0:0:a b))
> (lambda (a b) (+ a b))
(#<closure> (2) (+ #0:0:a #0:1:b))
> (lambda (a) (lambda (x) (+ x a)))
(#<closure> (1) (#<lambda> 1 (+ #0:0:x #1:0:a)))
> 

Lambda インスタンスは,評価されると,現在の環境 environ と組み合わされて Closure を戻り値とする。

                    } else if (x is Lambda) {
                        Lambda xl = (Lambda) x;
                        return new Closure (xl.arity, xl.body, environ);

5.3 環境の構築と末尾呼出しの最適化

(defun による) Lisp の利用者定義関数は,最終的には Closure インスタンス fn の実引数リスト argList への適用として評価される。 argList の各要素が GetArgs メソッドによりそれぞれ (再帰的に Eval メソッドで) 評価されて,IList args が作られる。 fnargsEval の第2引数がメソッド ApplyDefined に渡される。

        internal object Eval(object x, bool canLoseEnviron) {
            try {
                for (;;) {
                    ……
                    } else if (x is Cell) {
                        ……
                        } else {
                            Cell argList = xc.cdr as Cell;
                            if (argList == null && xc.cdr != null) {
                                throw new ProperListExpected (x);
                            }
                            fn = Eval(fn, false);
                            if (fn is Promise)
                                fn = ((Promise) fn).Deliver();
                            if (fn is Closure) {
                                IList args = GetArgs(argList, false);
                                if (ApplyDefined((Closure) fn, args,
                                                 canLoseEnviron, out x))
                                    return x;
                            } else if (fn is Macro) {
                                IList args = LL.IListFrom(argList);
                                ApplyDefined((Macro) fn, args, false, out x);
                            } else
                                return CallNative(fn, argList,
                                                  ! lazy.Contains(fn));
                        }
                    } else ……
                }
            } catch (EvalException ex) ……
        }

ApplyDefined では,Closure である fn が持っていた環境 fn.env の先頭に, 新しいフレームとして実引数から構成した args を加え,それを現在の環境 environ とする。 この新しい環境のもとで Closure の本体 fn.body を,前節で述べたスペシャルフォーム progn のように順に評価する。

        /// <returns>false ならば戻った先で x を評価する
        /// </returns>
        private bool ApplyDefined(DefinedFunction fn, IList args,
                                  bool canLoseEnviron, out object x) {
            Cell body = fn.body as Cell;
            ……
            int arity = fn.arity;
            if (arity < 0) {    // &rest 付きならば
                ……
            }
            if (arity != args.Count)
                throw new EvalException ("arity not matched");
            Cell old_env = environ;
            environ = new Cell (args, fn.env);
            try {
                for (;;) {
                    Cell d = body.cdr as Cell;
                    if (d == null)
                        break;
                    Eval(body.car, false);
                    body = d;
                }
                if (canLoseEnviron) {
                    old_env = environ; // 元の環境をつぶして
                    x = body.car;      // 末尾呼出しを
                    return false;      // 戻った先で評価する
                } else {
                    x = Eval(body.car, true); // 末尾呼出しとして評価する
                    return true;
                }
            } finally {
                environ = old_env;
            }
        }
ここで新しいフレーム args を,クロージャが持っていた環境 fn.env ではなく,現在の環境 envrion の先頭に加えるようにすると, Algol 由来の近代的な静的スコープ (字句的スコープ) ではなく,Lisp の元々の姿である旧時代風の 動的スコープ にできます。 ただし,前節で述べた Compile メソッドでは,束縛変数の出現を静的スコープにあわせてコンパイルしていますし, Eval メソッドでもそのコンパイル結果にあわせて束縛変数にアクセスしますから,実際に動的スコープを再現するには,もう少しあちこちに手を入れる必要があります。

前述のとおり progn の実装では,末尾式を自分では評価せずに,呼出し元である Eval に差し戻している。 しかし,利用者定義関数の適用では,環境を変化させているから,単純な差し戻しはできない。

通常,Eval の第2引数の値を引き継いだフラグ canLoseEnvironfalse である。 この場合,メソッドの終了時に環境を復旧する必要がある。したがって

                    x = Eval(body.car, true); // 末尾呼出しとして評価する
                    return true;

のように末尾式をこのメソッドから再帰的に評価して,終了時に try 文の finally 節で元の環境を復旧する。 ただし,今の環境については (末尾式だから) 評価後そのまま捨てて構わない。 そのことを示すために Eval の呼出しで第2引数を true にする。

一方,フラグが true,つまり元の環境が末尾呼出しのものだった場合,元の環境を復旧する必要はない。 そのまま捨て去り,単純に末尾式を呼出し元に差し戻す。

                    old_env = environ; // 元の環境をつぶして
                    x = body.car;      // 末尾呼出しを
                    return false;      // 戻った先で評価する

これにより,末尾呼出しに対して元の呼出し深さが維持される。つまり,末尾呼出しの最適化が実現される。

5.3.1 注意点

ここで述べた末尾呼出しの最適化の実現方法は,L2 Lisp の Pascal による最初の実装以来,基本的に変わっていない。 この方法の注意点として,呼出し深さゼロのところからの最初の末尾呼出しに対して最適化が効かないことが挙げられる。 下図に Lisp プログラムと,それを実行したときの時間軸に沿った呼出しの深さ (図では積み上げの「高さ」) を示す。 図の箱に記入した (f0)false は,Lisp 式 (f0)Eval メソッドの第2引数を false にして (つまり,末尾呼出しであるというフラグを下げて) 評価することを示す。


f1 からの f2f2 からの f3f3 からの g の呼出しについては,フラグが立っている状態での末尾呼出しのため,呼出し元 (当然,そこではフラグは立っている) に差し戻される。 しかし,f0 からの f1 の呼出しについては,フラグを立てることを除き,通常どおり呼出しを積み上げる。

この弱点を直す一つの方法は,Run メソッド (5.1 節) が呼び出す1引数 Eval を次のように定義することである。

        public object Eval(object exp) {
            Cell old_env = environ;
            try {
                return Eval(exp, true);
            } finally {
                environ = old_env;
            }
        }
しかし,積み上げがどんどん累積するわけではなく,十分に深刻な弱点とは思われませんから,本処理系では簡単のため特に対策はとっていません。
ところで,本処理系では,多くのメソッドにいちいち環境を引き渡す手間を省くため,environ をメンバ変数としているわけですが, 上記のメソッド定義をみると,もしも environ をメンバ変数ではなく引数にして Eval 後に復旧する手間を無くしたとしたら,今のフラグ引数は不要にできそうです。 これは実装上の興味深い論点になるかもしれません。

5.4 実引数の評価

5.3 節 で述べたように,利用者定義関数の実引数のリスト argList は,GetArgs メソッドで IList args に変換される。

                            if (fn is Closure) {
                                IList args = GetArgs(argList, false);
                                if (ApplyDefined((Closure) fn, args,
                                                 canLoseEnviron, out x))
                                    return x;

GetArgs メソッドの定義を下記に示す。 willForce は,評価結果が遅延評価式,つまり「約束」だった場合に強制的に評価を行うためのフラグである。

        private IList GetArgs(Cell argList, bool willForce) {
            List<object> args = new List<object> ();
            Cell jc = argList;
            if (jc != null)
                for (;;) {
                    object x = EvalAndForce(jc.car, willForce);
                    args.Add(x);
                    object j = jc.cdr;
                    jc = j as Cell;
                    if (jc == null) {
                        if (j != null)
                            throw new ProperListExpected (argList);
                        break;
                    }
                }
            return args;
        }

GetArgs メソッドは個々の引数の評価と強制のために EvalAndForce メソッドを使う。 ひるがえって,このメソッドが Eval メソッドを呼び出す。 呼出し前後で環境が取り替えられないように Eval の第2引数として false を渡す。

        private object EvalAndForce(object arg, bool willForce) {
            object x = Eval(arg, false);
            if (willForce && x is Promise)
                x = ((Promise) x).Deliver();
            return x;
        }

5.5 組込関数の呼出し

組込関数は,引数の個数により NullaryFunctionNAryFunction (2.3 節) の値として表現される。 典型的には Symbol (2.4 節) テーブル symbol に格納され,Lisp の大域変数として参照される。 Interp クラスのコンストラクタの一部を示す。

        public Interp () {
            symbol = new Dictionary<Symbol, object> ();
            lazy = new HybridDictionary ();
            ……
            symbol[Symbol.Of("read")] = new NullaryFunction (reader.Read);

            NAryFunction list_fn = new NAryFunction (LL.ListFrom);
            symbol[LL.S_LIST] = list_fn;
            lazy[list_fn] = true;
        }

ここではクラス Reader のメソッド Read (3.4 節) と,静的クラス LL の静的メソッド ListFrom (2.6 節) を,それぞれ組込関数 (read)(list E1EN) として使用できるように設定している。 lazy は,実引数が約束だったとき,強制的に評価することをしない 組込関数の集合である。実装上は関数値から bool 値への辞書である。

集合を辞書で代用したのは,.NET Framework 2.0 の範囲内で作ったからです。 .NET Framework 3.5 には HashSet<T> があります。

Eval メソッドは,組込関数 fn の実引数リスト argList への適用を,CallNative メソッドを使って実行する。

                                return CallNative(fn, argList,
                                                  ! lazy.Contains(fn));

CallNative メソッドの定義を示す。 ゼロ引数関数 NullaryFunction から2引数関数 BinaryFunction までは,じかに実引数値を評価して渡す。 NAryFunction には実引数の IList 値を構築して渡す。

        private object CallNative(object fn, Cell argList, bool willForce) {
            try {
                if (fn is NullaryFunction) {
                    if (argList != null)
                        goto ERR_0;
                    return ((NullaryFunction) fn) ();
                } else if (fn is UnaryFunction) {
                    if (argList == null)
                        goto ERR_1;
                    object x = EvalAndForce(argList.car, willForce);
                    if (argList.cdr != null)
                        goto ERR_1;
                    return ((UnaryFunction) fn) (x);
                } else if (fn is BinaryFunction) {
                    if (argList == null)
                        goto ERR_2;
                    object x = EvalAndForce(argList.car, willForce);
                    Cell j = argList.cdr as Cell;
                    if (j == null)
                        goto ERR_2;
                    object y = EvalAndForce(j.car, willForce);
                    if (j.cdr != null)
                        goto ERR_2;
                    return ((BinaryFunction) fn) (x, y);
                } else if (fn is NAryFunction) {
                    IList args = GetArgs(argList, willForce);
                    return ((NAryFunction) fn) (args);
                } else
                    throw new EvalException ("not applicable", fn);
            } catch (EvalException) {
                throw;
            } catch (Exception ex) {
                foreach (Symbol sym in symbol.Keys) {
                    if (symbol[sym] == fn) { // fn に該当するシンボルを探す
                        fn = sym;
                        break;
                    }
                }
                throw new EvalException
                    ((ex.GetType() + ": " + ex.Message + " -- " + fn + " "
                      + LL.Str(argList)), ex);
            }
            ERR_0: throw new EvalException ("no args expected", argList);
            ERR_1: throw new EvalException ("one arg expected", argList);
            ERR_2: throw new EvalException ("two args expected", argList);
        }

例外発生時には,デバッグの便宜のため,関数値 fn に該当する関数名を Symbol テーブルから逆引きして例外メッセージに含める。

したがって,組込関数そのものを Lisp 式として印字した値は C# のデリゲートの ToString() 値になるが (下記の例では L2Lisp.NAryFunction), 例外発生時のメッセージにはその組込関数に束縛されている Lisp の大域変数の名前が現れる (下記の例では /)。

> /
L2Lisp.NAryFunction
> (/ 2 0)
*** System.DivideByZeroException: Division by zero -- / (2 0)
  0: (/ 2 0)
> 

5.6 マクロ式

L2 Lisp は,その初期から ラムダ式 (lambda (ARGS…) BODY…) の同類としてマクロ式 (macro (ARGS…) BODY…) を用意していた。 本処理系は,シンボル macro を先頭要素とするリストの評価結果として,Closure インスタンスによく似た Macro インスタンスを作る。

                        } else if (fn == LL.S_MACRO) {
                            if (environ != null)
                                throw new EvalException ("nested macro", x);
                            ReplaceDummyVariables(ref x);
                            int arity = Compile(ref x);
                            return new Macro (arity, x);

macro に対する Macro インスタンスの構築は,lambda に対する Closure インスタンスの構築 (5.2 節) と,次の2点で異なる。

  1. 環境 environ は空でなければならない。
  2. Compile メソッドの適用前に ReplaceDummyVariables メソッドが適用される。

ラムダ式と異なり,直接または大域変数値として他の式に含まれるマクロ式の適用は,マクロ展開として,Compile メソッドによるコンパイル時に早々と評価される。 ここで微妙な問題が発生することを避けるため, インスタンス構築時 (Lisp プログラマから見ればマクロ定義時) の環境が空である (つまり,トップレベルである) ことを要求する。 環境が空だから,Macro のメンバ変数 env の値は null に固定される。 したがって,マクロ式の本体を評価するとき,静的スコープにより,自由変数として Symbol テーブルに含まれる大域変数にだけアクセスできる。

ReplaceDummyVariables メソッドは 「ダミーシンボルによる変数捕捉の回避」で述べたようなダミー・シンボルを実現する。 ただし,その内部表現については「コンパイル結果の表現方法の変更」に準ずる。

ラムダ式と同様に対話セッションでコンパイル結果を見ることができる。 Lisp マクロとしてはあまり意味がないが,わざと 5.2 節 のラムダ式の例に似せて試行すると,次のようになる。 #<macro> に続く 1 や 2 は引数の個数である。 #<closure> の場合と異なり,環境は印字されない (環境は必ず空だから表示する意味がない)。

> (macro (a) (+ a b))
(#<macro> 1 (+ #0:0:a b))
> (macro (a b) (+ a b))
(#<macro> 2 (+ #0:0:a #0:1:b))
> (macro (a) (macro (x) (+ x a)))
*** nested macro: (macro (x) (+ x #0:0:a))
  0: (macro (a) (macro (x) (+ x a)))
> 

ラムダ式の適用と異なり,マクロ式の適用では引数は評価されない。 評価されないままマクロ式の仮引数と結合し,マクロ式の本体が (Symbol テーブルの大域変数だけを自由変数として) 評価される。 そして,本体の末尾式の評価結果が (今度は適用そのものを取り囲む環境の中で) 再び評価され,適用結果となる。 いくつかの例を示す。

下記は一見するとラムダ式と変わらないように見える。 しかし,実際には 5 が評価結果として得られた後,再び評価されている。 5 を評価した結果は 5 だから,結果としてラムダ式の場合と変わらない。

> ((macro (a b) (+ a b)) 2 3)
5
> 

次の例では,マクロ式本体の評価結果としてリスト (2 3) が得られた後,それが再び評価され,2 が関数ではないとしてエラーになっている。

> ((macro (a b) (list a b)) 2 3)
*** not applicable: 2
  0: (2 3)
> 

次の例では, car(cons 1 2) が評価されずにそのままシンボルと3要素のリストとして マクロ式に渡され,マクロ式本体の評価結果としてリスト (car (cons 1 2)) が得られた後,それが再び評価され,1 が得られている。

> ((macro (a b) (list a b)) car (cons 1 2))
1
> 

Eval メソッドはマクロ式の適用を次のように実現する。

                            } else if (fn is Macro) {
                                IList args = LL.IListFrom(argList);
                                ApplyDefined((Macro) fn, args, false, out x);

argList を評価せずに LL.IListFromIList 値に変換し, Closure の場合と同じように利用者定義関数として適用する。ただし,末尾呼出しのフラグは下げる。 x に得られた評価結果を Eval メソッドの無限ループの中で再び評価する。 LL.IListFrom の実装はごく簡単である。

        public static IList IListFrom(IEnumerable<object> args) {
            if (args == null)
                return new List<object> ();
            else
                return new List<object> (args);
        }

しかし,典型的な用法では,マクロ式の適用は,このように Eval メソッドで評価されるのではなく,Compile メソッドによりコンパイル時に評価される。 この場合,マクロ式本体の評価結果は,いわゆるマクロ展開としてコンパイル結果の一部となり,その場では再評価されない。 こうして得られたクロージャが適用されたとき,マクロ式本体の評価結果が (そのとき,それを取り囲む環境の中で) 再評価される。

したがって健全なマクロ式は,式の中に入れ子になって置かれて Eval メソッドで式の評価時に適用されたとしても,Compile メソッドで式の評価前に適用されたとしても,最終的には同じ結果をもたらす。 Compile メソッドの実装を示す。

        /// <param name="x">(lambda …) または (macro …) を入力とする。
        /// コンパイルして得られた「本体」だけを出力とする
        /// </param>
        /// <returns>lambda 式の arity
        /// </returns>
        private int Compile(ref object x) {
            Debug.Assert(x is Cell);
            Debug.Assert((x as Cell).car == LL.S_LAMBDA ||
                         (x as Cell).car == LL.S_MACRO);
            Cell j = ((Cell) x).cdr as Cell;
            if (j == null)
                throw new EvalException ("arglist and body expected");
            bool has_rest;
            Dictionary<object, Arg> table = MakeArgTable(j.car, out has_rest);
            int arity = table.Count;
            if (has_rest)
                arity = -arity;
            j = j.cdr as Cell;
            if (j == null)
                throw new EvalException ("body expected");
            j = ScanArgs(j, table) as Cell;
            j = ExpandMacros(j, LL.MAX_MACRO_EXPS) as Cell;
            j = CompileInners(j) as Cell;
            x = j;
            return arity;
        }

実際のマクロ展開は ExpandMacros メソッドで行う。 count 引数を使って,入れ子的なマクロ展開の深さを制限する。 制限のため展開しなかったマクロ式の適用は,実行時に Eval メソッドで評価する。 本処理系の count の初期値 LL.MAX_MACRO_EXPS は 20 である。

        private object ExpandMacros(object j, int count) {
            if (count > 0 && j is Cell) {
                Cell jc = (Cell) j;
                object k = jc.car;
                if (k == LL.S_QUOTE || k == LL.S_LAMBDA || k == LL.S_MACRO)
                    return j;
                if (k is Symbol) {
                    object v;
                    if (symbol.TryGetValue((Symbol) k, out v))
                        k = v;
                }
                if (k is Macro) {
                    IList args = LL.IListFrom(jc.cdr as Cell);
                    object z;
                    ApplyDefined((Macro) k, args, false, out z);
                    return ExpandMacros(z, count - 1);
                } else {
                    UnaryFunction fn = (UnaryFunction) delegate (object x) {
                        return ExpandMacros(x, count);
                    };
                    return LL.MapCar(fn, jc);
                }
            } else
                return j;
        }

4.3 節 でみた defmacro, defun, if のマクロ定義を再掲する。

(setq defmacro
      (macro (name args &rest body)
             `(progn (setq ,name (macro ,args ,@body))
                     ',name)))
(defmacro defun (name args &rest body)
  `(progn (setq ,name (lambda ,args ,@body))
          ',name))
(defmacro if (test then &rest else)
  `(cond (,test ,then)
         ,@(cond (else `((t ,@else))))))

大域変数 if に代入されている準引用展開済みの実際のマクロ式を見る。 ここで -3 は,引数の個数,つまり arity が 3 であって,その一つが rest 引数であることを意味する。

> if
(#<macro> -3 (cons 'cond (cons (list #0:0:test #0:1:then) (cond (#0:2:else (list
 (cons 't #0:2:else)))))))
> 

ラムダ式をコンパイルするとき,入れ子で含まれる if の適用がマクロ展開されることを確かめる。

> (lambda (x) (if (= x 0) 'zero 'non-zero))
(#<closure> (1) (cond ((= #0:0:x 0) 'zero) (t 'non-zero)))
> 


目次へ 6.へ


Copyright (c) 2008 OKI Software Co., Ltd.