自分の勉強がてら簡単な Lisp 処理系を書いたので、その内容をメモとして整理しておく(コードはこちら)。
Lisp 処理系は大きく3つの要素に大別できる。
これらのそれぞれについて、以下で説明する。
Lisp のデータ型は、親子関係を持っている。 今回はシンプルな構成を目指して、次のようにする。
S式(Sexp) │ ├──────────┬──┐ アトム リスト (関数) (Atom) (List) ├────┬──┐ │ シンボル 数 t │ (Sym) (Number) (t) │ │ │ │ │ 整数(Int) │ └──────────nil(nil)ただし関数については手抜きをする予定(後述)。
S式(Sexp)とアトム(Atom)は、それ自体では具体的なデータを持たず、他のデータ型の親になるだけである。 よって、定義はあっさりとすませる。
module Sexp end class Atom include Sexp end
Lispでは下図のようにセルを繋いでリストを作る。
┌──┬──┐┌──┬──┐ │ │ ─┼┼→ │ ─┼→ nil └──┴──┘└──┴──┘ car cdr car cdr
したがって、car, cdr の読み書きができれば、基本部分は完成。
class List def initialize @car = nil @cdr = $nil_sexp end def car return @car end def cdr return @cdr end def set_car(v) @car = v end def set_cdr(v) @cdr = v end def print_sexp ls = self print "(" while true if (ls == $nil_sexp) break end ls.car.print_sexp print " " ls = ls.cdr end print ")" end end
car, cdr の読み書き以外にはデータ表示の処理も必要(print_sexp メソッド)。 処理としてはセルの cdr をたどりながら car 部の内容を表示する。 car 部がリストの場合は、再帰呼び出しで処理。
シンボルが最低限満たす必要がある条件は次の2つ。
したがって、シンボルが持てる値を1個だけに絞ってしまえば、ruby のハッシュそのままで表現できる。
class SymbolTable def initialize @hash = Hash.new end def set(name, value) @hash[name] = value end def get(name) if (@hash.has_key?(name)) return @hash[name]; else return $nil_sexp end end def remove(name) @hash.delete(name) end end
シンボル自体には、名前と値の読み書き、同値比較を実装する。
class Sym < Atom def initialize(name) @name = name @value = $nil_sexp end def set_value(value) @value = value end def print_sexp print @name end def name return @name end def value return @value end def equal(obj) if (symbolp(obj)) return (obj.name == @name) else return false end end end
値の読み書き、同値比較を実装する。
class Number < Atom end class Int < Number def initialize(i) @value = i end def print_sexp print "#{@value}" end def value @value end def equal(obj) if (numberp(obj)) return (obj.value == @value) else return false end end end
関数定義は、定義している文字列をそのままシンボルテーブルに登録してしまう。 シンボルに値を登録する時とほぼ同じ扱い。
ただし、このように実装してしまうと無名関数は使えなくなってしまう。 本来は型システムにちゃんと関数を組込む必要があるが、字句解析で手抜きをするため、今回の実装はこのようにした。
def func_define(ls) sym = ls.car val = ls.cdr.car if (atomp(val)) $symtable.set(sym.name, val) return val else $symtable.set(sym.name, val) return sym end end
システム全体でユニークなオブジェクトでなければならないのでシングルトンにする。 短いコードでそこまでする必要はないと思うけど。
class Nil < Atom include Singleton include Sexp def print_sexp print "nil" end def equal(obj) if (obj == $nil_sexp) return true else return false end end end class T < Atom include Singleton def print_sexp print "t" end def equal(obj) if (obj == $t_sexp) return true else return false end end end
データ型を持つ処理系の常として、あるデータがどの型であるのかを知る方法は必須である。 さもないと、異なる型のデータを比較しようとした際などに処理が破綻してしまう。
型システムは ruby のクラスを使って組み立てたので、型判定には ruby のクラス判定メソッドを使えばよい。
def atomp(sexp) return (sexp.instance_of?(Atom) || sexp.kind_of?(Atom)) end def symbolp(sexp) return sexp.instance_of?(Sym) end def numberp(sexp) return sexp.kind_of?(Number) end def listp(sexp) return sexp.instance_of?(List) end
楽をするために次の手抜きをしている。
class Line def initialize(str) @str = str @idx = 0 @prev_idx = 0 end def unget_token @idx = @prev_idx end def get_token @prev_idx = @idx result = "" if (@idx == @str.length) return nil end while true ch = @str.slice(@idx, 1) if (ch != " ") break end @idx = @idx + 1 end while true if (@idx == @str.length) break end ch = @str.slice(@idx, 1) if (ch == ")" || ch == "(" || ch == " ") @idx = @idx + 1 if (result.length > 0) @idx = @idx - 1 if ch == ")" return result else return ch end end result = result + ch @idx = @idx + 1 end return result end end def get_sexp(l) while t = l.get_token if (t == "(") return get_list(l) elsif (t =~ /^[0-9]+$/) return get_number(t) elsif (t == "t") return $t_sexp elsif (t == "nil") return $nil_sexp else return get_symbol(t) end end return $nil_sexp end def get_list(line) t = line.get_token if (t == ")") return $nil_sexp end top = List.new ls = top line.unget_token ls.set_car(get_sexp(line)) while true t = line.get_token if (t == nil) break end if (t == ")") return top end new_cell = List.new line.unget_token new_cell.set_car(get_sexp(line)) ls.set_cdr(new_cell) ls = new_cell end return $nil_sexp end def get_symbol(token) return Sym.new(token) end def get_number(token) return Int.new(token.to_i) end
考え方は簡単。 アトムであれば自身が持っている値を返し、リストであれば関数と見なして評価するだけ。
def eval_sexp(sexp) if (numberp(sexp)) return sexp elsif (sexp == $t_sexp || sexp == $nil_sexp) return sexp elsif (symbolp(sexp)) return $symtable.get(sexp.name) elsif (listp(sexp)) return funcall(sexp) else print "unexpected error.\n" sexp.print_sexp end end
関数の評価は次の手順で行う。
def funcall(ls) car = ls.car cdr = ls.cdr func = $symtable.get(car.name) if (func == $nil_sexp) print "#{car.name} is not a function.\n" elsif (listp(func)) lmdls = func.cdr.car body = func.cdr.cdr.car return eval_lambda(lmdls, body, cdr) else return func.call(cdr) end end
ユーザ定義の関数については引数の処理が必要になる。 関数に与えられた引数は、関数内だけで有効なシンボルとして扱われ、関数の呼び出し時に与えられた値が登録される。 この処理を束縛という。
束縛の実現方法はいくつかあるが、今回はいわゆる「浅い束縛」方式で実現する。 その理由は、シンボルテーブルを管理しているハッシュとの相性がよいため。 処理の手順は下記のようになる。
def eval_lambda(lmdls, body, args) env = Hash.new l = lmdls a = args while (l != $nil_sexp) key = l.car.name val = eval_sexp(a.car) env[key] = $symtable.get(key) $symtable.set(key, val) l = l.cdr a = a.cdr end ret = eval_sexp(body) env.each_pair do |key, value| $symtable.set(key, value) end return ret end
引数がリストであることを確認した上で、car/cdr 部をそれぞれ返す。
def func_car(ls) if (listp(ls)) arg = eval_sexp(ls.car) return eval_sexp(arg.car) else print "Invalid argument: car\n" end end def func_cdr(ls) if (listp(ls)) arg = eval_sexp(ls.car) return arg.cdr else print "Invalid argument: cdr\n" end end
新しいセルを生成し、car部/cdr部に引数をセットする。
def func_cons(ls) ln = List.new car = eval_sexp(ls.car) ln.set_car(car) cadr = eval_sexp(ls.cdr.car) ln.set_cdr(cadr) return ln end
型チェックのメソッドを呼び出して、atom かどうかを確認する。
def func_atom(ls) if (atomp(ls.car)) return $t_sexp else return $nil_sexp end end
同値比較の処理は、それぞれのデータ型(を定義するクラス)が持っているので、それらを呼び出す。
def func_eq(ls) ref = eval_sexp(ls.car) cdr = ls.cdr while (cdr != $nil_sexp) val = eval_sexp(cdr.car) unless (ref.equal(val)) return $nil_sexp end cdr = cdr.cdr end return $t_sexp end
if, quote, +, -, * を用意しているが詳細は略。 これらはたとえなかったとしても、pure Lisp として成立する!
def func_if(ls) cond = ls.car tform = ls.cdr.car if (ls.cdr.cdr == $nil_sexp) fform = $nil_sexp else fform = ls.cdr.cdr.car end if (eval_sexp(cond) == $t_sexp) return eval_sexp(tform) else return eval_sexp(fform) end end def func_quote(ls) if (listp(ls)) return ls else print "Invalid argument: quote\n" end end def func_plus(ls) if (ls == $nil_sexp) return Int.new(0) else return Int.new(eval_sexp(ls.car).value + func_plus(ls.cdr).value) end end def func_minus(ls) if (ls == $nil_sexp) return Int.new(0) else return Int.new(eval_sexp(ls.car).value - func_plus(ls.cdr).value) end end def func_multiply(ls) if (ls == $nil_sexp) return Int.new(1) else return Int.new(eval_sexp(ls.car).value * func_multiply(ls.cdr).value) end end
以上で必要なパーツはすべて揃ったので、後は「読み込み、評価、結果表示」をひたすら繰り返せばよい。 初期化を含めた具体的な処理は下記のようになる。
print "initializing..." $t_sexp = T.instance $nil_sexp = Nil.instance $symtable = SymbolTable.new $symtable.set("car", self.method(:func_car)) $symtable.set("cdr", self.method(:func_cdr)) $symtable.set("cons", self.method(:func_cons)) $symtable.set("atom", self.method(:func_atom)) $symtable.set("eq", self.method(:func_eq)) $symtable.set("if", self.method(:func_if)) $symtable.set("define", self.method(:func_define)) $symtable.set("quote", self.method(:func_quote)) $symtable.set("+", self.method(:func_plus)) $symtable.set("-", self.method(:func_minus)) $symtable.set("*", self.method(:func_multiply)) f = open("init.l") while l = f.gets l = l.chomp line = Line.new(l) s = get_sexp(line) eval_sexp(s) end f.close print "done.\n" while l = gets l = l.chomp line = Line.new(l) s = get_sexp(line) break if (symbolp(s) && s.name == "quit") v = eval_sexp(s) print "=> " if (listp(v)) v.print_sexp elsif (numberp(v)) print "#{v.value}" elsif (v == $nil_sexp) print "nil" elsif (v == $t_sexp) print "t" elsif (symbolp(v)) print v.name else print v.print_sexp end print "\n" end
階乗を計算する関数 fact を定義し、実行した例。
$ ./rlisp.rb initializing...done. (define fact (lambda (n) (if (eq n 0) 1 (* n (fact (- n 1)))))) => fact (fact 5) => 120 quit $
再帰呼び出しもうまく動作している。