266 lines
6.9 KiB
Python
266 lines
6.9 KiB
Python
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
|