2020-07-21 18:01:48 +02:00
|
|
|
import functools
|
2019-08-18 10:05:48 -07:00
|
|
|
import sys
|
python.2: fix tests, use source checkers and more python3 features
Check the whole code with flake8, pylint and mypy.
Report all possible errors with extensive context.
Demonstrate iterators, decorators, functional tools, chain maps,
dataclasses, match statements, assignments expressions.
Implement environments with python chain maps.
Rewrite the reader without external dependencies (but inspired by the
ptk library). The motivation was that no external library is fully
type-checked for now.
Avoid name clashes when possible (print, read, readline, types).
Write the readline history file at exit, not after each prompt.
Replace printer.pr_str as methods of MAL objects. This is idiomatic
python, and improves the error reporting.
Change some representations so that the python equality matches the
MAL equality. The recursion is now implicit.
Remove -O from ./run. It took me a while to understand that run-time
assertions were disabled! MAL is about development, not performance.
Dispatch the special forms from a dict, for readability (pylint
rightfully complains that there are too many return statements in
eval_()).
Copy the recursion overflow fix, the python interaction from the first
python implementation.
Add tests detecting that nil false 0 () [] "" are distinct, and that 0
() are not False when tested (in python False == 0 and an empty
container is tested ).
Add tests checking that metadata does not affect equality (they do
with a naive python dataclass).
2024-08-22 06:56:10 +02:00
|
|
|
import traceback
|
|
|
|
|
from collections.abc import Sequence
|
2019-08-18 10:05:48 -07:00
|
|
|
|
|
|
|
|
import core
|
python.2: fix tests, use source checkers and more python3 features
Check the whole code with flake8, pylint and mypy.
Report all possible errors with extensive context.
Demonstrate iterators, decorators, functional tools, chain maps,
dataclasses, match statements, assignments expressions.
Implement environments with python chain maps.
Rewrite the reader without external dependencies (but inspired by the
ptk library). The motivation was that no external library is fully
type-checked for now.
Avoid name clashes when possible (print, read, readline, types).
Write the readline history file at exit, not after each prompt.
Replace printer.pr_str as methods of MAL objects. This is idiomatic
python, and improves the error reporting.
Change some representations so that the python equality matches the
MAL equality. The recursion is now implicit.
Remove -O from ./run. It took me a while to understand that run-time
assertions were disabled! MAL is about development, not performance.
Dispatch the special forms from a dict, for readability (pylint
rightfully complains that there are too many return statements in
eval_()).
Copy the recursion overflow fix, the python interaction from the first
python implementation.
Add tests detecting that nil false 0 () [] "" are distinct, and that 0
() are not False when tested (in python False == 0 and an empty
container is tested ).
Add tests checking that metadata does not affect equality (they do
with a naive python dataclass).
2024-08-22 06:56:10 +02:00
|
|
|
|
|
|
|
|
from env import call_env
|
|
|
|
|
|
|
|
|
|
import mal_readline
|
|
|
|
|
|
|
|
|
|
from mal_types import (Boolean, Env, Error, Fn, Form, List, Macro,
|
|
|
|
|
Map, Nil, String, Symbol, TCOEnv,
|
|
|
|
|
ThrownException, Vector, pr_seq)
|
|
|
|
|
|
2019-08-18 10:05:48 -07:00
|
|
|
import reader
|
python.2: fix tests, use source checkers and more python3 features
Check the whole code with flake8, pylint and mypy.
Report all possible errors with extensive context.
Demonstrate iterators, decorators, functional tools, chain maps,
dataclasses, match statements, assignments expressions.
Implement environments with python chain maps.
Rewrite the reader without external dependencies (but inspired by the
ptk library). The motivation was that no external library is fully
type-checked for now.
Avoid name clashes when possible (print, read, readline, types).
Write the readline history file at exit, not after each prompt.
Replace printer.pr_str as methods of MAL objects. This is idiomatic
python, and improves the error reporting.
Change some representations so that the python equality matches the
MAL equality. The recursion is now implicit.
Remove -O from ./run. It took me a while to understand that run-time
assertions were disabled! MAL is about development, not performance.
Dispatch the special forms from a dict, for readability (pylint
rightfully complains that there are too many return statements in
eval_()).
Copy the recursion overflow fix, the python interaction from the first
python implementation.
Add tests detecting that nil false 0 () [] "" are distinct, and that 0
() are not False when tested (in python False == 0 and an empty
container is tested ).
Add tests checking that metadata does not affect equality (they do
with a naive python dataclass).
2024-08-22 06:56:10 +02:00
|
|
|
|
|
|
|
|
# Special forms return either a final result or a new TCO context.
|
|
|
|
|
SpecialResult = tuple[Form, Env | None]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def eval_def(args: Sequence[Form], env: Env) -> SpecialResult:
|
|
|
|
|
match args:
|
|
|
|
|
case [Symbol() as key, form]:
|
|
|
|
|
value = eval_(form, env)
|
|
|
|
|
env[key] = value
|
|
|
|
|
return value, None
|
|
|
|
|
case _:
|
|
|
|
|
raise Error('def!: bad arguments: ' + pr_seq(args))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def eval_let(args: Sequence[Form], env: Env) -> SpecialResult:
|
|
|
|
|
match args:
|
|
|
|
|
case [List() | Vector() as binds, form]:
|
|
|
|
|
if len(binds) % 2:
|
|
|
|
|
raise Error('let*: odd bind count: ' + pr_seq(binds))
|
|
|
|
|
let_env = env.new_child()
|
|
|
|
|
for i in range(0, len(binds), 2):
|
|
|
|
|
key = binds[i]
|
|
|
|
|
if not isinstance(key, Symbol):
|
|
|
|
|
raise Error(f'let*: {key} is not a symbol')
|
|
|
|
|
let_env[key] = eval_(binds[i + 1], let_env)
|
|
|
|
|
return form, let_env
|
|
|
|
|
case _:
|
|
|
|
|
raise Error('let*: bad arguments: ' + pr_seq(args))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def eval_do(args: Sequence[Form], env: Env) -> SpecialResult:
|
|
|
|
|
match args:
|
|
|
|
|
case [*forms, last]:
|
|
|
|
|
for form in forms:
|
|
|
|
|
eval_(form, env)
|
|
|
|
|
return last, env
|
|
|
|
|
case _:
|
|
|
|
|
raise Error('do: no argument')
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def eval_if(args: Sequence[Form], env: Env) -> SpecialResult:
|
|
|
|
|
if 2 <= len(args) <= 3:
|
|
|
|
|
if eval_(args[0], env) in (Nil.NIL, Boolean.FALSE):
|
|
|
|
|
if len(args) == 3:
|
|
|
|
|
return args[2], env
|
|
|
|
|
return Nil.NIL, None
|
|
|
|
|
return args[1], env
|
|
|
|
|
raise Error('if: bad argument count: ' + pr_seq(args))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def eval_fn(args: Sequence[Form], env: Env) -> SpecialResult:
|
|
|
|
|
match args:
|
|
|
|
|
case [List() | Vector() as forms, body]:
|
|
|
|
|
# The new structure convinces mypy.
|
|
|
|
|
parms = []
|
|
|
|
|
for parm in forms:
|
|
|
|
|
if not isinstance(parm, Symbol):
|
|
|
|
|
raise Error(f'fn*: {parm} is not a symbol')
|
|
|
|
|
parms.append(parm)
|
|
|
|
|
|
|
|
|
|
def fenv(f_args: Sequence[Form]) -> Env:
|
|
|
|
|
return call_env(env, parms, f_args)
|
|
|
|
|
|
|
|
|
|
def call(f_args: Sequence[Form]) -> Form:
|
|
|
|
|
return eval_(body, fenv(f_args))
|
|
|
|
|
|
|
|
|
|
return Fn(call, TCOEnv(body, fenv)), None
|
|
|
|
|
case _:
|
|
|
|
|
raise Error('fn*: bad arguments: ' + pr_seq(args))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def eval_quote(args: Sequence[Form], _env: Env) -> SpecialResult:
|
|
|
|
|
match args:
|
|
|
|
|
case [form]:
|
|
|
|
|
return form, None
|
|
|
|
|
case _:
|
|
|
|
|
raise Error('quote: bad arguments: ' + pr_seq(args))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def qq_loop(acc: List, elt: Form) -> List:
|
|
|
|
|
match elt:
|
|
|
|
|
case List([Symbol('splice-unquote'), form]):
|
|
|
|
|
return List((Symbol('concat'), form, acc))
|
|
|
|
|
case List([Symbol('splice-unquote'), *args]):
|
|
|
|
|
raise Error('splice-unquote: bad arguments: ' + pr_seq(args))
|
|
|
|
|
case _:
|
|
|
|
|
return List((Symbol('cons'), quasiquote(elt), acc))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def qq_foldr(forms: Sequence[Form]) -> List:
|
|
|
|
|
return functools.reduce(qq_loop, reversed(forms), List())
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def quasiquote(ast: Form) -> Form:
|
|
|
|
|
match ast:
|
|
|
|
|
case Map() | Symbol():
|
|
|
|
|
return List((Symbol('quote'), ast))
|
|
|
|
|
case Vector():
|
|
|
|
|
return List((Symbol('vec'), qq_foldr(ast)))
|
|
|
|
|
case List([Symbol('unquote'), form]):
|
|
|
|
|
return form
|
|
|
|
|
case List([Symbol('unquote'), *args]):
|
|
|
|
|
raise Error('unquote: bad arguments: ' + pr_seq(args))
|
|
|
|
|
case List():
|
|
|
|
|
return qq_foldr(ast)
|
|
|
|
|
case _:
|
2019-08-18 10:05:48 -07:00
|
|
|
return ast
|
|
|
|
|
|
python.2: fix tests, use source checkers and more python3 features
Check the whole code with flake8, pylint and mypy.
Report all possible errors with extensive context.
Demonstrate iterators, decorators, functional tools, chain maps,
dataclasses, match statements, assignments expressions.
Implement environments with python chain maps.
Rewrite the reader without external dependencies (but inspired by the
ptk library). The motivation was that no external library is fully
type-checked for now.
Avoid name clashes when possible (print, read, readline, types).
Write the readline history file at exit, not after each prompt.
Replace printer.pr_str as methods of MAL objects. This is idiomatic
python, and improves the error reporting.
Change some representations so that the python equality matches the
MAL equality. The recursion is now implicit.
Remove -O from ./run. It took me a while to understand that run-time
assertions were disabled! MAL is about development, not performance.
Dispatch the special forms from a dict, for readability (pylint
rightfully complains that there are too many return statements in
eval_()).
Copy the recursion overflow fix, the python interaction from the first
python implementation.
Add tests detecting that nil false 0 () [] "" are distinct, and that 0
() are not False when tested (in python False == 0 and an empty
container is tested ).
Add tests checking that metadata does not affect equality (they do
with a naive python dataclass).
2024-08-22 06:56:10 +02:00
|
|
|
|
|
|
|
|
def eval_quasiquote(args: Sequence[Form], env: Env) -> SpecialResult:
|
|
|
|
|
match args:
|
|
|
|
|
case [form]:
|
|
|
|
|
return quasiquote(form), env
|
|
|
|
|
case _:
|
|
|
|
|
raise Error('quasiquote: bad arguments: ' + pr_seq(args))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def eval_defmacro(args: Sequence[Form], env: Env) -> SpecialResult:
|
|
|
|
|
match args:
|
|
|
|
|
case [Symbol() as key, form]:
|
|
|
|
|
fun = eval_(form, env)
|
|
|
|
|
if not isinstance(fun, Fn):
|
|
|
|
|
raise Error(f'defmacro!: {fun} is not a function')
|
|
|
|
|
macro = Macro(fun.call)
|
|
|
|
|
env[key] = macro
|
|
|
|
|
return macro, None
|
|
|
|
|
case _:
|
|
|
|
|
raise Error('defmacro!: bad arguments: ' + pr_seq(args))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def eval_try(args: Sequence[Form], env: Env) -> SpecialResult:
|
|
|
|
|
match args:
|
|
|
|
|
case [test]:
|
|
|
|
|
return test, env
|
|
|
|
|
case [test, List([Symbol('catch*'), Symbol() as key, handler])]:
|
2019-08-18 10:05:48 -07:00
|
|
|
try:
|
python.2: fix tests, use source checkers and more python3 features
Check the whole code with flake8, pylint and mypy.
Report all possible errors with extensive context.
Demonstrate iterators, decorators, functional tools, chain maps,
dataclasses, match statements, assignments expressions.
Implement environments with python chain maps.
Rewrite the reader without external dependencies (but inspired by the
ptk library). The motivation was that no external library is fully
type-checked for now.
Avoid name clashes when possible (print, read, readline, types).
Write the readline history file at exit, not after each prompt.
Replace printer.pr_str as methods of MAL objects. This is idiomatic
python, and improves the error reporting.
Change some representations so that the python equality matches the
MAL equality. The recursion is now implicit.
Remove -O from ./run. It took me a while to understand that run-time
assertions were disabled! MAL is about development, not performance.
Dispatch the special forms from a dict, for readability (pylint
rightfully complains that there are too many return statements in
eval_()).
Copy the recursion overflow fix, the python interaction from the first
python implementation.
Add tests detecting that nil false 0 () [] "" are distinct, and that 0
() are not False when tested (in python False == 0 and an empty
container is tested ).
Add tests checking that metadata does not affect equality (they do
with a naive python dataclass).
2024-08-22 06:56:10 +02:00
|
|
|
return eval_(test, env), None
|
|
|
|
|
except ThrownException as exc:
|
|
|
|
|
return handler, env.new_child({key: exc.form})
|
|
|
|
|
except Error as exc:
|
|
|
|
|
return handler, env.new_child({key: String(str(exc))})
|
|
|
|
|
case _:
|
|
|
|
|
raise Error('try*: bad arguments: ' + pr_seq(args))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
specials = {
|
|
|
|
|
'def!': eval_def,
|
|
|
|
|
'let*': eval_let,
|
|
|
|
|
'do': eval_do,
|
|
|
|
|
'if': eval_if,
|
|
|
|
|
'fn*': eval_fn,
|
|
|
|
|
'quote': eval_quote,
|
|
|
|
|
'quasiquote': eval_quasiquote,
|
|
|
|
|
'defmacro!': eval_defmacro,
|
|
|
|
|
'try*': eval_try,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def eval_(ast: Form, env: Env) -> Form:
|
|
|
|
|
while True:
|
|
|
|
|
if env.get('DEBUG-EVAL') not in (None, Nil.NIL, Boolean.FALSE):
|
|
|
|
|
print(f'EVAL: {ast}') # , repr(ast))
|
|
|
|
|
for outer in env.maps:
|
|
|
|
|
print(' ENV:', ' '.join(f'{k}: {v}'
|
|
|
|
|
for k, v in reversed(outer.items()))[:75])
|
|
|
|
|
match ast:
|
|
|
|
|
case Symbol():
|
|
|
|
|
if (value := env.get(ast)) is not None:
|
|
|
|
|
return value
|
|
|
|
|
raise Error(f"'{ast}' not found")
|
|
|
|
|
case Map():
|
|
|
|
|
return Map((k, eval_(v, env)) for k, v in ast.items())
|
|
|
|
|
case Vector():
|
|
|
|
|
return Vector(eval_(x, env) for x in ast)
|
|
|
|
|
case List([first, *args]):
|
|
|
|
|
if isinstance(first, Symbol) and (spec := specials.get(first)):
|
|
|
|
|
ast, maybe_env = spec(args, env)
|
|
|
|
|
if maybe_env is None:
|
|
|
|
|
return ast
|
|
|
|
|
env = maybe_env
|
|
|
|
|
else:
|
|
|
|
|
match eval_(first, env):
|
|
|
|
|
case Macro(call):
|
|
|
|
|
ast = call(args)
|
|
|
|
|
case Fn(tco_env=TCOEnv(body, fenv)):
|
|
|
|
|
ast = body
|
|
|
|
|
env = fenv(tuple(eval_(x, env) for x in args))
|
|
|
|
|
case Fn(call):
|
|
|
|
|
return call(tuple(eval_(x, env) for x in args))
|
|
|
|
|
case not_fun:
|
|
|
|
|
raise Error(f'cannot apply {not_fun}')
|
|
|
|
|
case _:
|
|
|
|
|
return ast
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def rep(source: str, env: Env) -> str:
|
|
|
|
|
return str(eval_(reader.read(source), env))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def main() -> None:
|
|
|
|
|
repl_env = Env(core.ns) # Modifying ns is OK.
|
|
|
|
|
|
|
|
|
|
@core.built_in('eval')
|
|
|
|
|
def _(args: Sequence[Form]) -> Form:
|
|
|
|
|
match args:
|
|
|
|
|
case [form]:
|
|
|
|
|
return eval_(form, repl_env)
|
|
|
|
|
case _:
|
|
|
|
|
raise Error('bad arguments')
|
|
|
|
|
|
|
|
|
|
rep('(def! not (fn* (a) (if a false true)))', repl_env)
|
|
|
|
|
rep("""(def! load-file (fn* (f)
|
|
|
|
|
(eval (read-string (str "(do " (slurp f) "\nnil)")))))""", repl_env)
|
|
|
|
|
rep("""(defmacro! cond (fn* (& xs) (if (> (count xs) 0)
|
|
|
|
|
(list 'if (first xs) (if (> (count xs) 1) (nth xs 1)
|
|
|
|
|
(throw "odd number of forms to cond"))
|
|
|
|
|
(cons 'cond (rest (rest xs)))))))""", repl_env)
|
|
|
|
|
match sys.argv:
|
|
|
|
|
case _, file_name, *args:
|
|
|
|
|
repl_env['*ARGV*'] = List(String(a) for a in args)
|
|
|
|
|
rep(f'(load-file "{file_name}")', repl_env)
|
|
|
|
|
case _:
|
|
|
|
|
repl_env['*ARGV*'] = List()
|
|
|
|
|
while True:
|
|
|
|
|
try:
|
|
|
|
|
print(rep(mal_readline.input_('user> '), repl_env))
|
|
|
|
|
except EOFError:
|
|
|
|
|
break
|
|
|
|
|
# pylint: disable-next=broad-exception-caught
|
|
|
|
|
except Exception as exc:
|
|
|
|
|
traceback.print_exception(exc, limit=10)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if __name__ == '__main__':
|
|
|
|
|
main()
|