PythonでLISPっぽい何かを作ってみた

Pythonでのインタプリタのコードは、本体は200行程度で、builtinで100行程度の300行程度です。一番やりたかったのはmacro機構を実装してみたかったこと。

一般のデータに対しては、整数以外の関数は組み込んでないのですが、以下のようなコード例を解釈できます。

; arith
(say (* (+ 1 2) 10)) ; => 30

; cell
(say (cons 1 nil)) ; => (1)
(say (head (cons 1 nil))) ; => 1

; set
(set (quote hoge) 10)
(say (+ 10 hoge)) ; => 20

; lambda
(say ((lambda (a b) (* (+ a b) a)) 10 5)) ; => 150

; macros
(set (quote =) (macro (var value)
  ; `(set (quote ,var) ,value)
  (list (quote set) (list (quote quote) var) value)))

(= foo 5)
(say foo) ; => 5

(= if (macro (nil? is_not_nil is_nil)
  ; `(cond ,nil? ,is_not_nil ,is_nil)
  (list (quote cond) nil? is_not_nil is_nil)))

(if 1 (say 1) (say 0)) ; => 1
(if nil (say 1) (say 0)) ; => 0


(= fun lambda)
(= \ fun)
(= let (macro ((name value) body)
  ; `((fun (,name) ,body) ,value)
  (list (list (quote fun) (list name) body) value)))
(say (let (foo 30) (+ foo 10))) ; => 40

(= let* (macro args
               (if (tail args) ; (one-more? args)
                   ; `(let ,(head args) (let* ,@(tail args)))
                   (list (quote let) (head args)
                                     (cons (quote let*) (tail args)))
                   (head args))))
(say (let* (foo 10)
           (bar (+ foo 20))
           bar)) ; => 30


(= fact (\ (n) (if (== n 0) 1 (* n (fact (- n 1))))))
(say (fact 5)) ; => 120

; prototype based object system
(= newCount
   (ctor (\ (num)
            (do
             (self:= add (\ (other) (+ num other)))
             (self:= append (\ (other) (= num (+ num other))))
             (self:= get (\ () num))
             self))))

(= count (newCount 5))
; (say count) ; => <Scope>
(say (count:add 10)) ; => 15
(count:append 20)
(say (count:get)) ; => 25

condやdo、ファーストクラスmacroあたりはArcの表現を借りてます。あとsetはシンボルに対して環境に変数スロットがある風に値を更新するのではなく、直接環境に値をセットしていて、単一名前空間のレキシカルスコープにしてるので、この辺もArcっぽいかもしれない。

あと、おまけに、環境をオブジェクトにできるprototypeベースのオブジェクトシステムも組み込んでみました。メソッド呼び出しの基本構文((bind obj method) ...)を(obj:method ...)と記述できるようにしてもあります。

インタプリタpythonで記述してあるけれど、今回Python独自の言語機能はほとんど使用しませんでした。

# LISP like language: lexical, single namespace, symbol based, S-exp only

# cell and symbol
class Memory(object):
    def __repr__(self):
        return self.show()
    def is_nil(self):
        return False
    pass

class Cons(Memory):
    def __init__(self, head=None, tail=None):
        self.head = head
        self.tail = tail
        pass
    def show_inner(self):
        heads = "()" if self.head is None else self.head.show()
        if self.tail is None: return heads
        tails = self.tail.show_inner()
        return "%s %s" % (heads, tails)
    def show(self):
        return "(%s)" % self.show_inner()

def list2cons(objlist, last=None):
    ctxt = last
    rev = list(xrange(len(objlist)))
    rev.reverse()
    for i in rev:
        ctxt = Cons(objlist[i], ctxt)
        pass
    return ctxt

class Atom(Memory):
    def show_inner(self):
        return ". %s" % self.show()
    pass

class Symbol(Atom):
    def __init__(self, name):
        self.name = name
        pass
    def show(self):
        return self.name
    pass

# parser
def parse(code):
    return Parser().parse(code)

