Lispインタープリタの製作

  インタープリタの代表的な例題として、Lisp(リスプ)言語の簡単なインタープリタ

作って、その仕組みを解説する。

Lispとは

 Lisp言語の歴史はFortranの次に古い言語で、1959年にMITMacCarthyによりAIのための言語として、考案された。その特徴は

  1. S式による簡単なシンタックス。
  2. ラムダ論理による論理的な意味論
  3. AIのための記号処理ができる。リスト処理ができる。
  4. インタープリタによるinterativeな処理
  5. リストとしてプログラム自信をデータと扱えるメタな言語機能

特に1980年代にはAI言語として注目され、様々な言語に影響を与えた。この後、様々な処理系が開発されたが、現在標準的に使われているのは、common Lispである。その他の例としては、有名なeditorであるemacsはその機能をemacs lispで拡張できるようになっている。

簡単版Lispの仕様

 Lispのプログラムは、S式(Symbolic Expression)と呼ばれる構文で記述する。S式とは、以下のものである。

  (関数名 式 式 式 ...)

関数名は、ユーザが定義した関数でも、処理系であらかじめ定義されているものでもよい。Lispでは、この2つには区別はない。実行は引数の式を評価し、その値を引数として、指定された関数を呼び出す。式は、変数のシンボル、数字、またはS式である。式は、必ず値を持つものとする。

例えば、

  (+ (- x 2) 3)

では、(- x 2)を計算し、その結果と3を引数として、関数+を呼び出す。但し、これに従わない例外が若干ある。例えば、条件分岐を以下のような式で与えることにする。

  (if  条件式 then-式 else-)

この場合は、まず条件式を評価してから、その結果にしたがって、thenまたはelseの部の式を評価する。このifは通常の関数とは違う実行順序が必要である。また、変数の代入を次のような式で行うことにする。

  (= 変数 式)

この式では、変数のほうは評価せずに、式を評価した値を代入する。(評価してしまうと、変数の値になってしまう)

本来のLispでは、数値のほかにリストや記号、文字列などのデータが扱えるが、ここで作るLispでは、以下のような制限を加えたものとする。

  1. プログラムで扱うデータは整数のみ
  2. 条件分岐は、上に挙げたifの式とし、条件は0false、それ以外の値はtrueとして扱う。
  3. 変数の代入は上に挙げた=の式で行う。
  4. システムで用意する関数は、四則演算と>、<などの大小比較のみとする。
  5. リストは扱わないので、CG(ガーベージコレクション)はなし。

関数定義は以下の関数で行うことにする。

  (define 関数名 (パラメータ1 パラメータ2 ....) )

例えば、

