Lisp
インタープリタの製作インタープリタの代表的な例題として、Lisp(リスプ)言語の簡単なインタープリタ
作って、その仕組みを解説する。
Lisp
とはLisp言語の歴史はFortranの次に古い言語で、1959年にMITのMacCarthyによりAIのための言語として、考案された。その特徴は
特に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では、以下のような制限を加えたものとする。関数定義は以下の関数で行うことにする。
(define 関数名 (パラメータ1 パラメータ2 ....) 式)
例えば、
(define foo (x y) (+ (* x x) (* y y))
は、
xとyを引数として、2乗の和を返す関数fooを定義する。(foo 2 3)
では、まず、パラメータ
xに2、パラメータyに3をセットし、(+ (* x x) (* y y))を実行する。(* x x)は、4、(* y y)は9になって、この関数の値は13となる。この関数の実行後は、xとyのセットした値は元に戻ることに注意。S
式のデータ構造と読み込みまず、S式は以下のデータ構造をつかって表現することにしよう。
typedef struct object {
int kind; /* NUM, SYM, LIST */
int val; /* 数値の時, 値 */
Symbol *sym; /* シンボルの時、*/
struct object *left,*right; /* リストの時*/
} Object;
図は、リストのみの構造を示したものである。実際には、
12やxは、NUMやSYMのkindを持つObjectになる。シンボルは、同じ名前のシンボルを1つのデータで管理するもので、以下のようなデータ構造である。
typedef struct symbol {
char *name;
/* 名前の文字列 */int val; /*
変数の場合の値 */Object *func; /*
関数の場合の定義 */} Symbol;
これをつかって、表
Symbol SymbolTable[]で管理する。S式のデータ構造の変数などのシンボルはこのデータ構造を通じて、同じ名前は同じデータ構造で管理される。関数objectReadは、入力からS式を入力し、データ構造に変換する。文字解析はlexでも記述できるが、lex.cに定義してある。ここに、これらのデータ構造を操作する関数
(object.c)について説明しておく。インタープリタ:変数の扱い
まずは、簡単なインタープリタを作ってみることにする。変数を考えなければ、大体は式の評価でつくったインタープリタと同じである。システムで定義してある四則演算に関しては、あらかじめ
+,-,*,/などの記号に対してはシンボル定義をしておく(関数nitEval)。これで、インタプリターの本体は、
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を返しておくことにしよう。関数呼び出しでは、関数の実行中は
xとyの値を引数の値にしておかなくてはならない。ここで、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)の手順は、この動作を行うのが、関数
callFuncである。関数が再帰的に呼び出される場合にも、最近の結合されたものから探す(つまり、
Envを逆順に探す)ことにより、最も最近に結合された値を参照できることになる。さて、以上のことを統合すると、
これを使えば、階乗を計算する関数
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が返される。つまり、後者の例では、xはaddxyで結合された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)
提出は、