import re
class Parser(object):
    def parse(self, code):
        objs, code = self.parseCodes(code)
        s, code = self.parseSpaces(code)
        if code == "": return objs
        return None
    def parseSpaces(self, code):
        match = re.match(r"^\s*(;[^\n]*\n\s*)*", code)
        return code[match.start():match.end()], code[match.end():]

    def parseSymbol(self, code):
        match = re.match(r"^[^(),\s]+", code)
        if not match: return None
        return Symbol(code[match.start():match.end()]), code[match.end():]

    def parseList(self, code):
        if code == "": return None
        if code[0] != "(": return None
        objs, code = self.parseCodes(code[1:])
        s, code = self.parseSpaces(code)
        if code[0] == ")": return list2cons(objs), code[1:]
        if code[0] != ".": return None
        last, code = self.parseCode(code[1:])
        s, code = self.parseSpaces(code)
        if code[0] != ")": return None
        return list2cons(objs, last), code[1:]

    def parseCode(self, code):
        s, code = self.parseSpaces(code)
        ret = self.parseSymbol(code)
        if ret: return ret
        return self.parseList(code)

    def parseCodes(self, code):
        objs = []
        while True:
            if code == "": break
            ret = self.parseCode(code)
            if not ret: break
            obj, code = ret
            objs.append(obj)
            pass
        return objs, code
    pass

# runtime evaluator
class Scope(Atom):
    def __init__(self, parent=None):
        self.dict = {}
        self.dict["parent"] = parent
        pass
    def caller(self):
        return self.dict["parent"] if "parent" in self.dict else None
    def get(self, name):
        if name in self.dict: return self.dict[name]
        if self.caller(): return self.caller().get(name)
        return None
    def put(self, name, value):
        self.dict[name] = value
        return value
    def update(self, name, value):
        if name in self.dict:
            self.dict[name] = value
            return value
        if self.caller(): return self.caller().update(name, value)
        return None
    def show(self):
        return "<Scope>"
    def eval(self, obj):
        if isinstance(obj, Symbol): return self.get(obj.name)
        if isinstance(obj, Cons):
            func = self.eval(obj.head)
            return func.call(obj.tail, self)
        return obj
    pass


# for misc func
class Callable(Atom):
    def __init__(self, callable):
        self.callable = callable
        pass
    def show(self):
        return repr(self.callable)
    def call(self, cons, env):
        return self.callable(cons, env)
    def apply(self, scope, cons, env):
        return self.call(cons, env)
    pass

# Base Language Objects with unboxed call
class Value(Atom):
    def __init__(self, value):
        self.value = value
        pass
    def show(self):
        return repr(self.value)
    def call(self, cons, env):
        args = []
        while cons is not None:
            arg = env.eval(cons.head)
            args.append(arg.value) # unbox only Value
            cons = cons.tail
            pass
        return Value(self.value(*args))
    def apply(self, scope, cons, env):
        return self.call(cons, env)
    def is_nil(self):
        return self.value is False or self.value is None
    pass

def eval_list(cons, env):
    if cons is None: return None
    head = env.eval(cons.head)
    return Cons(head, eval_list(cons.tail, env))

def match_pattern(scope, pattern, cons):
    if isinstance(pattern, Symbol):
        scope.put(pattern.name, cons)
        return
    elif isinstance(pattern, Cons):
        match_pattern(scope, pattern.head, cons.head)
        match_pattern(scope, pattern.tail, cons.tail)
        return
    return

# (lambda (a1 a2 ...) body)
class Lambda(Atom):
    def __init__(self, cons, env):
        self.cons = cons
        self.env = env
        pass
    def show(self):
        return Cons(Symbol("lambda"), self.cons).show()
    def call(self, cons, env):
        scope = Scope(self.env)
        return self.apply(scope, cons, env)
    def apply(self, scope, cons, env):
        cons = eval_list(cons, env)
        args = self.cons.head
        match_pattern(scope, args, cons)
        body = self.cons.tail.head
        return scope.eval(body)
    pass

# (macro (a1 a2 ...) gencode)
class Macro(Atom):
    def __init__(self, cons, env):
        self.cons = cons
        self.env = env
        pass
    def show(self):
        return Cons(Symbol("lambda"), self.cons).show()
    def call(self, cons, env):
        expanded = self.expand(cons, env)
        # print expanded
        return env.eval(expanded)
    def apply(self, scope, cons, env):
        expanded = self.expand(cons, env)
        return scope.eval(expanded)
    def expand(self, cons, env):
        scope = Scope(self.env)
        args = self.cons.head
        body = self.cons.tail.head
        match_pattern(scope, args, cons)
        expanded = scope.eval(body)
        return expanded
    pass