(define foo (x y) (+ (* x x) (* y y))

は、xyを引数として、2乗の和を返す関数fooを定義する。

  (foo 2 3)

では、まず、パラメータxに2、パラメータy3をセットし、(+ (* x x) (* y y))を実行する。(* x x)は、4、(* y y)は9になって、この関数の値は13となる。この関数の実行後は、xyのセットした値は元に戻ることに注意。

S式のデータ構造と読み込み

まず、S式は以下のデータ構造をつかって表現することにしよう。

 typedef struct object {

int kind; /* NUM, SYM, LIST */

int val; /* 数値の時, 値 */

Symbol *sym; /* シンボルの時、*/

struct object *left,*right; /* リストの時*/

} Object;

 

 

 

 

 

 

図は、リストのみの構造を示したものである。実際には、12xは、NUMSYMkindを持つObjectになる。

シンボルは、同じ名前のシンボルを1つのデータで管理するもので、以下のようなデータ構造である。

typedef struct symbol {

char *name; /* 名前の文字列 */

int val; /* 変数の場合の値 */

Object *func; /* 関数の場合の定義 */

} Symbol;

これをつかって、表Symbol SymbolTable[]で管理する。S式のデータ構造の変数などのシンボルはこのデータ構造を通じて、同じ名前は同じデータ構造で管理される。関数objectReadは、入力からS式を入力し、データ構造に変換する。文字解析はlexでも記述できるが、lex.cに定義してある。

ここに、これらのデータ構造を操作する関数(object.c)について説明しておく。

インタープリタ:変数の扱い

 まずは、簡単なインタープリタを作ってみることにする。変数を考えなければ、大体は式の評価でつくったインタープリタと同じである。システムで定義してある四則演算に関しては、あらかじめ+,-,*,/などの記号に対してはシンボル定義をしておく(関数nitEval)

  1. まず、オブジェクトが数値NUMであれば、その値を返す。
  2. オブジェクトがリスト(f e1 e2)であれば、まず関数部分にあるシンボルを取り出し、システムで定義されたシンボルであるかを調べ、e1e2を評価し、演算を行う。
  3. さて、シンボルの場合は、変数と解釈する。変数の値は、シンボルテーブルのそれぞれのvalのところにいれておくとすると、オブジェクトpがシンボル(kind==SYM)だったときには、p->sym->valを返せばよい。
  4. 変数の代入(= var e1)ではe1を評価して、その値をp->sym->valにいれておく。varのほうは評価しないことに注意。

これで、インタプリターの本体は、

main(){

initEval()

while(p = objectRead(0)){

    v = evalObject(p);

    printf("value is %d\n",v);

  }

}

というように、readとインタプリタの実行evalObjectを繰り返すのがLispのインタープリタである。

関数の定義と呼び出し

 さて、関数の定義はdefineで行う。リストの先頭がdefineである場合には、引数と本体の部分のリストを定義される関数のシンボルのfuncのところにいれておくことにする。例えば、前のfooの例では、((x y) (+ (* x x) (* y y)) funcにいれておく。

なお、Lispでは式を評価したときには何らかの値を返すが、便宜的に0を返しておくことにしよう。

 関数呼び出しでは、関数の実行中はxyの値を引数の値にしておかなくてはならない。ここで、xやyのvalの部分にいれておくことも考えられるが、実行が終わると元の値にもどしておかなくてはならない。この意味でこれは単なる代入と異なり、このような操作を結合(bind)するという。このためのデータ構造として、結合した変数と値のペアを記録しておくデータ構造を用意する。どの変数がどのような値と結合されているかという状態のことを環境(environment)という。

typedef struct env {

Symbol *var;

int val;

} Environment;

Environment Env[MAX_ENV];

Envは変数と値のペアの配列で、パラメータの変数に値が結合されるごとにこの配列に記録しておく。この配列をどこまで使っているかを示すために、envpという変数を使う。変数の値を探す時には、この表を最近に結合された順に探し、この表で見つかった場合にはその値を使い、ない時にはシンボルテーブルにある値を使えばよい。関数の実行が終わったら、envpの値を元に戻せば結合はなくなる。代入で、変数の値を変える場合もこの表にある場合には、その値を変更しなくてはならない。(関数setValue, getValue

関数呼び出し(foo e1 e2)の手順は、

  1. シンボルfooを取り出し、funcのところから関数定義本体を取り出す。
  2. パラメータ部分(x y)を取り出し、e1を評価し、シンボルxと結合する。次に、e2を評価し、シンボルyと結合する。ここでは、Envにセットするだけで、envpは変えない。
  3. 引数の評価が終わったら、結合したところまで、envpを移動させる。これによって、xやyの値はenvにある値になる。
  4. 本体部分(関数定義の2つめの要素)を取り出し、評価する。その値を返す。
  5. 返す前に、envpの値を元にもどし、結合を解く。

この動作を行うのが、関数callFuncである。

 関数が再帰的に呼び出される場合にも、最近の結合されたものから探す(つまり、Envを逆順に探す)ことにより、最も最近に結合された値を参照できることになる。

 さて、以上のことを統合すると、

  1. まず、オブジェクトが数値NUMであれば、その値を返す。
  2. オブジェクトがリスト(f e1 e2)であれば、まず関数部分にあるシンボルを取り出し、システムで定義されたシンボルであるかを調べ、e1e2を評価し、演算を行う。
  3. オブジェクト が定義されたシンボルでなければ、関数定義であるので、callFuncを使って、関数呼び出しを行う。
  4. シンボルの場合は、変数と解釈する。変数の値は、getValueを使って、結合されている値を返す。
  5. 変数の代入(= var e1)ではe1を評価して、setValueを使って、結合されている値を変更する。
  6. もう一つ、(if cond e1 e2) を追加してみる。この場合には、condを評価し、cond0でなければ、e1を評価し、それ以外であれば、e2を評価して返す。

これを使えば、階乗を計算する関数facが以下のように定義できる。

 (defun fac (n) (if n (* (fac (- n 1)) n) 1))

(fac 10)

とすれば、10の階乗が計算されるはずである。

動的束縛

このような環境の作り方はCなどの言語とはちょっと異なる。例えば、

(define addx (y) (+ x y))

という関数があった場合、

(= x 10) (addx 2)

では、答えが 12になる。xは関数の外側のxが参照される。しかし、

(define addxy (x y) (addx y))

(= x 10) (addxy 3 2)

とすると、5が返される。つまり、後者の例では、xaddxyで結合されたxが参照される。つまり、どのような順番で呼び出されるかに依存してしまう。このような実現の方法を動的結合(dynamic binding)と呼ぶ(動的束縛と呼ぶこともある)。Cなどの言語との違いを考えてみよ。

 

演習課題3:

 解説したプログラムは、web上に公開してある。これを使って、フィボナッチ数fib(10)を計算しなさい。ifの部分については、意図的に削除してあるので追加すること。また、必要な組み込み関数があれば、eval.cに追加すること。フィボナッチ数fib(n)の定義は、以下の通り。

      fib(0) = 0, fib(1)=1, fib(n) = fib(n-1)+fib(n-2)

提出は、