require 'stringio'
module L2Lisp
SUMMARY = '
Little Lambda Lisp 4.3 in Ruby H19.7/19 (鈴)
Ruby 1.8.2 以降 (JRuby 1.0 を含む) による小さな Lisp インタープリタ
これはラムダ算法を Scheme と同程度に近似する Emacs Lisp もどきである。
版数4は Little Lambda Lisp 3 in Standard Pascal の移植・改訂版である
ことを意味する。cf. http://www.okisoft.co.jp/esc/llsp/v3.html
実行例: ruby L2Lisp.rb
上記のように無引数で起動すると対話セッションに入る。引数として
Lisp スクリプトのファイル名を1個以上与えると,それらを順に実行する。
ただし,ハイフン1文字の引数は標準入力と解釈し,対話セッションに入る。
セッション中,本記述は (progn (princ summary) "") で読むことができる。
著作権表示は (progn (princ copyright) "") で読むことができる。
他の Ruby プログラムからライブラリとして利用することもできる。例えば:
require "L2Lisp"
LL = L2Lisp
i = LL::Interp.new # インタープリタ・オブジェクト構築
i.symbol[:str] = proc {|a| a.to_s} # 組込み関数 str 追加
r = i.run(File.new("lisp_script")) # Lisp スクリプト実行
e = LL::Cell.new(:str, LL::Cell.new(123, nil)) # (str 123) 構築
r = i.eval(e) # (str 123) 評価
Lisp 値は次のように表現される:
数, 文字列 ⇒ 数, 文字列
シンボル ⇒ Symbol インスタンス (大域変数値は Interp#symbol に格納)
nil ⇒ nil
cons セル ⇒ Cell インスタンス
特徴:
* 静的スコープ
* 末尾呼出しの最適化
* 関数と変数の名前空間は同一
* シンボルの初期値は (未束縛ではなく) 自分自身
* 関数 / と - は1個以上の引数をとる。
* (eval e) は e を大域的な環境で評価する。
* (eq x y) の結果は Ruby の x == y に従う。
* (delay x) は Scheme と同じく x の約束を作る。
~x と略記できる。組込み関数と条件式は implicit forcing を行う。
* (read) は EOF に対して *eof* の大域変数値を返す。
* 評価時例外 EvalError は (catch *error* …) で捕捉できる。
* (lambda …) を評価すると再帰的に内部形式のラムダ式になる。
* (macro …) は大域的な環境でだけ評価できる。結果はマクロ式である。
マクロ式はラムダ式と同様だが,適用時,引数を評価せず,適用結果を評価する。
* マクロ式で,自由なシンボルは捕捉されないが,マクロ引数は捕捉される。
* (macro …) 内の $ で始まるシンボルは (quote 内も含め) dummy symbol となる。
そのマクロ式の中でだけ eq が成り立つ。(マクロ引数の捕捉防止用)
* (lambda …) を評価する時,最大 MAX_MACRO_EXPS 重だけ再帰的にマクロ展開する
(非大域的に束縛されたマクロを除く)。残りは適用時に処理される。
* 印字する時,高々 MAX_EXPANSIONS 重だけ再帰的に印字済みリストを印字する。
* (dump) は内部状態である symbol キーのリストと字句的環境のペアを返す。
* その他の点では Emacs Lisp のサブセット
'
COPYRIGHT = '
Copyright (c) 2007 Oki Software Co., Ltd.
Permission is hereby granted, free of charge, to any person
obtaining a copy of this software and associated documentation
files (the "Software"), to deal in the Software without
restriction, including without limitation the rights to use,
copy, modify, merge, publish, distribute, sublicense, and/or
sell copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following
conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE.
'
LL = L2Lisp
MAX_EXPANSIONS = 5 MAX_MACRO_EXPS = 20 MAX_EXC_TRACES = 10
LPAREN = :"("; RPAREN = :")"; DOT = :"."; QUOTE = :"'"; TILDE = :"~"
EOF = :"#<eof>"; NONE = :"#<none>"
CLOSURE = :"#<closure>"; LAMBDA = :"#<lambda>"; MACRO = :"#<macro>"
REST = :"&rest"; UNWIND_PROTECT = :"unwind-protect"
class SyntaxError < RuntimeError
end
class EvalError < RuntimeError
attr :trace
def initialize(message, exp=NONE)
super((exp == NONE) ? message : message + ': ' + LL.str(exp))
@trace = []
end
def to_s
s = "*** %s\n" % super
@trace.each_with_index {|t, index| s += "%3d: %s\n" % [index, t]}
return s
end
end
class Thrown < EvalError
attr :tag
attr :value
def initialize(tag, value)
super('no catcher for (%s %s)' % [LL.str(tag), LL.str(value)])
@tag = tag
@value = value
end
end
class VariableExpected < EvalError
def initialize(exp)
super('variable expected', exp)
end
end
class ProperListExpected < EvalError
def initialize(exp)
super('proper list expected', exp)
end
end
class Cell
include Enumerable
attr :car, true
attr :cdr, true
def initialize(car, cdr)
@car = car
@cdr = cdr
end
def inspect
return LL.str(self)
end
def each j = self
begin
yield j.car
j = j.cdr
end while Cell === j
j.nil? or raise ProperListExpected, j
end
def mapcar c = yield @car
x = y = Cell.new(c, nil)
j = @cdr
while Cell === j
c = yield j.car
y = y.cdr = Cell.new(c, nil)
j = j.cdr
end
j.nil? or raise ProperListExpected, j
return x
end
def length
return inject(0) {|x, y| x + 1}
end
def _repr(print_quote, reclevel, printed)
if printed.has_key?(self)
reclevel -= 1
return ['...'] if reclevel == 0
else
printed[self] = true
end
case @cdr
when nil
s = LL.str(@car, print_quote, reclevel, printed)
return [s]
when Cell
s = LL.str(@car, print_quote, reclevel, printed)
t = @cdr._repr(print_quote, reclevel, printed)
return t.unshift(s)
else
s = LL.str(@car, print_quote, reclevel, printed)
t = LL.str(@cdr, print_quote, reclevel, printed)
return [s, '.', t]
end
end
end
def str(x, print_quote=true, reclevel=MAX_EXPANSIONS, printed={})
case x
when Cell
return '(' + x._repr(print_quote, reclevel, printed).join(' ') + ')'
when Symbol then return x.to_s
when String, Exception then return (print_quote) ? x.inspect : x
else return x.inspect
end
end
module_function :str
def mapc(x, &block)
case x
when Cell then x.each(&block)
when nil then ;
else raise ProperListExpected, x
end
end
module_function :mapc
class Arg attr :level
attr :offset
attr :symbol
def initialize(level, offset, symbol)
@level = level
@offset = offset
@symbol = symbol
end
def inspect
return "#%p:%p%p" % [@level, @offset, @symbol]
end
def set_value(x, link)
@level.times {link = link.cdr}
link.car[@offset] = x
end
def get_value(link)
@level.times {link = link.cdr}
return link.car[@offset]
end
end
class Dummy attr :symbol
def initialize(symbol)
@symbol = symbol
end
def inspect
return "%p:%x" % [@symbol, object_id]
end
end
class Promise attr :exp, true
attr :link, true
def initialize(exp, link)
@exp = exp
@link = link
end
def inspect
if @link == NONE
return "#delivered:%p" % @exp
else
return "#promised:%p:%p" % [@link, @exp]
end
end
end
class EagerList < Cell def initialize(list, interp)
super(list.car, list.cdr)
@interp = interp
end
def each
j = self
begin
yield j.car
j = j.cdr
j = @interp.deliver(j) if Promise === j
end while Cell === j
j.nil? or raise ProperListExpected, j
end
def mapcar
c = yield @car
x = y = Cell.new(c, nil)
j = @cdr
j = @interp.deliver(j) if Promise === j
while Cell === j
c = yield j.car
y = y.cdr = Cell.new(c, nil)
j = j.cdr
j = @interp.deliver(j) if Promise === j
end
j.nil? or raise ProperListExpected, j
return x
end
end
class Reader
def initialize(rf)
@rf = rf @buf = [] @line = nil end
def read
begin
_read_token
return _parse_expression
rescue SyntaxError => ex
@buf.clear raise EvalError, 'SyntaxError: %s -- %d: %p' % [ex, @rf.lineno, @line]
end
end
def _parse_expression
case @token
when DOT, RPAREN
raise SyntaxError, 'unexpected: %s' % @token
when LPAREN
_read_token
return _parse_list_body
when QUOTE
_read_token
return Cell.new(:quote, Cell.new(_parse_expression, nil))
when TILDE
_read_token
return Cell.new(:delay, Cell.new(_parse_expression, nil))
else
return @token
end
end
def _parse_list_body
case @token
when EOF then raise SyntaxError, 'unexpected EOF'
when RPAREN then return nil
else
e1 = _parse_expression
_read_token
if @token == DOT
_read_token
(@token != EOF) or raise SyntaxError, 'unexpected EOF'
e2 = _parse_expression
_read_token
(@token == RPAREN) or raise SyntaxError, '")" expected: %s' % @token
else
e2 = _parse_list_body
end
return Cell.new(e1, e2)
end
end
def _read_token
while @buf.empty?
@line = @rf.gets
if @line.nil?
@token = EOF
@rf.close
return
end
@buf = @line.chomp.scan(TOKEN_PAT).flatten.compact
end
case t = @buf.shift
when '(', ')', '.', '\'', '~' then @token = t.to_sym
when 'nil' then @token = nil
when /\A\".*\"\z/ then @token = t[1..-2]
else
begin @token = Integer(t)
rescue ArgumentError
begin @token = Float(t)
rescue ArgumentError
if /\A([A-Za-z0-9]|_|&|\$|\*|\/|\+|-|<|>|=|!|\?)+\z/ === t
@token = t.to_sym
else raise SyntaxError, 'bad token: %p' % t
end
end
end
end
end
TOKEN_PAT = /\s+|;.*$|(".*?"|[^()'~ ]+|.)/
end
class Interp
attr :symbol
def initialize
@reader = Reader.new(STDIN)
@environ = nil
@symbol = {}
def @symbol.inspect
LL.str(keys.inject(nil) {|a, b| Cell.new(b, a)})
end
@symbol[:car] = proc {|x| (x.nil?) ? nil : x.car}
@symbol[:cdr] = proc {|x| (x.nil?) ? nil : x.cdr}
@symbol[:cons] = @cons = proc {|x, y| Cell.new(x, y)}
@symbol[:atom] = proc {|x| (Cell === x) ? nil : :t}
@symbol[:eq] = proc {|x, y| (x == y) ? :t : nil}
@symbol[:prin1] = proc {|x| print(LL.str(x, true)); x}
@symbol[:princ] = proc {|x| print(LL.str(x, false)); x}
@symbol[:terpri] = proc {print("\n"); :t}
@symbol[:read] = proc {@reader.read}
@symbol[:dump] = proc { x = @symbol.keys.inject(nil) {|a, b| Cell.new(b, a)}
Cell.new(x, Cell.new(@environ, nil))
}
@symbol[:assq] = proc {|key, list|
(list.nil?) ? nil :
EagerList.new(list, self).detect {|x|
x = deliver(x) if Promise === x
Cell === x and ((Promise === x.car) ? deliver(x.car) : x.car) == key
}
}
@symbol[:'+'] = proc {|*x| x.inject(0) {|a, b| a + b}}
@symbol[:'*'] = proc {|*x| x.inject(1) {|a, b| a * b}}
@symbol[:'/'] = proc {|x, *y| y.inject(x) {|a, b| a / b}}
@symbol[:'-'] = proc {|x, *y|
(y.length == 0) ? -x : y.inject(x) {|a, b| a - b}
}
@symbol[:'<'] = proc {|x, y| (x < y) ? :t : nil}
@symbol[:eval] = proc {|x|
old_env = @environ
@environ = nil begin eval(x, true) ensure @environ = old_env
end
}
@symbol[:rplaca] = proc {|x, y| x.car = y}
@symbol[:rplacd] = proc {|x, y| x.cdr = y}
@symbol[:throw] = proc {|x, y| raise Thrown.new(x, y)}
@symbol[:stringp] = proc {|x| (String === x) ? :t : nil}
@symbol[:length] = proc {|x|
(x.nil?) ? 0 : ((Cell === x) ? EagerList.new(x, self) : x).length
}
@symbol[:force] = proc {|x| x}
@symbol[:"*eof*"] = EOF
@symbol[:summary] = SUMMARY
@symbol[:copyright] = COPYRIGHT
run(PRELUDE)
end
def eval(x, can_lose_current_env=false)
begin
loop {
case x
when Symbol then return @symbol.fetch(x, x)
when Arg then return x.get_value(@environ)
when Cell
case x.car
when :quote then return x.cdr.car
when :cond
x, cont = eval_cond_body(x.cdr)
return x unless cont
when :setq then return eval_setq_body(x.cdr)
when :lambda then return compile_lambda(x.cdr, true)
when :macro then return compile_lambda(x.cdr, false)
when LAMBDA then return make_closure(CLOSURE, x.cdr)
when CLOSURE, MACRO then return x
when :catch then return eval_catch_body(x.cdr)
when UNWIND_PROTECT then return eval_unwind_protect_body(x.cdr)
when :delay
kdr = x.cdr
(Cell === kdr and kdr.cdr.nil?) or raise EvalError, "bad delay"
return Promise.new(kdr.car, @environ)
else
case kar = x.car
when Symbol then kar = @symbol.fetch(kar, kar)
when Arg then kar = kar.get_value(@environ)
when Cell
case kar.car
when CLOSURE, MACRO then ;
else kar = eval(kar, false)
end
end
case kar
when Cell
case kar.car
when CLOSURE
args = get_args(x.cdr, true)
x, cont = apply_lambda(kar.cdr, args, can_lose_current_env)
return x unless cont
when MACRO
args = get_args(x.cdr, false)
x, cont = apply_lambda(kar.cdr, args, false)
else raise EvalError.new('not applicable list', kar)
end
when Proc
args = ((kar == @cons) ? get_args(x.cdr, true) :
get_delivered_args(x.cdr))
begin
return kar.call(*args)
rescue EvalError
raise
rescue => e
raise EvalError, '%s: %s -- %p %p' % [e.class, e, kar, args]
end
when :apply
args = get_delivered_args(x.cdr)
(args.length >= 2) or raise EvalError, "2 args required"
f, *a = args
a = a.reverse.inject {|rest, first| Cell.new(first, rest)}
if Cell === a
a = EagerList.new(a, self).mapcar {|element|
Cell.new(:quote, Cell.new(element, nil))
}
end
x = Cell.new(f, a)
else raise EvalError.new('not applicable atom', kar)
end
end
else return x
end
}
rescue EvalError => ex
ex.trace << LL.str(x) if ex.trace.length < MAX_EXC_TRACES
raise
end
end
def deliver(promise) unless promise.link == NONE
old_env = @environ
@environ = promise.link
begin
x = eval(promise.exp, true)
x = deliver(x) if Promise === x
ensure
@environ = old_env
end
unless promise.link == NONE promise.exp = x
promise.link = NONE
end
end
return promise.exp
end
def eval_cond_body(body)
while Cell === body case clause = body.car when nil then ;
when Cell
result = eval(clause.car, false)
result = deliver(result) if Promise === result
if result != nil clause = clause.cdr
return result, false unless Cell === clause
while Cell === (d = clause.cdr)
eval(clause.car, false)
clause = d
end
d.nil? or raise ProperListExpected, d
return clause.car, true end
else raise EvalError.new('cond test expected', clause)
end
body = body.cdr
end
body.nil? or raise ProperListExpected, body
return nil, false end
def eval_setq_body(body) result = nil
while Cell === body
lval = body.car
body = body.cdr
(Cell === body) or raise EvalError, 'right value expected'
result = eval(body.car, false)
case lval
when Symbol then @symbol[lval] = result
when Arg then lval.set_value(result, @environ)
else raise VariableExpected, lval
end
body = body.cdr
end
body.nil? or raise ProperListExpected, body
return result
end
def get_args(list, flag)
args = [] while Cell === list
x = list.car
args << (flag ? eval(x, false) : x)
list = list.cdr
end
list.nil? or raise ProperListExpected, list
return args
end
def get_delivered_args(list)
args = []
while Cell === list
x = eval(list.car, false)
args << ((Promise === x) ? deliver(x) : x)
list = list.cdr
end
list.nil? or raise ProperListExpected, list
return args
end
def apply_lambda(j, args, can_lose_original_env)
body = j.cdr
(Cell === body) or raise EvalError, 'body expected'
j = j.car
arity = j.car
link = j.cdr
if arity < 0
arity = -arity -1 if arity <= args.length x = nil
rests = args.slice!(arity .. -1)
rests.reverse_each {|e| x = Cell.new(e, x)}
args << x
arity += 1
end
end
(args.length == arity) or raise EvalError, 'arity not matched'
old_env = @environ @environ = Cell.new(args, link) begin
while Cell === (d = body.cdr)
eval(body.car, false)
body = d
end
if can_lose_original_env then old_env = @environ return body.car, true else
return eval(body.car, true), false
end
ensure
@environ = old_env
end
end
def compile_lambda(kdr, is_lambda)
if is_lambda then x = compile1(LAMBDA, kdr)
else
x = replace_dummy_variables1(kdr, {})
x = compile1(MACRO, x)
end
if is_lambda then return make_closure(CLOSURE, x.cdr)
else
@environ.nil? or raise EvalError.new('nested macro', x)
return make_closure(MACRO, x.cdr)
end
end
def replace_dummy_variables1(j, names)
case j
when Symbol
if j.to_s[0] == ?$
k = names[j]
if k.nil? then names[j] = k = Dummy.new(j) end
return k
else return j
end
when Cell
case j.car
when CLOSURE, LAMBDA, MACRO then return j
else return j.mapcar {|x| replace_dummy_variables1(x, names)}
end
else return j
end
end
def compile1(newkar, j)
(Cell === j) or raise EvalError, 'arglist and body expected'
rest, table = make_arg_table2(j.car)
arity = table.length
arity = -arity if rest
(Cell === j.cdr) or raise EvalError, 'body expected'
body = scan2(j.cdr, table)
body = expand_macros2(body, MAX_MACRO_EXPS)
body = compile_inners2(body)
return Cell.new(newkar, Cell.new(arity, body))
end
def compile_inners2(j)
if Cell === j
case j.car
when :quote, CLOSURE, LAMBDA, MACRO then return j
when :lambda then return compile1(LAMBDA, j.cdr)
when :macro then raise EvalError.new('nested macro', j)
else return j.mapcar {|x| compile_inners2(x)}
end
else return j
end
end
def make_arg_table2(i) offset = 0
rest = false
table = {}
while Cell === i
j = i.car
(not rest) or raise EvalError.new('2nd rest', j)
if j == REST i = i.cdr
(Cell === i) or raise VariableExpected, i
j = i.car
(j != REST) or raise VariableExpected, j
rest = true
end
case j
when Symbol then sym = j
when Arg then sym = j = j.symbol
when Dummy then sym = j.symbol
else raise VariableExpected, j
end
table[j] = Arg.new(0, offset, sym)
offset += 1
i = i.cdr
end
i.nil? or raise ProperListExpected, i
return rest, table
end
def scan2(j, table)
case j
when Symbol, Dummy
k = table[j]
return (k.nil?) ? j : k
when Arg
k = table[j.symbol]
return (k.nil?) ? Arg.new(j.level + 1, j.offset, j.symbol) : k
when Cell
case j.car
when :quote, CLOSURE, LAMBDA, MACRO then return j
else return j.mapcar {|x| scan2(x, table)}
end
else return j
end
end
def expand_macros2(j, count)
if count > 0 and Cell === j
case k = j.car
when :quote, :lambda, :macro, CLOSURE, LAMBDA, MACRO then return j
else
if Symbol === k then k = @symbol.fetch(k, k) end
if Cell === k and k.car == MACRO
args = get_args(j.cdr, false)
z, cont = apply_lambda(k.cdr, args, false)
return expand_macros2(z, count - 1)
else
return j.mapcar {|x| expand_macros2(x, count)}
end
end
else return j
end
end
def make_closure(newkar, j) return Cell.new(newkar, Cell.new(Cell.new(j.car, @environ), j.cdr))
end
def eval_catch_body(j) (Cell === j) or raise EvalError.new('tag and body expected', j)
tag = eval(j.car, false)
begin
result = nil
LL.mapc(j.cdr) {|x| result = eval(x, false)}
return result
rescue Thrown => th
if tag == th.tag then return th.value else raise end
rescue EvalError => ex if tag == :"*error*" then return ex else raise end
end
end
def eval_unwind_protect_body(j) (Cell === j) or raise EvalError.new('body (and cleanup) expected', j)
begin
return eval(j.car, false)
ensure LL.mapc(j.cdr) {|x| eval(x, false)}
end
end
def run(rf, interactive=(rf==STDIN))
rf = StringIO.new(rf) if String === rf
rr = Reader.new(rf)
result = nil
loop {
print '> ' if interactive
begin
x = rr.read
if x == EOF
printf "Goodbye\n" if interactive
return result
end
result = eval(x, false)
puts LL.str(result) if interactive
rescue EvalError => ex
if interactive then print ex else raise end
end
}
end
end
PRELUDE = %q{
(setq list (lambda (&rest x) x))
(setq progn (macro (&rest x) (list cond (cons t x))))
(setq defmacro
(macro (name args &rest body)
(list progn
(list setq name (cons macro (cons args body)))
(list quote name))))
(defmacro defun (name args &rest body)
(list progn
(list setq name (cons lambda (cons args body)))
(list quote name)))
(defun caar (x) (car (car x)))
(defun cadr (x) (car (cdr x)))
(defun cdar (x) (cdr (car x)))
(defun cddr (x) (cdr (cdr x)))
(defun null (x) (eq x nil))
(defun not (x) (eq x nil))
(defun identity (x) x)
(defun print (x) (prin1 x) (terpri) x)
(defun > (x y) (< y x))
(defun >= (x y) (not (< x y)))
(defun <= (x y) (not (< y x)))
(defun = (x y) (eq x y))
(defun /= (x y) (not (= x y)))
(defmacro if (test then &rest else)
(cons cond (cons (list test then)
(cond (else (list (cons t else)))))))
(defmacro let (args &rest body)
((lambda (vars vals)
(defun vars (x)
(if (null x) nil
(cons (if (atom (car x)) (car x) (caar x))
(vars (cdr x)))))
(defun vals (x)
(if (null x) nil
(cons (if (atom (car x)) nil (cadr (car x)))
(vals (cdr x)))))
(cons (cons lambda (cons (vars args) body))
(vals args)))
nil nil))
(defun equal (x y)
(cond ((atom x)
(eq x y))
((equal (car x) (car y))
(equal (cdr x) (cdr y)))))
(defun mapcar (f x)
(if (null x) nil
(cons (f (car x))
(mapcar f (cdr x)))))
(defun _append (x y)
(if (null x) y
(cons (car x) (_append (cdr x) y))))
(defmacro append (x &rest y)
(if (null y) x
(list '_append x (cons 'append y))))
(defmacro and (x &rest y)
(if (null y) x
(list cond
(list x (cons 'and y)))))
(defmacro or (x &rest y)
(if (null y) x
(list cond
(list x)
(list (cons 'or y)))))
(defmacro while (test &rest body)
(list let (quote ($a))
(list setq $a
(list lambda () (list cond (cons test (append body '(($a)))))))
'($a)))
(defun reduce (f x)
(if (null x)
(f)
(let ((r (car x)))
(setq x (cdr x))
(while x
(setq r (f r (car x))
x (cdr x)))
r)))
(defun range (m n) (cond ((< m n)
(cons m ~(range (+ m 1) n)))))
}
end
if __FILE__ == $0 lisp = L2Lisp::Interp.new
if ARGV.empty? then lisp.run(STDIN)
else
ARGV.each {|file_name|
if file_name == '-' then lisp.run(STDIN)
else lisp.run(File.new(file_name))
end
}
end
end