# builtins
import sys
class Builtins(Scope):
    def __init__(self):
        Scope.__init__(self)
        # basic
        self.put("lambda", Callable(Lambda))
        self.put("macro", Callable(Macro))
        self.put("nil", None)
        self.put("quote", Callable(lambda cons, env: cons.head))
        def listFunc(cons, env):
            if cons is None: return None
            head = env.eval(cons.head)
            tail = listFunc(cons.tail, env)
            return Cons(head, tail)
        self.put("list", Callable(listFunc))
        def doFunc(cons, env):
            head = env.eval(cons.head)
            if cons.tail is None: return head
            return doFunc(cons.tail, env)
        self.put("do", Callable(doFunc))
        def headFunc(cons, env):
            data = env.eval(cons.head)
            return data.head
        self.put("head", Callable(headFunc))
        def tailFunc(cons, env):
            data = env.eval(cons.head)
            return data.tail
        self.put("tail", Callable(tailFunc))
        def consFunc(cons, env):
            head = env.eval(cons.head)
            tail = env.eval(cons.tail.head)
            return Cons(head, tail)
        self.put("cons", Callable(consFunc))
        def condFunc(cons, env):
            last = env.eval(cons.head)
            if cons.tail is None: return last
            if last is not None and not last.is_nil():
                return env.eval(cons.tail.head)
            return condFunc(cons.tail.tail, env)
        self.put("cond", Callable(condFunc))
        def setFunc(cons, env):
            head = env.eval(cons.head)
            tail = env.eval(cons.tail.head)
            return env.put(head.name, tail)
        self.put("set", Callable(setFunc))

        # object
        def ctorFunc(cons, env):
            ctor = env.eval(cons.head)
            def create(cons, cenv):
                obj = Scope(env)
                obj.put("self", obj)
                ctor.apply(obj, cons, cenv)
                return obj
            return Callable(create)
        self.put("ctor", Callable(ctorFunc))
        def newFunc(cons, env):
            obj = Scope(env)
            obj.put("self", obj)
            return obj
        self.put("new", Callable(newFunc))
        def bindFunc(cons, env):
            target = env.eval(cons.head)
            #print cons.tail.head
            method = target.eval(cons.tail.head)
            def bound(cons, env):
                return method.apply(target, cons, env)
            return Callable(bound)
        self.put("bind", Callable(bindFunc))

        # system
        def evalFunc(cons, env):
            data = env.eval(cons.head)
            return env.eval(data)
        self.put("eval", Callable(evalFunc))
        def parseFunc(cons, env):
            code = env.eval(cons.head)
            return list2cons(parse(code.value))
        self.put("parse", Callable(parseFunc))
        def sayFunc(cons, env):
            result = env.eval(cons.head)
            show = "nil" if result is None else result.show()
            sys.stdout.write(show + "\n")
            return result
        self.put("say", Callable(sayFunc))
        def writeFunc(cons, env):
            result = env.eval(cons.head)
            sys.stdout.write(result.value)
            return result
        self.put("write", Callable(writeFunc))
        def readFunc(cons, env):
            return Value(sys.stdin.readline().strip())
        self.put("read", Callable(readFunc))

        # operators
        self.put("+", Value(lambda a, b: a + b))
        self.put("-", Value(lambda a, b: a - b))
        self.put("*", Value(lambda a, b: a * b))
        self.put("/", Value(lambda a, b: a / b))
        self.put("%", Value(lambda a, b: a % b))
        self.put("==", Value(lambda a, b: a == b))
        pass
    def get(self, name):
        if re.match(r"^-?[\d]+$", name): return Value(int(name)) # literal
        match = re.match(r"^([^:]+):([^:]+)$", name)
        if match:
            recv, name = match.groups()
            pair = Cons(Symbol(recv), Cons(Symbol(name), None))
            bind = self.get("bind")
            def bound(cons, env):
                method = bind.call(pair, env)
                return method.call(cons, env)
            return Callable(bound)
        return Scope.get(self, name)
    pass

if __name__ == "__main__":
    filename = sys.argv[1]
    code = open(filename).read()
    datalist = parse(code)
    # print datalist
    env = Builtins()
    for data in datalist:
        env.eval(data)
        pass
    pass