Lispインタープリタの拡張

  前回、簡単なLispインタープリタを代表的な例題として作った。今回は、これを本格的なプログラムが書けるように拡張してみよう。前回のLispでは、以下の機能をつくった。

  1. 組み込みの四則演算子による整数の計算
  2. 変数が使える。変数の代入と参照。
  3. 関数定義ができる。
  4. If文により、条件分岐ができる(課題3)

さて、プログラミング言語として、これ以外に必要な機能は何であろうか。CやJavaなど、近代的なプログラミング言語にはいろいろな機能がある。

このほかにもいろいろあるが、ここで、考えてみる機能は以下の通りである。

  1. while文について考えてみることにする。
  2. 局所変数とブロック文
  3. 文字列と配列
  4. 関数の値を返すreturn

while文に関しては、以下のような定義にしてみることにする。

  (while 条件式 式)

これについては、インタプリターevalObjectを使って、条件式を実行し、その値が真(すなわち0でない)になるまで、式を実行すればよい。これについては、今回の課題の1つにする。

ブロック文と局所変数

局所変数のあるブロック文(式)については以下の定義としよう。

 (block (局所変数1 局所変数2 ...) 1 2 3)

この文では、局所変数をつくり、ならぶ式を順番に実行することにする。Lispでは式は必ず値を返さなくてならないので、便宜的に最後の式の値を返すことにしよう。

 式1,2,3...を実行している間に局所変数が現れた場合には、宣言された局所変数を参照しなくてはならない。しかし、このブロック文が終わった時には、元の値に戻さなくてはならない。つまり、有効範囲(スコープ)を持つ。

 局所変数に対する処理は基本的には関数のパラメータ変数に対する式と同じである。block文に現れた局所変数について、現在の環境に登録しておく。パラメータの場合には引数と結合しておいたが、局所変数に関しては何の値でもかまわない(つまり、未定義)。具体的には、

  1. 局所変数のある部分のリストを取り出す。
  2. 局所変数のリストから、局所変数のシンボルを取り出す。
  3. 環境に登録する。Env[envp++] = 局所変数のシンボル
  4. これを、局所変数の全部に繰り返す。

このあとで、式1,2,...を実行する。この間で、変数が参照される場合には、関数getValue/setValueで、値を参照するためにEnvにある変数の値が参照されることになる。

 式が全部実行しおわったら、envpは元に戻しておく。これによって、局所変数は取り消されることになる。

return

関数の途中からreturnできると便利な場合がある。たとえば、C言語では

 foo(){

if(...) return 100;

...

}

のように、途中でreturn文を実行すると途中から、関数を終了し値を返すことができる。この機能を作ってみることにする。retrun文は以下のように書くことする。

 (return )

式の値を実行中の関数の値として返す。

 インタープリターでは関数本体を実行するときには、evalObjectで再帰的に呼び出しながら、実行している。関数の呼び出しの部分を思い出してみよう。関数callFuncで、まず、引数とパラメータ変数を結合し、本体の式をevalObjectで実行する。その中でさらに、evalObjectが呼び出されて実行が進んでいく。その途中で、return文が実行されたときには、最初のcallFuncのところに戻ってこなくてはならない。

 この動作を行うためにsetjmp/longjmpを使わなくてはならない。setjmp/longjmpは関数の現在の状態を記録しておき、呼び出された先から戻る機能である。例えば、

 #iclude <setjmp.h>

jmp_buf env;

foo() { ... setjmp(env); ... goo1(); ...}

goo1() { ... goo2(); ...}

goo2() { ... longjmp(env,1); }

この例では、foosetjmpenvに状態を覚えておき、goo1, goo2と呼び出されたときに、goo2longjmpをすることよって、setjmpの後に戻ってくる。setjmpでは、はじめにセットしたときに0を、longjmpで戻ってきたときにはlongjmpで指定された値を返す。したがって、longjmpでは第2引数に0以外の値を与える。

 この機能を使ってreturn文を作ってみることにしよう。まず、戻るべき最も最近の状態jmp_bufを覚えておくために、変数funcReturnEnvを使う。

  jmp_buf *funcRetrunEnv;

   ...

  ret_env_save = funcReturnEnv; /* 元の値をとっておく */

funcReturnEnv = &ret_env; /* 今度戻ってくるところにセット */

if(setjmp(ret_env) != 0){ /* longjmp で戻ってきたとき */

val = funcReturnVal; /* returnからの値をとる*/

} else {         /* はじめにセットしたとき,本体を評価*/

val = evalObject(getNth(func_def,1)); /*なにもなければその値 */

}

funcReturnEnv = ret_env_save; /* 前の値に戻す*/

これで、return文のほうは、

  funcReturnVal = evalObject() /* 式を評価 */

