class LoopBreak(Exception): def __init__(self): super().__init__('`break` outside a loop') class LoopContinue(Exception): def __init__(self): super().__init__('`continue` outside a loop') class FuncReturn(Exception): def __init__(self, val): super().__init__('`return` outside a function') self.val = val def parse_expr(s: str, idx: int): idx = skip_space(s, idx) if s[idx] == '(': # a list idx += 1 l = [] while True: idx = skip_space(s, idx) if idx >= len(s): raise Exception('unbalanced parenthesis') if s[idx] == ')': idx += 1 break idx, v = parse_expr(s, idx) l.append(v) return idx, l elif s[idx] == ')': raise Exception('bad parenthesis') else: # an atom start = idx while idx < len(s) and (not s[idx].isspace()) and s[idx] not in '()': idx += 1 if start == idx: raise Exception('empty program') return idx, parse_atom(s[start:idx]) def skip_space(s: str, idx: int): while True: save = idx while idx < len(s) and s[idx].isspace(): idx += 1 if idx < len(s) and s[idx] == ';': idx += 1 while idx < len(s) and s[idx] != '\n': idx += 1 if idx == save: break return idx def parse_atom(s): # TODO: implement this import json try: return ['val', json.loads(s)] except json.JSONDecodeError: return s def pl_parse(s): idx, node = parse_expr(s, 0) idx = skip_space(s, idx) if idx < len(s): raise ValueError('trailing garbage') return node def name_lookup(env, key): while env: current, env = env if key in current: return current raise ValueError('undefined name') def pl_eval(env, node): if not isinstance(node, list): assert isinstance(node, str) return name_lookup(env, node)[node] if len(node) == 0: raise ValueError('empty list') if len(node) == 2 and node[0] == 'val': return node[1] if node[0] == 'var' and len(node) == 3: _, name, val = node scope, _ = env if name in scope: raise ValueError('duplicated name') val = pl_eval(env, val) scope[name] = val return val if node[0] == 'set' and len(node) == 3: _, name, val = node scope = name_lookup(env, name) val = pl_eval(env, val) scope[name] = val return val if node[0] in ('do', 'then', 'else') and len(node) > 1: new_env = (dict(), env) # TODO: Why is Smith using the same variable name for the array items # and their evaluations? Try using a different variable name for the # latter. for val in node[1:]: val = pl_eval(new_env, val) return val import operator binops = { '+': operator.add, '-': operator.sub, '*': operator.mul, '/': operator.truediv, 'eq': operator.eq, 'ne': operator.ne, 'ge': operator.ge, 'gt': operator.gt, 'le': operator.le, 'lt': operator.lt, 'and': operator.and_, 'or': operator.or_, } if len(node) == 3 and node[0] in binops: op = binops[node[0]] return op(pl_eval(env, node[1]), pl_eval(env, node[2])) unops = { '-': operator.neg, 'not': operator.not_, } if len(node) == 2 and node[0] in unops: op = unops[node[0]] return op(pl_eval(env, node[1])) if len(node) in (3, 4) and node[0] in ('?', 'if'): _, cond, yes, *no = node no = no[0] if no else ['val', None] new_env = (dict(), env) if pl_eval(new_env, cond): return pl_eval(new_env, yes) else: return pl_eval(new_env, no) if node[0] == 'loop' and len(node) == 3: _, cond, body = node ret = None while True: new_env = (dict(), env) if not pl_eval(new_env, cond): break try: ret = pl_eval(new_env, body) except LoopBreak: break except LoopContinue: continue return ret if node[0] == 'break' and len(node) == 1: raise LoopBreak if node[0] == 'continue' and len(node) == 1: raise LoopContinue # function definition if node[0] == 'def' and len(node) == 4: _, name, args, body = node # sanity checks for arg_name in args: if not isinstance(arg_name, str): raise ValueError('bad argument name') if len(args) != len(set(args)): raise ValueError('duplicated arguments') # add the function to the scope dct, _ = env key = (name, len(args)) if key in dct: raise ValueError('duplicated function') dct[key] = (args, body, env) return # function call if node[0] == 'call' and len(node) >= 2: _, name, *args = node key = (name, len(args)) fargs, fbody, fenv = name_lookup(env, key)[key] # args new_env = dict() for arg_name, arg_val in zip(fargs, args): new_env[arg_name] = pl_eval(env, arg_val) # call try: return pl_eval((new_env, fenv), fbody) except FuncReturn as ret: return ret.val # return if node[0] == 'return' and len(node) == 1: raise FuncReturn(None) if node[0] == 'return' and len(node) == 2: _, val = node raise FuncReturn(pl_eval(env, val)) if node[0] == 'print': return print(*(pl_eval(env, val) for val in node[1:])) raise ValueError('unknown expression') def pl_parse_prog(s): return pl_parse('(do ' + s + ')') def test_eval(): def f(s): return pl_eval(dict(), pl_parse_prog(s)) assert f('1') == 1 assert f('(+ 1 3)') == 4 assert f('(? (lt 1 3) "yes" "no")') == "yes" assert f('(print 1 2 3)') is None assert f(''' ;; first scope (var a 1) (var b (+ a 1)) ;; a=1, b=2 (do ;; new scope (var a (+ b 5)) ;; name collision (set b (+ a 10)) ) ;; a=1, b=17 (* a b) ''') == 17 assert f(''' (def gauss (n) (if (le n 0) (then 0) (else (+ n (call gauss (- n 1)))))) (call gauss 5) ''') == 5 + 4 + 3 + 2 + 1 assert f(''' (def gauss (n) (do (var r 0) (loop (gt n 0) (do (set r (+ r n)) (set n (- n 1)) )) (return r) )) (call gauss 5) ''') == 5 + 4 + 3 + 2 + 1 test_eval() # Run this file as python `/path/to/pl_comp.py`; should work