mal: Step 6¶
- project:
mal-py
- filename:
step6_file.py
read¶
mal-py
step6_file.py
import itertools
from core import ns
from env import Env
from reader import Fn, S, read_str
from printer import print_form
def read(ins):
return read_str(ins)
evaluate¶
To evaluate code we “just” have to switch on the form
mal-py
step6_file.py
def evaluate(form, env: Env):
while True:
match form:
{{ insert(slots['eval'], indent=12) }}
Symbol values are looked up in the environment
mal-pystep6_file.pycase S(name): return env.get(name)
Special case the empty list
mal-pystep6_file.pycase []: return []
Unsurprisingly, a list of terms is where most of the action is
mal-pystep6_file.pycase [*fs]: match fs[0]: {{ insert(slots['special-form'], indent=8) }}
We dispatch on the first item in the list and treat it accordingly.
Finally, everything else (numbers, strings etc.) return as-is
mal-pystep6_file.pycase _: return form
Special forms¶
By definition, each special form is an edge case and needs to be handled separately.
def!¶
def! is how mal spells setq and updates the current environment
mal-py
step6_file.py
case S('def!'):
env.set(fs[1].name, v := evaluate(fs[2], env))
return v
do¶
Equivalent to progn, evals all terms and returns the last one.
mal-py
step6_file.py
case S('do'):
# tco: evaluate() everything except for the final item
for f in fs[1:-1]:
evaluate(f, env)
# tco: update ``form`` to be the final element so that it's evaluated on the
# next iteration
form = fs[-1]
fn*¶
a.k.a lambda.
I’m surprised at how simple conceptually it is.
A function definition:
Creates a new
Envas a child of the current environment, it’s parameters define names that are yet to have values associated with them.When called, these missing values are provided from its arguments and the function then calls evaluate on its body with the new local environment.
mal-py
step6_file.py
case S('fn*'):
def fn(*args):
localenv = Env(outer=env, binds=fs[1], exprs=args)
return evaluate(fs[2], localenv)
# tco: return Fn rather than Python function def
return Fn(params=fs[1], body=fs[2], env=env, f=fn)
Of course, the fact Python already has closures means that it is doing all the heavy lifting!
if¶
mal considers anything that is not false or nil to be true.
Python however, considers other values like 0 to also be False!
This mismatch made implementing if surprinsgly difficult, especially when it likes to coerce 0 to False or False to 0.
The best I have come up with so far is to call print_form on the result and check the string values.
mal-py
step6_file.py
case S('if'):
t = print_form(evaluate(fs[1], env))
if t not in {'false', 'nil'}:
# tco: change ``form`` so that it's evaluated on the next iteration
form = fs[2]
elif len(fs) > 3:
# tco: change ``form`` so that it's evaluated on the next iteration
form = fs[3]
else:
return None
let*¶
let* is used to introduce a new local Env with additional bindings.
mal-py
step6_file.py
case S('let*'):
letenv = Env(env)
for binding, expr in itertools.batched(fs[1], n=2):
letenv.set(binding.name, evaluate(expr, letenv))
# tco: update both ``form`` and ``env`` so that it's evaluated on the next iteration
form = fs[2]
env = letenv
Non-special forms¶
If the first symbol isn’t any of the above, then normal semantics apply i.e. all items are evaluated, the first is assumed to be a function and passed the remainder as arguments.
mal-py
step6_file.py
case _:
f, *args = [evaluate(f, env) for f in fs]
match f:
# tco: handle mal function calls.
case Fn(params, body, fenv, fn):
form=body
env = Env(outer=fenv, binds=params, exprs=args)
case _:
return f(*args)
print¶
mal-py
step6_file.py
def printit(form):
return print_form(form)
REPL¶
rep
mal-py
step6_file.py
def rep(ins: str, env: Env):
try:
return printit(evaluate(read(ins), env))
except EOFError:
return f"EOF"
except KeyError as e:
return f"{e.args[0]!r} not found"
except Exception as e:
print(f"RuntimeError: {e}")
# import pdb; pdb.post_mortem()
repl
mal-py
step6_file.py
def repl(ins: io.TextIO):
env = Env()
for symb, val in ns.items():
env.set(symb, val)
env.set('eval', lambda f: evaluate(f, env))
rep("(def! not (fn* (a) (if a false true)))", env)
rep('(def! load-file (fn* (f) (eval (read-string (str "(do " (slurp f) "\nnil)")))))', env)
print("user> ", end='', flush=True)
while (line := ins.readline()) != "":
print(rep(line, env))
print("user> ", end='', flush=True)
if __name__ == "__main__":
import sys
repl(sys.stdin)