Lisp
インタープリタの拡張前回、簡単なLispインタープリタを代表的な例題として作った。今回は、これを本格的なプログラムが書けるように拡張してみよう。前回のLispでは、以下の機能をつくった。
さて、プログラミング言語として、これ以外に必要な機能は何であろうか。CやJavaなど、近代的なプログラミング言語にはいろいろな機能がある。
このほかにもいろいろあるが、ここで、考えてみる機能は以下の通りである。
while
文に関しては、以下のような定義にしてみることにする。(while 条件式 式)
これについては、インタプリター
evalObjectを使って、条件式を実行し、その値が真(すなわち0でない)になるまで、式を実行すればよい。これについては、今回の課題の1つにする。ブロック文と局所変数
局所変数のあるブロック文(式)については以下の定義としよう。
(block (局所変数1 局所変数2 ...) 式1 式2 式3)
この文では、局所変数をつくり、ならぶ式を順番に実行することにする。
Lispでは式は必ず値を返さなくてならないので、便宜的に最後の式の値を返すことにしよう。式
1,2,3...を実行している間に局所変数が現れた場合には、宣言された局所変数を参照しなくてはならない。しかし、このブロック文が終わった時には、元の値に戻さなくてはならない。つまり、有効範囲(スコープ)を持つ。局所変数に対する処理は基本的には関数のパラメータ変数に対する式と同じである。
block文に現れた局所変数について、現在の環境に登録しておく。パラメータの場合には引数と結合しておいたが、局所変数に関しては何の値でもかまわない(つまり、未定義)。具体的には、このあとで、式
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); }
この例では、
fooのsetjmpでenvに状態を覚えておき、goo1, goo2と呼び出されたときに、goo2でlongjmpをすることよって、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で次に何がくるかを仮定しながら構文解析を行っている。内部について詳しくは解説しないが、だいぶ複雑である。通常、このような
parserはyaccなどのツールを使って作られるのが普通である。次回からは、構文解析の技法の基本とyaccによる構文解析の作り方に進むことにする。さらに、yaccを使ってtiny Cのインタプリターを作ってみる。
演習課題4:
解説したプログラムは、
web上に公開してある。(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)
提出は、