longjmp( *funcRetrunEnv,1); /* 最近のsetjmpにかえる!!!*/

とすることで、returnの値を返すことができる。

文字列、配列

文字列や配列の機能がなければ面白くないので、付け加えてみることにする。

 本来のLispでは、実行時にデータ型をチェックしながら実行することができるが、このインタープリターでは、配列や文字列を扱う場合にはその配列や文字列のアドレスを数値として持つことにして作ってみることにした。

 文字列に関しては、readされたところで、文字列を保存し、そのアドレスを数値NUMとして返すことにする。関数printlnは、printf(フォーマット文字列、値)と同じ機能を持つ関数である。これを使えば、

  (println "this is %d" x

として、xの値を出力することができる。

 配列も同じように、変数にアドレスを数値として持つことにする。次の関数をつくってみたので、みてほしい(array.c)

なお、配列は、1次元配列のみである。

 

さて、以上でインタープリタは終わりである。これで、一通りのプログラムが書けるはずである。次に、これをもとにC風の言語を作っていくことにする。

 

tiny C

Lispでは、プログラムは括弧の式で書き、いわば中間表現をそのまま人間が手で書いているようなものであった。これはこれで、Lisp言語ではプログラムをリストのデータ構造として扱えたりして、重要な機能の一部になっているが、やはり、書きにくいし、なれないと読みにくいなどの欠点がある。CやJavaなどほとんどのプログラミング言語ではその表記は読みやすく工夫されている。

 これからは、Cのサブセットのような文法を持つ言語tiny Cを作っていくことにする。これまでつくったLispインタプリターを使ってtiny Cのインタプリターをつくる。この後、この言語のコンパイラを作ることを考える。

では、tiny Cの文法を考えてみることにする。BNF記法で以下のように定義してみる。

<program> := { <function-definition> }*

<function-definition> :=

   <function-name> "(" <parameter-list> ")" <function-body>

<parameter-list> := <> | <variable> { "," <variable> }*

<function-body> := "{" <local-variable-decl> { <statement> }* "}"

<local-variable-decl> := <> | "var" <local-variable-list> ";"

<local-variable-list> := <variable> { "," <variable> }*

<statement> := <assignment-statement> | <function-call-statement> |

<if-statement> | <return-statement>

<assignment-statement> := <varaible> "=" <expression> ";"

<function-call-statement> := <function-name> "(" argument-list ")" ";"

<argument-list> := <> | <expression> { "," <expression> }*

<if-statement> := "if" "(" <expression> ")" <statement> "else" <statement>

<return-statement> := "return" <expression> ";"

<expression> := <expression> <term-op> <term>

<term-op> := "+" | "-" | "<" | ">"

<term> := <variable> | NUMBER | STRING | <function-call-expression>

<function-call-expression> := <fucntion-name> "(" <argument-list> ")"

<function-name> := SYMBOL

<variable> := SYMBOL

<expression>のあたりではだいぶ省略してある。この言語の仕様に従えば、例えば次のようなプログラムを書くことができる。

 main(){ println("foo is %d",foo(10,1); )

foo(x,y){ var t; if(x > y) t = x + y; else t = x - y; return t; }

このプログラムは、Lispでは、

 (define main () (println "foo is %d" (foo 10 1)))

(define foo (x y)

(block (t) (if (> x y) (= t (+ x y)) (= t (- x y)) (return t)))

つまり、構文解析により、上のプログラムを入力し、下のLispの内部データ構造を使えば、そのままインタプリターで実行すればよいことになる。

 この構文解析を前に紹介したtop-down parserで書いたものがclex.c cparser.cである。基本的には、先に数式のparserで解説した作り方で、nextTokenで次に何がくるかを仮定しながら構文解析を行っている。内部について詳しくは解説しないが、だいぶ複雑である。

 通常、このようなparseryaccなどのツールを使って作られるのが普通である。次回からは、構文解析の技法の基本とyaccによる構文解析の作り方に進むことにする。さらに、yaccを使ってtiny Cのインタプリターを作ってみる。

 

演習課題4:

 解説したプログラムは、web上に公開してある。

  1. Lispインタプリターのwhile文の処理を完成させて、次のプログラムを実行しなさい。whileの処理については、意図的に削除してある。提出は実行結果と追加変更した部分のみでよい。
  2. (define main ()

    (block (s i)

    (= s 0)

    (= i 0)

    (while (< i 10)

    (block ()

    (= n (+ n i))

    (= i (+ i 1))))

    (println "sum is %d" n)))

    (main)

  3. tinyCのインタプリターで、課題3のフィボナッチ数fib(10)を計算しなさい。そのために必要な機能があれば、追加すること。tiny Cのコンパイル方法はMakefileにある通り。

提出は、