From e7178e3c4e344d5d83b64f780a29e2953904e4af Mon Sep 17 00:00:00 2001 From: Nicolas Boulenguez Date: Thu, 22 Aug 2024 06:56:10 +0200 Subject: [PATCH] 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). --- README.md | 11 +- impls/python.2/Dockerfile | 16 +- impls/python.2/Makefile | 24 +- impls/python.2/core.py | 851 ++++++++++++++++------------- impls/python.2/env.py | 56 +- impls/python.2/mal_readline.py | 21 + impls/python.2/mal_types.py | 362 +++++------- impls/python.2/reader.py | 284 +++++----- impls/python.2/run | 2 +- impls/python.2/step0_repl.py | 37 +- impls/python.2/step1_read_print.py | 47 +- impls/python.2/step2_eval.py | 136 +++-- impls/python.2/step3_env.py | 214 ++++---- impls/python.2/step4_if_fn_do.py | 253 +++++---- impls/python.2/step5_tco.py | 281 +++++----- impls/python.2/step6_file.py | 322 ++++++----- impls/python.2/step7_quote.py | 413 +++++++------- impls/python.2/step8_macros.py | 458 ++++++++-------- impls/python.2/step9_try.py | 489 +++++++++-------- impls/python.2/stepA_mal.py | 495 ++++++++--------- impls/python.2/tests/step5_tco.mal | 15 + impls/python.2/tests/stepA_mal.mal | 32 ++ impls/tests/step4_if_fn_do.mal | 51 +- impls/tests/step9_try.mal | 26 +- impls/tests/stepA_mal.mal | 13 + 25 files changed, 2522 insertions(+), 2387 deletions(-) create mode 100644 impls/python.2/mal_readline.py create mode 100644 impls/python.2/tests/step5_tco.mal create mode 100644 impls/python.2/tests/stepA_mal.mal diff --git a/README.md b/README.md index af927451c7..d6ac14fc5c 100644 --- a/README.md +++ b/README.md @@ -971,13 +971,10 @@ python stepX_YYY.py ### Python.2 (3.X) -The second Python implementation makes heavy use of type annotations and uses the Arpeggio parser library. - -``` -# Recommended: do these steps in a Python virtual environment. -pip3 install Arpeggio==1.9.0 -python3 stepX_YYY.py -``` +The second Python implementation is checked for style and types +(flake8, pylint, mypy). It reports all errors with details. +It demonstrates iterators, decorators, functional tools, chain maps, +dataclasses, introspection, match statements, assignement expressions. ### RPython diff --git a/impls/python.2/Dockerfile b/impls/python.2/Dockerfile index 857c3eb25f..20eb418b68 100644 --- a/impls/python.2/Dockerfile +++ b/impls/python.2/Dockerfile @@ -9,10 +9,8 @@ MAINTAINER Joel Martin RUN apt-get -y update # Required for running tests -RUN apt-get -y install make python - -# Some typical implementation and test requirements -RUN apt-get -y install curl libreadline-dev libedit-dev +RUN apt-get -y install make python3 +RUN ln -fs /usr/bin/python3 /usr/local/bin/python RUN mkdir -p /mal WORKDIR /mal @@ -21,11 +19,5 @@ WORKDIR /mal # Specific implementation requirements ########################################################## -# Nothing additional needed for python -RUN apt-get -y install python3 - -# For dist packaging -RUN apt-get -y install zip - -# Pypi modules -RUN apt-get -y install python3-pip && pip3 install Arpeggio==1.9.0 +# For checking: +# RUN apt-get -y install flake8 mypy pylint diff --git a/impls/python.2/Makefile b/impls/python.2/Makefile index 6d1a45785b..19fa6d5381 100644 --- a/impls/python.2/Makefile +++ b/impls/python.2/Makefile @@ -1,6 +1,26 @@ +# make check sources=reader.py may be convenient +sources ?= *.py + +f8 += D100 # Missing docstring in public module +f8 += D101 # Missing docstring in public class +f8 += D102 # Missing docstring in public method +f8 += D103 # Missing docstring in public function +f8 += D105 # Missing docstring in magic method +f8 += D107 # Missing docstring in __init__ +f8 += I100 # order of import statements (incompatible with pylint) +f8 += W503 # line break before binary operator (incompatible with 504) +pl += missing-module-docstring +pl += missing-class-docstring +pl += missing-function-docstring +pl += R0801 # Similar lines in 2 files (steps...) + all: - true -.PHONY: clean +check: + pylint --disable=$(shell echo $(pl) | sed 's/ /,/g') $(sources) + mypy $(sources) + flake8 --ignore=$(shell echo $(f8) | sed 's/ /,/g') $(sources) clean: + rm -f *~ + rm -fr __pycache__/ .mypy_cache/ diff --git a/impls/python.2/core.py b/impls/python.2/core.py index f0e21e5164..1075723ab7 100644 --- a/impls/python.2/core.py +++ b/impls/python.2/core.py @@ -1,441 +1,526 @@ +import collections.abc +import dataclasses +import itertools import time -from typing import List, Union, Dict - -import reader -from mal_types import ( - MalInt, - MalNil, - MalList, - MalBoolean, - MalExpression, - MalFunctionCompiled, - MalAtom, - MalFunctionRaw, - MalHash_map, - MalVector, -) -from mal_types import ( - MalInvalidArgumentException, - MalString, - MalException, - MalSymbol, - MalNotImplementedException, - MalIndexError, -) - - -def prn(args: List[MalExpression]) -> MalNil: - result_string = " ".join(map(lambda x: x.readable_str(), args)) - print(result_string) - return MalNil() - - -def pr_str(args: List[MalExpression]) -> MalString: - result_string = " ".join(map(lambda x: x.readable_str(), args)) - return MalString(result_string) - - -def println(args: List[MalExpression]) -> MalNil: - result_string = " ".join(map(lambda x: x.unreadable_str(), args)) - print(result_string) - return MalNil() - - -def list_q(x: MalExpression) -> MalBoolean: - if isinstance(x, MalList): - return MalBoolean(True) - return MalBoolean(False) - - -def empty_q(x: MalExpression) -> MalBoolean: - if sequential_q(x): - return MalBoolean(len(x.native()) == 0) - raise MalInvalidArgumentException(x, "not a list") - - -def count(x: MalExpression) -> MalInt: - if isinstance(x, MalList) or isinstance(x, MalVector): - return MalInt(len(x.native())) - elif isinstance(x, MalNil): - return MalInt(0) - raise MalInvalidArgumentException(x, "not a list") +import operator +import typing +from collections.abc import Callable, Sequence +import mal_readline -def equal(a: MalExpression, b: MalExpression) -> MalBoolean: - if (isinstance(a, MalList) or isinstance(a, MalVector)) and ( - isinstance(b, MalList) or isinstance(b, MalVector) - ): - a_native = a.native() - b_native = b.native() - if len(a_native) != len(b_native): - return MalBoolean(False) - for x in range(0, len(a_native)): - if not equal(a_native[x], b_native[x]): - return MalBoolean(False) - return MalBoolean(True) - if type(a) == type(b) and a.native() == b.native(): - return MalBoolean(True) - return MalBoolean(False) - - -def less(a: MalExpression, b: MalExpression) -> MalBoolean: - if not isinstance(a, MalInt): - raise MalInvalidArgumentException(a, "not an int") - if not isinstance(b, MalInt): - raise MalInvalidArgumentException(b, "not an int") - return MalBoolean(a.native() < b.native()) - - -def less_equal(a: MalExpression, b: MalExpression) -> MalBoolean: - if not isinstance(a, MalInt): - raise MalInvalidArgumentException(a, "not an int") - if not isinstance(b, MalInt): - raise MalInvalidArgumentException(b, "not an int") - return MalBoolean(a.native() <= b.native()) - - -def read_string(a: MalExpression) -> MalExpression: - if isinstance(a, MalString): - result = reader.read(a.native()) - return result - raise MalInvalidArgumentException(a, "not a string") - - -def slurp(filename: MalExpression) -> MalString: - assert isinstance(filename, MalString) - with open(filename.native(), "r") as the_file: - contents = the_file.read() - return MalString(contents) - - -def core_str(args: List[MalExpression]) -> MalString: - result = "" - for a in args: - result += a.unreadable_str() - return MalString(result) +from mal_types import (Atom, Boolean, Error, Fn, Form, Keyword, List, + Macro, Map, Nil, Number, PythonCall, String, + Symbol, ThrownException, Vector, pr_seq) +import reader -def deref_q(atom: MalExpression) -> MalExpression: - assert isinstance(atom, MalAtom) - return atom.native() +ns: dict[str, Form] = {} -def reset(atom: MalExpression, val: MalExpression) -> MalExpression: - assert isinstance(atom, MalAtom) - atom.reset(val) - return val +def built_in(name: str) -> Callable[[PythonCall], None]: + """Register in ns and add context to Errors.""" -def vec(arg: MalExpression) -> MalExpression: - assert isinstance(arg, MalList) or isinstance(arg, MalVector) - return MalVector(arg.native ()) + def decorate(old_f: PythonCall) -> None: + def new_f(args: Sequence[Form]) -> Form: + try: + return old_f(args) + except Error as exc: + exc.add_note('The ' + name + ' core function received [' + + pr_seq(args) + ' ] as arguments.') + raise -def cons(first: MalExpression, rest: MalExpression) -> MalExpression: - assert isinstance(rest, MalList) or isinstance(rest, MalVector) - return MalList([first] + rest.native()) + ns[name] = Fn(new_f) + return decorate -def concat(args: List[MalExpression]) -> MalExpression: - result_list: List[MalExpression] = [] - for x in args: - assert isinstance(x, MalList) or isinstance(x, MalVector) - result_list = result_list + x.native() - return MalList(result_list) +def equality(value: Form) -> PythonCall: -def not_(expr: MalExpression) -> MalExpression: - if isinstance(expr, MalNil) or ( - isinstance(expr, MalBoolean) and expr.native() is False - ): - return MalBoolean(True) - else: - return MalBoolean(False) + def new_f(args: Sequence[Form]) -> Form: + match args: + case [form]: + return Boolean(form == value) + case _: + raise Error('bad arguments') + return new_f -def nth(list_: MalExpression, index: MalExpression) -> MalExpression: - assert isinstance(list_, MalList) or isinstance(list_, MalVector) - assert isinstance(index, MalInt) - list_native = list_.native() - if index.native() > len(list_native) - 1: - raise MalIndexError(index.native()) - return list_native[index.native()] -def _callable(arg: MalExpression): - return isinstance(arg, (MalFunctionCompiled, MalFunctionRaw)) +built_in('nil?')(equality(Nil.NIL)) +built_in('false?')(equality(Boolean.FALSE)) +built_in('true?')(equality(Boolean.TRUE)) -def apply(args: List[MalExpression]) -> MalExpression: - func = args[0] - assert _callable(func) - rest_args: List[MalExpression] = [] - for i in range(1, len(args) - 1): - rest_args.append(args[i]) - last_arg = args[len(args) - 1] - assert isinstance(last_arg, MalList) or isinstance(last_arg, MalVector) - rest_args = rest_args + last_arg.native() - return func.call(rest_args) +def membership(*classes: type) -> PythonCall: -def map_(func: MalExpression, map_list: MalExpression) -> MalExpression: - assert _callable(func) - assert isinstance(map_list, MalList) or isinstance(map_list, MalVector) - result_list: List[MalExpression] = [] - for i in range(len(map_list.native())): - elem = map_list.native()[i] - result_list.append(func.call([elem])) - return MalList(result_list) + def new_f(args: Sequence[Form]) -> Form: + match args: + case [form]: + return Boolean(isinstance(form, classes)) + case _: + raise Error('bad arguments') + return new_f -def throw(exception: MalExpression) -> MalExpression: - raise MalException(exception) +built_in('number?')(membership(Number)) +built_in('symbol?')(membership(Symbol)) +built_in('keyword?')(membership(Keyword)) +built_in('string?')(membership(String)) +built_in('list?')(membership(List)) +built_in('map?')(membership(Map)) +built_in('atom?')(membership(Atom)) +built_in('vector?')(membership(Vector)) +built_in('macro?')(membership(Macro)) +built_in('sequential?')(membership(List, Vector)) +built_in('fn?')(membership(Fn)) -def nil_q(arg: MalExpression) -> MalExpression: - return MalBoolean(isinstance(arg, MalNil)) +def arithmetic(old_f: Callable[[int, int], int]) -> PythonCall: -def true_q(arg: MalExpression) -> MalExpression: - return MalBoolean(isinstance(arg, MalBoolean) and arg.native()) + def new_f(args: Sequence[Form]) -> Form: + match args: + case [Number() as left, Number() as right]: + return Number(old_f(left, right)) + case _: + raise Error('bad arguments') + return new_f -def false_q(arg: MalExpression) -> MalExpression: - return MalBoolean(isinstance(arg, MalBoolean) and not arg.native()) +built_in('+')(arithmetic(operator.add)) +built_in('-')(arithmetic(operator.sub)) +built_in('*')(arithmetic(operator.mul)) +built_in('/')(arithmetic(operator.floordiv)) -def symbol_q(arg: MalExpression) -> MalExpression: - return MalBoolean(isinstance(arg, MalSymbol)) +def comparison(old_f: Callable[[int, int], bool]) -> PythonCall: -def keyword_q(arg: MalExpression) -> MalExpression: - return MalBoolean(isinstance(arg, MalString) and arg.is_keyword()) + def new_f(args: Sequence[Form]) -> Form: + match args: + case [Number() as left, Number() as right]: + return Boolean(old_f(left, right)) + case _: + raise Error('bad arguments') + return new_f -def keyword(arg: MalExpression) -> MalExpression: - assert isinstance(arg, MalString) - if arg.is_keyword(): - return arg - else: - return MalString(arg.unreadable_str(), keyword=True) +built_in('<')(comparison(operator.lt)) +built_in('<=')(comparison(operator.le)) +built_in('>')(comparison(operator.gt)) +built_in('>=')(comparison(operator.ge)) -def symbol(arg: MalExpression) -> MalExpression: - assert isinstance(arg, MalString) - return MalSymbol(arg.unreadable_str()) +@built_in('=') +def _(args: Sequence[Form]) -> Form: + match args: + case [left, right]: + return Boolean(left == right) + case _: + raise Error('bad arguments') -def readline(arg: MalExpression) -> Union[MalString, MalNil]: - try: - assert isinstance(arg, MalString) - line = input(arg.native()) - except EOFError: - return MalNil() - return MalString(line) -def fn_q(arg: MalExpression) -> MalExpression: - return MalBoolean(_callable(arg) and not arg.is_macro()) +built_in('list')(List) +built_in('vector')(Vector) -def macro_q(arg: MalExpression) -> MalExpression: - return MalBoolean(_callable(arg) and arg.is_macro()) -def string_q(arg: MalExpression) -> MalExpression: - return MalBoolean(isinstance(arg, MalString) and not arg.is_keyword()) +@built_in('prn') +def _(args: Sequence[Form]) -> Form: + print(pr_seq(args)) + return Nil.NIL -def number_q(arg: MalExpression) -> MalExpression: - return MalBoolean(isinstance(arg, MalInt)) -def not_implemented(func: str) -> MalExpression: - raise MalNotImplementedException(func) +@built_in('pr-str') +def _(args: Sequence[Form]) -> Form: + return String(pr_seq(args)) -def get(map: MalExpression, key: MalExpression) -> MalExpression: - if isinstance(map, MalNil): - return MalNil() - if not isinstance(map, MalHash_map): - raise MalInvalidArgumentException(map, "not a hash map") - if key.native() in map.native(): - return map.native()[key.native()] - else: - return MalNil() +@built_in('println') +def _(args: Sequence[Form]) -> Form: + print(pr_seq(args, readably=False)) + return Nil.NIL -def first(args: List[MalExpression]) -> MalExpression: - try: - if isinstance(args[0], MalNil): - return MalNil() - return args[0].native()[0] - except IndexError: - return MalNil() - except TypeError: - raise MalInvalidArgumentException(args[0], "not a list") +@built_in('empty?') +def _(args: Sequence[Form]) -> Form: + match args: + case [List() | Vector() as seq]: + return Boolean(not seq) + case _: + raise Error('bad arguments') -def rest(args: List[MalExpression]) -> MalExpression: - try: - if isinstance(args[0], MalNil): - return MalList([]) - return MalList(args[0].native()[1:]) - except TypeError: - raise MalInvalidArgumentException(args[0], "not a list or vector") +@built_in('count') +def _(args: Sequence[Form]) -> Form: + match args: + case [List() | Vector() as seq]: + return Number(len(seq)) + case [Nil()]: + return Number(0) + case _: + raise Error('bad arguments') -def vector_q(arg: MalExpression) -> MalExpression: - return MalBoolean(isinstance(arg, MalVector)) +@built_in('read-string') +def _(args: Sequence[Form]) -> Form: + match args: + case [String(line)]: + return reader.read(line) + case _: + raise Error('bad arguments') -def map_q(arg: MalExpression) -> MalExpression: - return MalBoolean(isinstance(arg, MalHash_map)) +@built_in('slurp') +def _(args: Sequence[Form]) -> Form: + match args: + case [String(file_name)]: + with open(file_name, 'r', encoding='utf-8') as the_file: + return String(the_file.read()) + case _: + raise Error('bad arguments') -def sequential_q(arg: MalExpression) -> MalExpression: - return MalBoolean(isinstance(arg, MalList) or isinstance(arg, MalVector)) +@built_in('str') +def _(args: Sequence[Form]) -> Form: + return String(pr_seq(args, readably=False, sep='')) -def vector(args: List[MalExpression]) -> MalExpression: - return MalVector(args) - - -def hash_map(args: List[MalExpression]) -> MalExpression: - assert len(args) % 2 == 0 - map_ = {} # type: Dict[str, MalExpression] - for i in range(0, len(args) - 1, 2): - assert isinstance(args[i], MalString) - map_[args[i].native()] = args[i + 1] - return MalHash_map(map_) - - -def assoc(args: List[MalExpression]) -> MalExpression: - if len(args) == 0: - raise MalInvalidArgumentException(MalNil(), "no arguments supplied to assoc") - elif len(args) == 1: - return args[0] - if not isinstance(args[0], MalHash_map): - raise MalInvalidArgumentException(args[0], "not a hash map") - dict_a_copy: Dict[str, MalExpression] = args[0].native().copy() - dict_b: Dict[str, MalExpression] = hash_map(args[1:]).native() - for key in dict_b: - dict_a_copy[key] = dict_b[key] - return MalHash_map(dict_a_copy) - - -def contains_q(args: List[MalExpression]) -> MalExpression: - if len(args) < 2: - raise MalInvalidArgumentException(MalNil(), "contains? requires two arguments") - if not isinstance(args[0], MalHash_map): - raise MalInvalidArgumentException(args[0], "not a hash-map") - if not isinstance(args[1], MalString): - return MalBoolean(False) - return MalBoolean(args[1].native() in args[0].native()) - - -def keys(args: List[MalExpression]) -> MalExpression: - if len(args) != 1: - raise MalInvalidArgumentException( - MalNil(), "keys requires exactly one argument" - ) - if not isinstance(args[0], MalHash_map): - raise MalInvalidArgumentException(args[0], "not a hash map") - return MalList([MalString(x, is_already_encoded=True) for x in args[0].native()]) - - -def vals(args: List[MalExpression]) -> MalExpression: - if len(args) != 1: - raise MalInvalidArgumentException( - MalNil(), "vals requires exactly one argument" - ) - if not isinstance(args[0], MalHash_map): - raise MalInvalidArgumentException(args[0], "not a hash map") - return MalList([args[0].native()[x] for x in args[0].native()]) - - -def dissoc(args: List[MalExpression]) -> MalExpression: - if len(args) == 0: - raise MalInvalidArgumentException(MalNil(), "no arguments supplied to dissoc") - elif len(args) == 1: - return args[0] - if not isinstance(args[0], MalHash_map): - raise MalInvalidArgumentException(args[0], "not a hash map") - dict_a_copy: Dict[str, MalExpression] = args[0].native().copy() - list_b: List[MalExpression] = MalList(args[1:]).native() - for key in list_b: - try: - del dict_a_copy[key.unreadable_str()] - except KeyError: - pass - return MalHash_map(dict_a_copy) - - -def swap(args: List[MalExpression]) -> MalExpression: - atom = args[0] - assert isinstance(atom, MalAtom) - func = args[1] - assert _callable(func) - atom.reset(func.call([atom.native()] + args[2:])) - return atom.native() - - -ns = { - "+": MalFunctionCompiled(lambda args: MalInt(args[0].native() + args[1].native())), - "-": MalFunctionCompiled(lambda args: MalInt(args[0].native() - args[1].native())), - "*": MalFunctionCompiled(lambda args: MalInt(args[0].native() * args[1].native())), - "/": MalFunctionCompiled( - lambda args: MalInt(int(args[0].native() / args[1].native())) - ), - "prn": MalFunctionCompiled(lambda args: prn(args)), - "pr-str": MalFunctionCompiled(lambda args: pr_str(args)), - "println": MalFunctionCompiled(lambda args: println(args)), - "list": MalFunctionCompiled(lambda args: MalList(args)), - "list?": MalFunctionCompiled(lambda args: list_q(args[0])), - "empty?": MalFunctionCompiled(lambda args: empty_q(args[0])), - "count": MalFunctionCompiled(lambda args: count(args[0])), - "=": MalFunctionCompiled(lambda args: equal(args[0], args[1])), - "<": MalFunctionCompiled(lambda args: less(args[0], args[1])), - "<=": MalFunctionCompiled(lambda args: less_equal(args[0], args[1])), - ">": MalFunctionCompiled(lambda args: less(args[1], args[0])), - ">=": MalFunctionCompiled(lambda args: less_equal(args[1], args[0])), - "read-string": MalFunctionCompiled(lambda args: read_string(args[0])), - "slurp": MalFunctionCompiled(lambda args: slurp(args[0])), - "str": MalFunctionCompiled(lambda args: core_str(args)), - "atom": MalFunctionCompiled(lambda args: MalAtom(args[0])), - "atom?": MalFunctionCompiled(lambda args: MalBoolean(isinstance(args[0], MalAtom))), - "deref": MalFunctionCompiled(lambda args: deref_q(args[0])), - "reset!": MalFunctionCompiled(lambda args: reset(args[0], args[1])), - "vec": MalFunctionCompiled(lambda args: vec(args[0])), - "cons": MalFunctionCompiled(lambda args: cons(args[0], args[1])), - "concat": MalFunctionCompiled(concat), - "not": MalFunctionCompiled(lambda args: not_(args[0])), - "nth": MalFunctionCompiled(lambda args: nth(args[0], args[1])), - "apply": MalFunctionCompiled(lambda args: apply(args)), - "map": MalFunctionCompiled(lambda args: map_(args[0], args[1])), - "throw": MalFunctionCompiled(lambda args: throw(args[0])), - "nil?": MalFunctionCompiled(lambda args: nil_q(args[0])), - "true?": MalFunctionCompiled(lambda args: true_q(args[0])), - "false?": MalFunctionCompiled(lambda args: false_q(args[0])), - "symbol": MalFunctionCompiled(lambda args: symbol(args[0])), - "symbol?": MalFunctionCompiled(lambda args: symbol_q(args[0])), - "readline": MalFunctionCompiled(lambda args: readline(args[0])), - "time-ms": MalFunctionCompiled(lambda args: MalInt(int(time.time() * 1000))), - "meta": MalFunctionCompiled(lambda args: not_implemented("meta")), - "with-meta": MalFunctionCompiled(lambda args: not_implemented("with-meta")), - "fn?": MalFunctionCompiled(lambda args: fn_q(args[0])), - "macro?": MalFunctionCompiled(lambda args: macro_q(args[0])), - "string?": MalFunctionCompiled(lambda args: string_q(args[0])), - "number?": MalFunctionCompiled(lambda args: number_q(args[0])), - "seq": MalFunctionCompiled(lambda args: not_implemented("seq")), - "conj": MalFunctionCompiled(lambda args: not_implemented("conj")), - "get": MalFunctionCompiled(lambda args: get(args[0], args[1])), - "first": MalFunctionCompiled(lambda args: first(args)), - "rest": MalFunctionCompiled(lambda args: rest(args)), - "keyword?": MalFunctionCompiled(lambda args: keyword_q(args[0])), - "keyword": MalFunctionCompiled(lambda args: keyword(args[0])), - "vector?": MalFunctionCompiled(lambda args: vector_q(args[0])), - "map?": MalFunctionCompiled(lambda args: map_q(args[0])), - "sequential?": MalFunctionCompiled(lambda args: sequential_q(args[0])), - "vector": MalFunctionCompiled(lambda args: vector(args)), - "hash-map": MalFunctionCompiled(lambda args: hash_map(args)), - "assoc": MalFunctionCompiled(lambda args: assoc(args)), - "contains?": MalFunctionCompiled(lambda args: contains_q(args)), - "keys": MalFunctionCompiled(lambda args: keys(args)), - "vals": MalFunctionCompiled(lambda args: vals(args)), - "dissoc": MalFunctionCompiled(lambda args: dissoc(args)), - "swap!": MalFunctionCompiled(lambda args: swap(args)), -} +@built_in('atom') +def _(args: Sequence[Form]) -> Form: + match args: + case [form]: + return Atom(form) + case _: + raise Error('bad arguments') + + +@built_in('deref') +def _(args: Sequence[Form]) -> Form: + match args: + case [Atom(val)]: + return val + case _: + raise Error('bad arguments') + + +@built_in('reset!') +def _(args: Sequence[Form]) -> Form: + match args: + case [Atom() as atm, form]: + atm.val = form + return form + case _: + raise Error('bad arguments') + + +@built_in('vec') +def _(args: Sequence[Form]) -> Form: + match args: + case [List() as seq]: + return Vector(seq) + case [Vector() as seq]: + return seq + case _: + raise Error('bad arguments') + + +@built_in('cons') +def _(args: Sequence[Form]) -> Form: + match args: + case [head, List() | Vector() as tail]: + return List((head, *tail)) + case _: + raise Error('bad arguments') + + +def cast_sequence(arg: Form) -> List | Vector: + match arg: + case List() | Vector(): + return arg + case _: + raise Error(f'{arg} is not a sequence') + + +@built_in('concat') +def _(args: Sequence[Form]) -> Form: + return List(itertools.chain.from_iterable(cast_sequence(x) for x in args)) + + +@built_in('nth') +def _(args: Sequence[Form]) -> Form: + match args: + case [List() | Vector() as seq, Number() as idx]: + # Python would accept index = -1. + if 0 <= idx < len(seq): + return seq[idx] + raise Error(f'index {idx} not in range of {seq}') + case _: + raise Error('bad arguments') + + +@built_in('apply') +def _(args: Sequence[Form]) -> Form: + match args: + case [Fn(call) | Macro(call), *some, + List() | Vector() as more]: + return call((*some, *more)) + case _: + raise Error('bad arguments') + + +@built_in('map') +def _(args: Sequence[Form]) -> Form: + match args: + case [Fn(call), List() | Vector() as seq]: + return List(call((x, )) for x in seq) + case _: + raise Error('bad arguments') + + +@built_in('throw') +def _(args: Sequence[Form]) -> Form: + match args: + case [form]: + raise ThrownException(form) + case _: + raise Error('bad arguments') + + +@built_in('keyword') +def _(args: Sequence[Form]) -> Form: + match args: + case [String(string)]: + return Keyword(string) + case [Keyword() as keyword]: + return keyword + case _: + raise Error('bad arguments') + + +@built_in('symbol') +def _(args: Sequence[Form]) -> Form: + match args: + case [String(string)]: + return Symbol(string) + case [Symbol() as symbol]: + return symbol + case _: + raise Error('bad arguments') + + +@built_in('readline') +def _(args: Sequence[Form]) -> Form: + match args: + case [String(prompt)]: + try: + return String(mal_readline.input_(prompt)) + except EOFError: + return Nil.NIL + case _: + raise Error('bad arguments') + + +@built_in('time-ms') +def _(args: Sequence[Form]) -> Form: + if args: + raise Error('bad arguments') + return Number(time.time() * 1000.0) + + +@built_in('meta') +def _(args: Sequence[Form]) -> Form: + match args: + case [Fn() | List() | Vector() | Map() as form]: + return form.meta + case _: + raise Error('bad arguments') + + +@built_in('with-meta') +def _(args: Sequence[Form]) -> Form: + # container = type(container)(container, meta=meta) confuses mypy. + match args: + case [List() as container, meta]: + return List(container, meta=meta) + case [Vector() as container, meta]: + return Vector(container, meta=meta) + case [Map() as container, meta]: + return Map(container, meta) + case [Fn() as container, meta]: + return dataclasses.replace(container, meta=meta) + case _: + raise Error('bad arguments') + + +@built_in('seq') +def _(args: Sequence[Form]) -> Form: + match args: + case [List() as seq]: + return seq if seq else Nil.NIL + case [Vector() as seq]: + return List(seq) if seq else Nil.NIL + case [String(string)]: + return List(String(c) for c in string) if string else Nil.NIL + case [Nil()]: + return Nil.NIL + case _: + raise Error('bad arguments') + + +@built_in('conj') +def conj(args: Sequence[Form]) -> Form: + match args: + case [Vector() as seq, *forms]: + return Vector((*seq, *forms)) + case [List() as seq, *forms]: + return List((*reversed(forms), *seq)) + case _: + raise Error('bad arguments') + + +@built_in('get') +def _(args: Sequence[Form]) -> Form: + match args: + case [Map() as mapping, Keyword() | String() as key]: + return mapping.get(key, Nil.NIL) + case [Nil(), Keyword() | String()]: + return Nil.NIL + case _: + raise Error('bad arguments') + + +@built_in('first') +def _(args: Sequence[Form]) -> Form: + match args: + case [List() | Vector() as seq]: + return seq[0] if seq else Nil.NIL + case [Nil()]: + return Nil.NIL + case _: + raise Error('bad arguments') + + +@built_in('rest') +def _(args: Sequence[Form]) -> Form: + match args: + case [List() | Vector() as seq]: + return List(seq[1:]) + case [Nil()]: + return List() + case _: + raise Error('bad arguments') + + +@built_in('hash-map') +def _(args: Sequence[Form]) -> Form: + return Map(Map.cast_items(args)) + + +@built_in('assoc') +def _(args: Sequence[Form]) -> Form: + match args: + case [Map() as mapping, *binds]: + return Map(itertools.chain(mapping.items(), Map.cast_items(binds))) + case _: + raise Error('bad arguments') + + +@built_in('contains?') +def _(args: Sequence[Form]) -> Form: + match args: + case [Map() as mapping, Keyword() | String() as key]: + return Boolean(key in mapping) + case _: + raise Error('bad arguments') + + +@built_in('keys') +def _(args: Sequence[Form]) -> Form: + match args: + case [Map() as mapping]: + return List(mapping.keys()) + case _: + raise Error('bad arguments') + + +@built_in('vals') +def _(args: Sequence[Form]) -> Form: + match args: + case [Map() as mapping]: + return List(mapping.values()) + case _: + raise Error('bad arguments') + + +@built_in('dissoc') +def _(args: Sequence[Form]) -> Form: + match args: + case [Map() as mapping, *keys]: + result = Map(mapping) + for key in keys: + if not isinstance(key, (Keyword, String)): + raise Error(f'{key} is not a valid map key') + if key in result: + del result[key] + return result + case _: + raise Error('bad arguments') + + +@built_in('swap!') +def _(args: Sequence[Form]) -> Form: + match args: + case [Atom(old) as atm, Fn(call), *more]: + new = call((old, *more)) + atm.val = new + return new + case _: + raise Error('bad arguments') + + +@built_in('py!*') +def _(args: Sequence[Form]) -> Form: + match args: + case [String(python_statement)]: + # pylint: disable-next=exec-used + exec(compile(python_statement, '', 'single'), globals()) + return Nil.NIL + case _: + raise Error('bad arguments') + + +def py2mal(obj: typing.Any) -> Form: + match obj: + case None: + return Nil.NIL + case bool(): + return Boolean(obj) + case int(): + return Number(obj) + case str(): + return String(obj) + case Sequence(): + return List(py2mal(x) for x in obj) + case collections.abc.Mapping(): + result = Map() + for py_key, py_val in obj.items(): + key = py2mal(py_key) + if not isinstance(key, (Keyword, String)): + raise Error(f'{key} is not a valid map key') + result[key] = py2mal(py_val) + return Map() + case _: + raise Error(f'failed to translate {obj}') + + +@built_in('py*') +def _(args: Sequence[Form]) -> Form: + match args: + case [String(python_expression)]: + # pylint: disable-next=eval-used + return py2mal(eval(python_expression)) + case _: + raise Error('bad arguments') diff --git a/impls/python.2/env.py b/impls/python.2/env.py index a8dd3962d3..4ed0b84984 100644 --- a/impls/python.2/env.py +++ b/impls/python.2/env.py @@ -1,42 +1,20 @@ -from typing import Optional, Dict, List +# Env is defined in mal_types.py in order to avoid a circular dependency. +from collections.abc import Sequence -from mal_types import MalExpression, MalSymbol, MalList, MalUnknownSymbolException +from mal_types import Env, Error, Form, List, pr_seq -class Env(object): - """MAL Environment""" - - def __init__( - self, - outer: Optional["Env"], - binds: Optional[List[MalExpression]] = None, - exprs: Optional[List[MalExpression]] = None, - ) -> None: - self._outer = outer - self._data: Dict[str, MalExpression] = {} - if binds is not None and exprs is not None: - for x in range(0, len(binds)): - assert isinstance(binds[x], MalSymbol) - if binds[x].native() == "&": - self.set(str(binds[x + 1]), MalList(exprs[x:])) - break - else: - self.set(str(binds[x]), exprs[x]) - - def set(self, key: str, value: MalExpression) -> MalExpression: - self._data[key] = value - return value - - def get(self, key: str) -> Optional["Env"]: - if key in self._data: - return self._data[key] - if self._outer is not None: - return self._outer.get(key) - return None - - def __repr__(self) -> str: - env_str = "{" - for d in self._data: - env_str += str(d) + ": " + str(self._data[d]) + ", " - env_str += "}" - return f"environment: (data: {env_str} outer: {repr(self._outer) if self._outer is not None else 'None'})" +def call_env(env: Env, parms: Sequence[str], args: Sequence[Form]) -> Env: + match parms: + case [*required, '&', rest]: + if len(args) < len(required): + raise Error('not enough arguments for fn*[' + + ' '.join(parms) + ']: ' + pr_seq(args)) + fn_env = env.new_child(dict(zip(required, args))) + fn_env[rest] = List(args[len(required):]) + return fn_env + case _: + if len(args) != len(parms): + raise Error('bad argument count for fn*[' + + ' '.join(parms) + ']: ' + pr_seq(args)) + return env.new_child(dict(zip(parms, args))) diff --git a/impls/python.2/mal_readline.py b/impls/python.2/mal_readline.py new file mode 100644 index 0000000000..13b286afba --- /dev/null +++ b/impls/python.2/mal_readline.py @@ -0,0 +1,21 @@ +# Importing this module is sufficient for the 'input' builtin command +# to support readline. + +import atexit +import os.path +import readline + +histfile = os.path.join(os.path.expanduser('~'), '.mal-history') +try: + readline.read_history_file(histfile) +except FileNotFoundError: + pass +readline.set_history_length(1000) +atexit.register(readline.write_history_file, histfile) + + +def input_(prompt: str) -> str: + line = input(prompt) + if line: + readline.add_history(line) + return line diff --git a/impls/python.2/mal_types.py b/impls/python.2/mal_types.py index b7f7cfd615..25645099f5 100644 --- a/impls/python.2/mal_types.py +++ b/impls/python.2/mal_types.py @@ -1,289 +1,185 @@ -from typing import Callable, Dict, List, Any +# Named mal_types because 'types' is already a standard python module. +import collections +import dataclasses +import enum +import itertools +import re +import typing +from collections.abc import Callable, Iterable, Iterator, Mapping, Sequence -class MalExpression(object): - def __init__(self): - assert False # cannot instantiate +# The selected representations ensure that the Python == equality +# matches the MAL = equality. - def native(self) -> Any: - """Return a shallow native Python equivalent for the expression. +# pr_str is implemented here without printer.py because +# __str__ is idiomatic and gives formatted error messages soon +# (that is, without circular dependencies or evil tricks). +# So there are three ways to format a MAL object. +# str(form) +# the default, used by pr_seq or format strings like f'{form}' +# implemented by form.__str__(readably=True) +# form.__str__(readably=False) +# used by some core functions via pr_seq +# implemented by form.__str__(readably=False) +# repr(form) +# the python representation for debugging - For example, (1 2 3) might become [MalInt(1), MalInt(2), MalInt(3)]""" - pass - def __str__(self) -> str: - return self.readable_str() +class Nil(enum.Enum): + NIL = None - def readable_str(self) -> str: - """Return a human-readable (preferably Mal input format) form of the expression.""" - pass + def __str__(self, readably: bool = True) -> str: + return 'nil' - def unreadable_str(self) -> str: - """Returns an unescaped/raw str. Defaults to being the same as readable_str.""" - return self.readable_str() +class Boolean(enum.Enum): + FALSE = False + TRUE = True -class MalString(MalExpression): - def __init__( - self, input_value: str, is_already_encoded: bool = False, keyword: bool = False - ) -> None: - # print("STR: " + input_value) - if is_already_encoded: - self._value = input_value - if keyword: - self._value = "\u029e" + input_value - else: - self._value = input_value + def __str__(self, readably: bool = True) -> str: + return 'true' if self is self.TRUE else 'false' - def readable_str(self) -> str: - if self.is_keyword(): - return ":" + self._value[1:] - else: - val = self._value - val = val.replace("\\", "\\\\") # escape backslashes - val = val.replace("\n", "\\n") # escape newlines - val = val.replace('"', '\\"') # escape quotes - val = '"' + val + '"' # add surrounding quotes - return val +class Number(int): - def unreadable_str(self) -> str: - if self.is_keyword(): - return ":" + self._value[1:] - else: - return self._value + def __str__(self, readably: bool = True) -> str: + return super().__str__() - def native(self) -> Any: - return self._value - def is_keyword(self) -> bool: - return len(self._value) > 1 and self._value[0] == "\u029e" +class Symbol(str): + def __str__(self, readably: bool = True) -> str: + # pylint: disable=invalid-str-returned + return self -class MalList(MalExpression): - def __init__(self, values: List[MalExpression]) -> None: - for x in values: - assert isinstance(x, MalExpression) - self._values = values - - def readable_str(self) -> str: - return "(" + " ".join(map(lambda x: x.readable_str(), self._values)) + ")" - - def unreadable_str(self) -> str: - return "(" + " ".join(map(lambda x: x.unreadable_str(), self._values)) + ")" - - def native(self) -> List[MalExpression]: - return self._values - - -class MalSymbol(MalExpression): - def __init__(self, value: str) -> None: - assert type(value) is str - - self._value = str(value) - - def readable_str(self) -> str: - return str(self._value) - - def eval(self, environment) -> MalExpression: - # print("Evaluating: " + repr(self)) - return environment.get(self) - - def native(self) -> str: - return self._value - - -class MalException(Exception, MalExpression): - def __init__(self, value: MalExpression) -> None: - self._value = value - - def readable_str(self) -> str: - return str(self._value) - - def native(self) -> MalExpression: - return self._value - - -class MalIndexError(MalException): - def __init__(self, index: int) -> None: - super().__init__(MalString("Index out of bounds: " + str(index))) - - -class MalSyntaxException(MalException): - def __init__(self, message) -> None: - super().__init__(MalString(message)) - - -class MalUnknownTypeException(MalException): - def __init__(self, message) -> None: - super().__init__(MalString(message)) - - -class MalInvalidArgumentException(MalException): - def __init__(self, arg: MalExpression, reason: str) -> None: - super().__init__( - MalString(arg.readable_str() + ": invalid argument: " + reason) - ) - - -class MalUnknownSymbolException(MalException): - def __init__(self, func: str) -> None: - super().__init__(MalString("'" + func + "' not found")) - self.func = func - - -class MalNotImplementedException(MalException): - def __init__(self, func: str) -> None: - super().__init__(MalString("not implemented: " + func)) - - -class MalFunctionCompiled(MalExpression): - def __init__( - self, native_function: Callable[[List[MalExpression]], MalExpression] - ) -> None: - self._native_function = native_function - self._is_macro = False - - def readable_str(self): - return "#" if self._is_macro else "#" - - def native(self) -> Callable[[List[MalExpression]], MalExpression]: - return self._native_function - - def call(self, args: List[MalExpression]) -> MalExpression: - # print("CALL: " + str([str(arg) for arg in args])) - return self._native_function(args) - def is_macro(self) -> bool: - return self._is_macro +# The two other string types are wrapped in dataclasses in order to +# avoid problems with == (symbols) and pattern matching (list and +# vectors). +@dataclasses.dataclass(frozen=True, slots=True) +class String: + val: str - def make_macro(self) -> None: - self._is_macro = True + @staticmethod + def _repl(match: re.Match[str]) -> str: + char = match.group() + return '\\' + ('n' if char == '\n' else char) + def __str__(self, readably: bool = True) -> str: + return '"' + re.sub(r'[\\"\n]', String._repl, self.val) + '"' \ + if readably else self.val -class MalFunctionRaw(MalExpression): - def __init__( - self, - fn: Callable[[List[MalExpression]], MalExpression], - ast: MalExpression, - params: MalList, - env, - ) -> None: - self._ast = ast - self._params = params - self._env = env - self._native_function = fn - self._is_macro = False - def readable_str(self): - return "#" if self._is_macro else "#" +@dataclasses.dataclass(frozen=True, slots=True) +class Keyword: + val: str - def ast(self) -> MalExpression: - return self._ast + def __str__(self, readably: bool = True) -> str: + return ':' + self.val - def params(self) -> MalList: - return self._params - def env(self): - return self._env +class List(tuple['Form', ...]): + # Avoid a name clash with typing.List. This improves mypy output. - def native(self) -> Callable[[List[MalExpression]], MalExpression]: - return self._native_function + def __init__(self, _: Iterable['Form'] = (), + meta: 'Form' = Nil.NIL) -> None: + """Add a meta field, tuple.__new__ does the rest.""" + self.meta = meta - def call(self, args: List[MalExpression]) -> MalExpression: - return self._native_function(args) + def __str__(self, readably: bool = True) -> str: + return '(' + pr_seq(self, readably) + ')' - def is_macro(self) -> bool: - return self._is_macro - def make_macro(self) -> None: - self._is_macro = True +class Vector(tuple['Form', ...]): + def __init__(self, _: Iterable['Form'] = (), + meta: 'Form' = Nil.NIL) -> None: + """Add a meta field, tuple.__new__ does the rest.""" + self.meta = meta -class MalInt(MalExpression): - def __init__(self, value: int) -> None: - assert type(value) is int - self._value = value + def __str__(self, readably: bool = True) -> str: + return '[' + pr_seq(self, readably) + ']' - def readable_str(self) -> str: - return str(self._value) - def native(self) -> int: - return self._value +class Map(dict[Keyword | String, 'Form']): + def __init__(self, + arg: Iterable[tuple[Keyword | String, 'Form']] + | Mapping[Keyword | String, 'Form'] = (), + meta: 'Form' = Nil.NIL, + ) -> None: + dict.__init__(self, arg) + self.meta = meta -class MalVector(MalExpression): - def __init__(self, values: List[MalExpression]) -> None: - self._values = values + def __str__(self, readably: bool = True) -> str: + return '{' + pr_seq(itertools.chain.from_iterable(self.items()), + readably) + '}' - def readable_str(self) -> str: - return "[" + " ".join(map(lambda x: x.readable_str(), self._values)) + "]" + @staticmethod + def cast_items(args: Iterable['Form'] + ) -> Iterator[tuple[Keyword | String, 'Form']]: + key: Keyword | String | None = None + for form in args: + if key: + yield key, form + key = None + elif isinstance(form, (Keyword, String)): + key = form + else: + raise Error(f'{key} is not a valid map key') + if key: + raise Error(f'odd count in map binds, no value for {form}') - def unreadable_str(self) -> str: - return "[" + " ".join(map(lambda x: x.unreadable_str(), self._values)) + "]" - def native(self) -> List[MalExpression]: - return self._values +Env = collections.ChainMap[str, 'Form'] +PythonCall = Callable[[Sequence['Form']], 'Form'] -class MalHash_map(MalExpression): - def __init__(self, values: Dict[str, MalExpression]) -> None: - self._dict = values.copy() +class TCOEnv(typing.NamedTuple): + body: 'Form' + fenv: Callable[[Sequence['Form']], Env] - def readable_str(self) -> str: - result_list: List[str] = [] - for x in self._dict: - result_list.append(MalString(x).readable_str()) - result_list.append(self._dict[x].readable_str()) - return "{" + " ".join(result_list) + "}" - def unreadable_str(self) -> str: - result_list: List[str] = [] - for x in self._dict: - result_list.append(MalString(x, is_already_encoded=True).unreadable_str()) - result_list.append(self._dict[x].unreadable_str()) - return "{" + " ".join(result_list) + "}" +@dataclasses.dataclass(frozen=True, slots=True) +class Fn: + call: PythonCall + tco_env: TCOEnv | None = None + meta: 'Form' = Nil.NIL - def native(self) -> Dict[str, MalExpression]: - return self._dict + def __str__(self, readably: bool = True) -> str: + return '#' -class MalNil(MalExpression): - def __init__(self) -> None: - pass +@dataclasses.dataclass(frozen=True, slots=True) +class Macro: + call: PythonCall - def readable_str(self) -> str: - return "nil" + def __str__(self, readably: bool = True) -> str: + return '#' - def eval(self, environment) -> MalExpression: - return self - def native(self) -> None: - return None +@dataclasses.dataclass(slots=True) +class Atom: + val: 'Form' + def __str__(self, readably: bool = True) -> str: + return f'(atom {self.val})' -class MalBoolean(MalExpression): - def __init__(self, value: bool) -> None: - self._value = value - def readable_str(self) -> str: - if self._value: - return "true" - return "false" +Form = (Atom | Boolean | Fn | Keyword | Macro | List + | Map | Nil | Number | String | Symbol | Vector) - def native(self) -> bool: - return self._value +class Error(Exception): + """Local exceptions, as recommended by pylint.""" -class MalAtom(MalExpression): - def __init__(self, value: MalExpression) -> None: - self._value = value - def native(self) -> MalExpression: - return self._value +@dataclasses.dataclass(frozen=True, slots=True) +class ThrownException(Exception): + form: Form - def readable_str(self) -> str: - return "(atom " + str(self._value) + ")" - def reset(self, value: MalExpression) -> None: - self._value = value +def pr_seq(args: Iterable[Form], readably: bool = True, sep: str = ' ') -> str: + # This would be OK if the signature was usual. + # pylint: disable-next=unnecessary-dunder-call + return sep.join(x.__str__(readably) for x in args) diff --git a/impls/python.2/reader.py b/impls/python.2/reader.py index bdc12fdbe2..28aed158bb 100644 --- a/impls/python.2/reader.py +++ b/impls/python.2/reader.py @@ -1,209 +1,175 @@ -from typing import Dict +import re +from collections.abc import Callable, Iterator, Mapping +from re import Match -from arpeggio import ( # type: ignore - ParserPython, - PTNodeVisitor, - visit_parse_tree, - ZeroOrMore, -) -from arpeggio import RegExMatch as _, NoMatch # type: ignore +from mal_types import (Boolean, Error, Form, Keyword, List, Map, Nil, + Number, String, Symbol, Vector) -from mal_types import ( - MalExpression, - MalInt, - MalList, - MalBoolean, - MalNil, - MalVector, - MalHash_map, -) -from mal_types import MalSymbol, MalString, MalSyntaxException +# The `token` decorator adds regular expression groups all along this file. +# The name of a group is the name of the decorated funtion, allowing +# `read_form` to call it when it founds the token. +# The global regular expression is compiled once when the module is loaded. +token_groups: list[str] = [] -# Arpeggio grammar -def mExpression(): - return [ - mWithMetaExpression, - mQuotedExpression, - mQuasiQuotedExpression, - mSpliceUnquotedExpression, - mUnquotedExpression, - mDerefExpression, - mList, - mVector, - mHash_map, - mInt, - mString, - mKeyword, - mNil, - mBoolean, - mSymbol, - ] +class Lexer: + # Consume unnamed groups, but do not report them. + # Report None at the end of the input. + def __init__(self, source: str) -> None: + self._tokens = (t for t in pattern.finditer(source) if t.lastgroup) + self._peek: Match[str] | None = None + self.consume() -def mWithMetaExpression(): - return "^", mExpression, mExpression + def consume(self) -> None: + try: + self._peek = next(self._tokens) + except StopIteration: + self._peek = None + def peek(self) -> re.Match[str] | None: + return self._peek -def mQuotedExpression(): - return "'", mExpression +def token(regex: str): + """Bind a regular expression to a function in this module. Form constuctor. -def mQuasiQuotedExpression(): - return "`", mExpression + The lexer does not report tokens with None as constructor. + """ + def decorator(fun: Callable[[Lexer, Match[str]], Form] | None): + if fun: + group = f'(?P<{fun.__name__}>{regex})' + else: + group = f'(?:{regex})' + token_groups.append(group) + return fun -def mSpliceUnquotedExpression(): - return "~@", mExpression + return decorator -def mUnquotedExpression(): - return "~", mExpression +def context(match: Match[str]) -> str: + """Format some information for error reporting.""" + start_idx = match.start() - 10 + if 0 < start_idx: + start = '...' + match.string[start_idx:match.start()] + else: + start = match.string[:match.start()] + end_idx = match.end() + 20 + if end_idx < len(match.string): + end = match.string[match.end():end_idx] + '...' + else: + end = match.string[match.end():] + return f': {start}{match.group()}{end}' -def mDerefExpression(): - return "@", mExpression +token(r'(?:[\s,]|;[^\n\r]*)+')(None) -def mList(): - return "(", ZeroOrMore(mExpression), ")" +def unescape(match: Match[str]) -> str: + """Map a backslash sequence to a character for strings.""" + char = match.string[match.end() - 1] + return '\n' if char == 'n' else char -def mVector(): - return "[", ZeroOrMore(mExpression), "]" +@token(r'"(?:(?:[^"\\]|\\.)*")?') +def string(_: Lexer, tok: Match[str]) -> Form: + start, end = tok.span() + if end - start == 1: + raise Error('read: unbalanced string delimiter' + context(tok)) + return String(re.sub(r'\\.', unescape, tok.string[start + 1:end - 1])) -def mHash_map(): - return ("{", ZeroOrMore(mExpression), "}") +def read_list(lexer: Lexer, closing: str, pos: Match[str]) -> Iterator[Form]: + while not ((tok := lexer.peek()) and tok.group() == closing): + yield read_form(lexer, pos) + lexer.consume() -def mInt(): - return _(r"-?[0123456789]+") +@token(r'\(') +def list_start(lexer: Lexer, tok: Match[str]) -> Form: + return List(read_list(lexer, ')', tok)) -def mString(): - return _(r""""(?:\\.|[^\\"])*"?""") +@token(r'\[') +def vector_start(lexer: Lexer, tok: Match[str]) -> Form: + return Vector(read_list(lexer, ']', tok)) -def mKeyword(): - return _(r""":[^\s\[\]{}('"`,;)]*""") +@token(r'\{') +def map_start(lexer: Lexer, tok: Match[str]) -> Form: + return Map(Map.cast_items(read_list(lexer, '}', tok))) -def mSymbol(): - return _(r"""[^\s\[\]{}('"`,;)]*""") +single_macros = { + "'": 'quote', + '`': 'quasiquote', + '@': 'deref', + '~': 'unquote', + '~@': 'splice-unquote', +} -def mNil(): - return _(r"""nil(?!\?)""") +@token("['`@]|~@?") +def macro(lexer: Lexer, tok: Match[str]) -> Form: + return List((Symbol(single_macros[tok.group()]), read_form(lexer, tok))) -def mBoolean(): - return _(r"""(true|false)(?!\?)""") +@token(r'\^') +def with_meta(lexer: Lexer, tok: Match[str]) -> Form: + tmp = read_form(lexer, tok) + return List((Symbol('with-meta'), read_form(lexer, tok), tmp)) -class ReadASTVisitor(PTNodeVisitor): - def visit_mExpression(self, node, children) -> MalExpression: - return children[0] # children should already be Mal types +@token('[])}]') +def list_end(_: Lexer, tok: Match[str]) -> Form: + raise Error('read: unbalanced list/vector/map terminator' + context(tok)) - def visit_mInt(self, node, children) -> MalInt: - return MalInt(int(node.value)) - def visit_mString(self, node, children) -> MalString: - # node.value will have quotes, escape sequences - assert type(node.value) is str - if node.value[0] != '"': - raise Exception("internal error: parsed a string with no start quote") - val: str = node.value - if len(val) < 2 or val[-1] != '"': - raise MalSyntaxException("unbalanced string") - val = val[1:-1] # remove outer quotes +@token(r'-?\d+') +def number(_: Lexer, tok: Match[str]) -> Form: + return Number(tok.group()) - # handle escaped characters - i = 0 - result = "" - while i < len(val): - if val[i] == "\\": - if (i + 1) < len(val): - if val[i + 1] == "n": - result += "\n" - elif val[i + 1] == "\\": - result += "\\" - elif val[i + 1] == '"': - result += '"' - i += 2 - else: - raise MalSyntaxException( - "unbalanced string or invalid escape sequence" - ) - else: - result += val[i] - i += 1 - return MalString(result) +almost_symbols: Mapping[str, Form] = { + 'nil': Nil.NIL, + 'false': Boolean.FALSE, + 'true': Boolean.TRUE, +} - def visit_mKeyword(self, node, children) -> MalString: - assert type(node.value) is str - assert len(node.value) > 1 - return MalString(node.value[1:], keyword=True) - def visit_mList(self, node, children) -> MalList: - return MalList(children) +@token(r"""[^]\s"'(),;@[^`{}~]+""") +def symbol(_: Lexer, tok: Match[str]) -> Form: + start, end = tok.span() + if tok.string[start] == ':': + return Keyword(tok.string[start + 1:end]) + value = tok.group() + return almost_symbols.get(value) or Symbol(value) - def visit_mVector(self, node, children) -> MalVector: - return MalVector(children) - def visit_mHash_map(self, node, children): - assert len(children) % 2 == 0 - dict = {} # type: Dict[MalExpression, MalExpression] - for i in range(0, len(children), 2): - assert isinstance(children[i], MalString) - dict[children[i].native()] = children[i + 1] - return MalHash_map(dict) +@token('.') +def should_never_match(lexer: Lexer, tok: Match[str]) -> Form: + assert False, f'{lexer} {tok}' - def visit_mSymbol(self, node, children) -> MalSymbol: - return MalSymbol(node.value) - def visit_mBoolean(self, node, children) -> MalBoolean: - if node.value == "true": - return MalBoolean(True) - if node.value == "false": - return MalBoolean(False) - raise Exception("Internal reader error") +def read_form(lexer: Lexer, pos: Match[str] | None) -> Form: + """Parse a form from `lexer`, reporting errors as if started from `pos`.""" + if (tok := lexer.peek()): + lexer.consume() + assert tok.lastgroup, f'{lexer} {tok}' + assert tok.lastgroup in globals(), f'{lexer} {tok}' + return globals()[tok.lastgroup](lexer, tok) + if pos: + raise Error('read: unbalanced form, started' + context(pos)) + raise Error('read: the whole input was empty') - def visit_mNil(self, node, children) -> MalNil: - return MalNil() - def visit_mWithMetaExpression(self, node, children) -> MalList: - return MalList([MalSymbol("with-meta"), children[1], children[0]]) +def read(source: str) -> Form: + lexer = Lexer(source) + result = read_form(lexer, None) + if tok := lexer.peek(): + raise Error('read: trailing items after the form' + context(tok)) + return result - def visit_mQuotedExpression(self, node, children) -> MalList: - return MalList([MalSymbol("quote"), children[0]]) - def visit_mQuasiQuotedExpression(self, node, children) -> MalList: - return MalList([MalSymbol("quasiquote"), children[0]]) - - def visit_mSpliceUnquotedExpression(self, node, children) -> MalList: - return MalList([MalSymbol("splice-unquote"), children[0]]) - - def visit_mUnquotedExpression(self, node, children) -> MalList: - return MalList([MalSymbol("unquote"), children[0]]) - - def visit_mDerefExpression(self, node, children) -> MalList: - return MalList([MalSymbol("deref"), children[0]]) - - -def comment(): - return _(";.*") - - -def read(x: str) -> MalExpression: - """Parse a string into a MalExpression""" - reader = ParserPython(mExpression, comment_def=comment, ws="\t\n\r ,", debug=False) - - try: - parsed = visit_parse_tree(reader.parse(x), ReadASTVisitor()) - assert issubclass(type(parsed), MalExpression) - return parsed - except NoMatch as e: - # print(str(e)) - raise MalSyntaxException("invalid syntax or unexpected EOF") +pattern = re.compile('|'.join(token_groups)) diff --git a/impls/python.2/run b/impls/python.2/run index d5f8d5d146..f82a47e536 100755 --- a/impls/python.2/run +++ b/impls/python.2/run @@ -1,2 +1,2 @@ #!/bin/bash -exec python3 -O $(dirname $0)/${STEP:-stepA_mal}.py "${@}" +exec python3 $(dirname $0)/${STEP:-stepA_mal}.py "${@}" diff --git a/impls/python.2/step0_repl.py b/impls/python.2/step0_repl.py index e323a6d861..238f6e0b91 100644 --- a/impls/python.2/step0_repl.py +++ b/impls/python.2/step0_repl.py @@ -1,28 +1,29 @@ -import readline +import mal_readline -def READ(x: str) -> str: - return x +def read(source: str) -> str: + return source -def EVAL(x: str) -> str: - return x +def eval_(ast: str) -> str: + return ast -def PRINT(x: str) -> str: - return x +def print_(form: str) -> str: + return form -def rep(x: str) -> str: - return PRINT(EVAL(READ(x))) +def rep(source: str) -> str: + return print_(eval_(read(source))) -# repl loop -eof: bool = False -while not eof: - try: - line = input("user> ") - readline.add_history(line) - print(rep(line)) - except EOFError: - eof = True +def main() -> None: + while True: + try: + print(rep(mal_readline.input_('user> '))) + except EOFError: + break + + +if __name__ == '__main__': + main() diff --git a/impls/python.2/step1_read_print.py b/impls/python.2/step1_read_print.py index 1913c145d1..5fa5161420 100644 --- a/impls/python.2/step1_read_print.py +++ b/impls/python.2/step1_read_print.py @@ -1,38 +1,31 @@ -import readline +import traceback -import reader -from mal_types import MalExpression, MalSyntaxException - - -def READ(x: str) -> MalExpression: - return reader.read(x) +import mal_readline +from mal_types import Form -def EVAL(x: MalExpression) -> MalExpression: - return x +import reader -def PRINT(x: MalExpression) -> str: - return str(x) +def eval_(ast: Form) -> Form: + # print(repr(ast)) # the result of read, as python + return ast -def rep(x: str) -> str: - try: - return PRINT(EVAL(READ(x))) - except BaseException: - return "Expression is invalid or unbalanced: " + x +def rep(source: str) -> str: + return str(eval_(reader.read(source))) -if __name__ == "__main__": - # repl loop - eof: bool = False - while not eof: +def main() -> None: + while True: try: - line = input("user> ") - readline.add_history(line) - try: - print(rep(line)) - except MalSyntaxException as e: - print("ERROR: invalid syntax: " + str(e)) + print(rep(mal_readline.input_('user> '))) except EOFError: - eof = True + break + # pylint: disable-next=broad-exception-caught + except Exception as exc: + traceback.print_exception(exc, limit=10) + + +if __name__ == '__main__': + main() diff --git a/impls/python.2/step2_eval.py b/impls/python.2/step2_eval.py index 7c2fcf9ec2..75cf1c66d8 100644 --- a/impls/python.2/step2_eval.py +++ b/impls/python.2/step2_eval.py @@ -1,66 +1,88 @@ -import readline -from typing import Dict +import traceback +from collections.abc import Mapping, Sequence + +import mal_readline + +from mal_types import (Error, Fn, Form, List, + Map, Number, Symbol, + Vector, pr_seq) import reader -from mal_types import MalExpression, MalSymbol -from mal_types import MalFunctionCompiled, MalInt -from mal_types import MalList, MalVector, MalHash_map -from mal_types import MalUnknownSymbolException, MalSyntaxException -repl_env = { - "+": MalFunctionCompiled(lambda a: MalInt(a[0].native() + a[1].native())), - "-": MalFunctionCompiled(lambda a: MalInt(a[0].native() - a[1].native())), - "*": MalFunctionCompiled(lambda a: MalInt(a[0].native() * a[1].native())), - "/": MalFunctionCompiled(lambda a: MalInt(int(a[0].native() / a[1].native()))), -} +Env = Mapping[str, Fn] -def READ(x: str) -> MalExpression: - return reader.read(x) +def eval_(ast: Form, env: Env) -> Form: + # print(f'EVAL: {ast}', repr(ast) + 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]): + match eval_(first, env): + 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 EVAL(ast: MalExpression, env: Dict[str, MalFunctionCompiled]) -> MalExpression: - # print("EVAL: " + str(ast)) - if isinstance(ast, MalSymbol): - try: - return env[str(ast)] - except KeyError: - raise MalUnknownSymbolException(str(ast)) - if isinstance(ast, MalVector): - return MalVector([EVAL(x, env) for x in ast.native()]) - if isinstance(ast, MalHash_map): - new_dict = {} # type: Dict[str, MalExpression] - for key in ast.native(): - new_dict[key] = EVAL(ast.native()[key], env) - return MalHash_map(new_dict) - if not isinstance(ast, MalList): - return ast - if len(ast.native()) == 0: - return ast - f, *args = (EVAL(form, env) for form in ast.native()) - return f.call(args) - - -def PRINT(exp: MalExpression) -> str: - return str(exp) - - -def rep(x: str) -> str: - return PRINT(EVAL(READ(x), repl_env)) - - -if __name__ == "__main__": - # repl loop - eof: bool = False - while not eof: +def add(args: Sequence[Form]) -> Form: + match args: + case [Number(left), Number(right)]: + return Number(left + right) + case _: + raise Error('+: bad arguments' + pr_seq(args)) + + +def sub(args: Sequence[Form]) -> Form: + match args: + case [Number(left), Number(right)]: + return Number(left - right) + case _: + raise Error('-: bad arguments' + pr_seq(args)) + + +def mul(args: Sequence[Form]) -> Form: + match args: + case [Number(left), Number(right)]: + return Number(left * right) + case _: + raise Error('*: bad arguments' + pr_seq(args)) + + +def floordiv(args: Sequence[Form]) -> Form: + match args: + case [Number(left), Number(right)]: + return Number(left // right) + case _: + raise Error('/: bad arguments' + pr_seq(args)) + + +def rep(source: str, env: Env) -> str: + return str(eval_(reader.read(source), env)) + + +def main() -> None: + repl_env: Env = { + '+': Fn(add), '-': Fn(sub), '*': Fn(mul), '/': Fn(floordiv), + } + + while True: try: - line = input("user> ") - readline.add_history(line) - try: - print(rep(line)) - except MalUnknownSymbolException as e: - print("'" + e.func + "' not found") - except MalSyntaxException as e: - print("ERROR: invalid syntax: " + str(e)) + print(rep(mal_readline.input_('user> '), repl_env)) except EOFError: - eof = True + break + # pylint: disable-next=broad-exception-caught + except Exception as exc: + traceback.print_exception(exc, limit=10) + + +if __name__ == '__main__': + main() diff --git a/impls/python.2/step3_env.py b/impls/python.2/step3_env.py index 02689f4f4c..e1641b0f13 100644 --- a/impls/python.2/step3_env.py +++ b/impls/python.2/step3_env.py @@ -1,98 +1,124 @@ -import readline -from typing import Dict +import traceback +from collections.abc import Sequence + +import mal_readline + +from mal_types import (Boolean, Env, Error, Fn, Form, List, + Map, Nil, Number, Symbol, + Vector, pr_seq) import reader -from env import Env -from mal_types import ( - MalExpression, - MalBoolean, MalNil, MalSymbol, - MalInvalidArgumentException, - MalUnknownSymbolException, - MalSyntaxException, -) -from mal_types import MalInt, MalList, MalFunctionCompiled, MalVector, MalHash_map - -repl_env = Env(None) -repl_env.set("+", MalFunctionCompiled(lambda a: MalInt(a[0].native() + a[1].native()))) -repl_env.set("-", MalFunctionCompiled(lambda a: MalInt(a[0].native() - a[1].native()))) -repl_env.set("*", MalFunctionCompiled(lambda a: MalInt(a[0].native() * a[1].native()))) -repl_env.set( - "/", MalFunctionCompiled(lambda a: MalInt(int(a[0].native() / a[1].native()))) -) - - -def READ(x: str) -> MalExpression: - return reader.read(x) - - -def EVAL(ast: MalExpression, env: Env) -> MalExpression: - dbgeval = env.get("DEBUG-EVAL") - if (dbgeval is not None - and not isinstance(dbgeval, MalNil) - and (not isinstance(dbgeval, MalBoolean) or dbgeval.native())): - print("EVAL: " + str(ast)) - if isinstance(ast, MalSymbol): - key = str(ast) - val = env.get(key) - if val is None: raise MalUnknownSymbolException(key) - return val - if isinstance(ast, MalVector): - return MalVector([EVAL(x, env) for x in ast.native()]) - if isinstance(ast, MalHash_map): - new_dict = {} # type: Dict[str, MalExpression] - for key in ast.native(): - new_dict[key] = EVAL(ast.native()[key], env) - return MalHash_map(new_dict) - if not isinstance(ast, MalList): - return ast - if len(ast.native()) == 0: - return ast - first = str(ast.native()[0]) - rest = ast.native()[1:] - if first == "def!": - key = str(ast.native()[1]) - value = EVAL(ast.native()[2], env) - return env.set(key, value) - if first == "let*": - assert len(rest) == 2 - let_env = Env(env) - bindings = rest[0] - assert isinstance(bindings, MalList) or isinstance(bindings, MalVector) - bindings_list = bindings.native() - assert len(bindings_list) % 2 == 0 - for i in range(0, len(bindings_list), 2): - assert isinstance(bindings_list[i], MalSymbol) - assert isinstance(bindings_list[i + 1], MalExpression) - let_env.set(str(bindings_list[i]), EVAL(bindings_list[i + 1], let_env)) - expr = rest[1] - return EVAL(expr, let_env) - f, *args = (EVAL(form, env) for form in ast.native()) - try: - return f.call(args) - except AttributeError: - raise MalInvalidArgumentException(f, "attribute error") - - -def PRINT(x: MalExpression) -> str: - return str(x) - - -def rep(x: str) -> str: - return PRINT(EVAL(READ(x), repl_env)) - - -if __name__ == "__main__": - # repl loop - eof: bool = False - while not eof: + + +def eval_def(args: Sequence[Form], env: Env) -> Form: + match args: + case [Symbol() as key, form]: + value = eval_(form, env) + env[key] = value + return value + case _: + raise Error('def!: bad arguments: ' + pr_seq(args)) + + +def eval_let(args: Sequence[Form], env: Env) -> Form: + 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 eval_(form, let_env) + case _: + raise Error('let*: bad arguments: ' + pr_seq(args)) + + +specials = { + 'def!': eval_def, + 'let*': eval_let, +} + + +def eval_(ast: Form, env: Env) -> Form: + 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)): + return spec(args, env) + match eval_(first, env): + 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 add(args: Sequence[Form]) -> Form: + match args: + case [Number(left), Number(right)]: + return Number(left + right) + case _: + raise Error('+: bad arguments' + pr_seq(args)) + + +def sub(args: Sequence[Form]) -> Form: + match args: + case [Number(left), Number(right)]: + return Number(left - right) + case _: + raise Error('-: bad arguments' + pr_seq(args)) + + +def mul(args: Sequence[Form]) -> Form: + match args: + case [Number(left), Number(right)]: + return Number(left * right) + case _: + raise Error('*: bad arguments' + pr_seq(args)) + + +def floordiv(args: Sequence[Form]) -> Form: + match args: + case [Number(left), Number(right)]: + return Number(left // right) + case _: + raise Error('/: bad arguments' + pr_seq(args)) + + +def rep(source: str, env: Env) -> str: + return str(eval_(reader.read(source), env)) + + +def main() -> None: + repl_env = Env({ + '+': Fn(add), '-': Fn(sub), '*': Fn(mul), '/': Fn(floordiv), + }) + + while True: try: - line = input("user> ") - readline.add_history(line) - try: - print(rep(line)) - except MalUnknownSymbolException as e: - print("'" + e.func + "' not found") - except MalSyntaxException as e: - print("ERROR: invalid syntax: " + str(e)) + print(rep(mal_readline.input_('user> '), repl_env)) except EOFError: - eof = True + break + # pylint: disable-next=broad-exception-caught + except Exception as exc: + traceback.print_exception(exc, limit=10) + + +if __name__ == '__main__': + main() diff --git a/impls/python.2/step4_if_fn_do.py b/impls/python.2/step4_if_fn_do.py index 7bc6d97eb9..49f1375a16 100644 --- a/impls/python.2/step4_if_fn_do.py +++ b/impls/python.2/step4_if_fn_do.py @@ -1,126 +1,137 @@ -import readline -from typing import Dict +import traceback +from collections.abc import Sequence import core + +from env import call_env + +import mal_readline + +from mal_types import (Boolean, Env, Error, Fn, Form, List, + Map, Nil, Symbol, + Vector, pr_seq) + import reader -from env import Env -from mal_types import ( - MalExpression, - MalSymbol, - MalInvalidArgumentException, - MalUnknownSymbolException, - MalSyntaxException, -) -from mal_types import ( - MalList, - MalNil, - MalBoolean, - MalFunctionCompiled, - MalVector, - MalHash_map, -) - -repl_env = Env(None) -for key in core.ns: - repl_env.set(key, core.ns[key]) - - -def READ(x: str) -> MalExpression: - return reader.read(x) - - -def EVAL(ast: MalExpression, env: Env) -> MalExpression: - dbgeval = env.get("DEBUG-EVAL") - if (dbgeval is not None - and not isinstance(dbgeval, MalNil) - and (not isinstance(dbgeval, MalBoolean) or dbgeval.native())): - print("EVAL: " + str(ast)) - if isinstance(ast, MalSymbol): - key = str(ast) - val = env.get(key) - if val is None: raise MalUnknownSymbolException(key) - return val - if isinstance(ast, MalVector): - return MalVector([EVAL(x, env) for x in ast.native()]) - if isinstance(ast, MalHash_map): - new_dict = {} # type: Dict[str, MalExpression] - for key in ast.native(): - new_dict[key] = EVAL(ast.native()[key], env) - return MalHash_map(new_dict) - if not isinstance(ast, MalList): - return ast - if len(ast.native()) == 0: - return ast - first = str(ast.native()[0]) - rest = ast.native()[1:] - if first == "def!": - key = str(ast.native()[1]) - value = EVAL(ast.native()[2], env) - return env.set(key, value) - if first == "let*": - assert len(rest) == 2 - let_env = Env(env) - bindings = rest[0] - assert isinstance(bindings, MalList) or isinstance(bindings, MalVector) - bindings_list = bindings.native() - assert len(bindings_list) % 2 == 0 - for i in range(0, len(bindings_list), 2): - assert isinstance(bindings_list[i], MalSymbol) - assert isinstance(bindings_list[i + 1], MalExpression) - let_env.set(str(bindings_list[i]), EVAL(bindings_list[i + 1], let_env)) - expr = rest[1] - return EVAL(expr, let_env) - if first == "do": - for x in range(0, len(rest) - 1): - EVAL(rest[x], env) - return EVAL(rest[len(rest) - 1], env) - if first == "if": - condition = EVAL(rest[0], env) - - if isinstance(condition, MalNil) or ( - isinstance(condition, MalBoolean) and condition.native() is False - ): - if len(rest) >= 3: - return EVAL(rest[2], env) - else: - return MalNil() - else: - return EVAL(rest[1], env) - if first == "fn*": - - def func_body(x): - func_env = Env(outer=env, binds=rest[0].native(), exprs=x) - return EVAL(rest[1], func_env) - - return MalFunctionCompiled(func_body) - - f, *args = (EVAL(form, env) for form in ast.native()) - try: - return f.call(args) - except AttributeError: - raise MalInvalidArgumentException(f, "attribute error") - - -def PRINT(x: MalExpression) -> str: - return str(x) - - -def rep(x: str) -> str: - return PRINT(EVAL(READ(x), repl_env)) - - -if __name__ == "__main__": - # repl loop - eof: bool = False - while not eof: + + +def eval_def(args: Sequence[Form], env: Env) -> Form: + match args: + case [Symbol() as key, form]: + value = eval_(form, env) + env[key] = value + return value + case _: + raise Error('def!: bad arguments: ' + pr_seq(args)) + + +def eval_let(args: Sequence[Form], env: Env) -> Form: + 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 eval_(form, let_env) + case _: + raise Error('let*: bad arguments: ' + pr_seq(args)) + + +def eval_do(args: Sequence[Form], env: Env) -> Form: + match args: + case [*forms, last]: + for form in forms: + eval_(form, env) + return eval_(last, env) + case _: + raise Error('do: no argument') + + +def eval_if(args: Sequence[Form], env: Env) -> Form: + if 2 <= len(args) <= 3: + if eval_(args[0], env) in (Nil.NIL, Boolean.FALSE): + if len(args) == 3: + return eval_(args[2], env) + return Nil.NIL + return eval_(args[1], env) + raise Error('if: bad argument count: ' + pr_seq(args)) + + +def eval_fn(args: Sequence[Form], env: Env) -> Form: + 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 call(f_args: Sequence[Form]) -> Form: + return eval_(body, call_env(env, parms, f_args)) + + return Fn(call) + case _: + raise Error('fn*: bad arguments: ' + pr_seq(args)) + + +specials = { + 'def!': eval_def, + 'let*': eval_let, + 'do': eval_do, + 'if': eval_if, + 'fn*': eval_fn, +} + + +def eval_(ast: Form, env: Env) -> Form: + 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)): + return spec(args, env) + match eval_(first, env): + 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. + + rep('(def! not (fn* (a) (if a false true)))', repl_env) + + while True: try: - line = input("user> ") - readline.add_history(line) - try: - print(rep(line)) - except MalUnknownSymbolException as e: - print("'" + e.func + "' not found") - except MalSyntaxException as e: - print("ERROR: invalid syntax: " + str(e)) + print(rep(mal_readline.input_('user> '), repl_env)) except EOFError: - eof = True + break + # pylint: disable-next=broad-exception-caught + except Exception as exc: + traceback.print_exception(exc, limit=10) + + +if __name__ == '__main__': + main() diff --git a/impls/python.2/step5_tco.py b/impls/python.2/step5_tco.py index 1b1ab21a0d..4cea3821a6 100644 --- a/impls/python.2/step5_tco.py +++ b/impls/python.2/step5_tco.py @@ -1,144 +1,151 @@ -import readline -from typing import List, Dict +import traceback +from collections.abc import Sequence import core + +from env import call_env + +import mal_readline + +from mal_types import (Boolean, Env, Error, Fn, Form, List, + Map, Nil, Symbol, TCOEnv, + Vector, pr_seq) + import reader -from env import Env -from mal_types import MalExpression, MalSymbol -from mal_types import ( - MalList, - MalNil, - MalBoolean, - MalFunctionCompiled, - MalFunctionRaw, - MalVector, - MalHash_map, -) -from mal_types import ( - MalUnknownSymbolException, - MalSyntaxException, - MalInvalidArgumentException, -) - -repl_env = Env(None) -for key in core.ns: - repl_env.set(key, core.ns[key]) - - -def READ(x: str) -> MalExpression: - return reader.read(x) - - -def EVAL(ast: MalExpression, env: Env) -> MalExpression: + +# 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)) + + +specials = { + 'def!': eval_def, + 'let*': eval_let, + 'do': eval_do, + 'if': eval_if, + 'fn*': eval_fn, +} + + +def eval_(ast: Form, env: Env) -> Form: while True: - dbgeval = env.get("DEBUG-EVAL") - if (dbgeval is not None - and not isinstance(dbgeval, MalNil) - and (not isinstance(dbgeval, MalBoolean) or dbgeval.native())): - print("EVAL: " + str(ast)) - ast_native = ast.native() - if isinstance(ast, MalSymbol): - key = str(ast) - val = env.get(key) - if val is None: raise MalUnknownSymbolException(key) - return val - if isinstance(ast, MalVector): - return MalVector([EVAL(x, env) for x in ast_native]) - if isinstance(ast, MalHash_map): - new_dict = {} # type: Dict[str, MalExpression] - for key in ast_native: - new_dict[key] = EVAL(ast_native[key], env) - return MalHash_map(new_dict) - if not isinstance(ast, MalList): - return ast - elif len(ast_native) == 0: - return ast - - first_str = str(ast_native[0]) - if first_str == "def!": - name: str = str(ast_native[1]) - value: MalExpression = EVAL(ast_native[2], env) - return env.set(name, value) - elif first_str == "let*": - assert len(ast_native) == 3 - let_env = Env(env) - bindings: MalExpression = ast_native[1] - assert isinstance(bindings, MalList) or isinstance(bindings, MalVector) - bindings_list: List[MalExpression] = bindings.native() - assert len(bindings_list) % 2 == 0 - for i in range(0, len(bindings_list), 2): - assert isinstance(bindings_list[i], MalSymbol) - assert isinstance(bindings_list[i + 1], MalExpression) - let_env.set(str(bindings_list[i]), EVAL(bindings_list[i + 1], let_env)) - env = let_env - ast = ast_native[2] - continue - elif first_str == "do": - for x in range(1, len(ast_native) - 1): - EVAL(ast_native[x], env) - ast = ast_native[len(ast_native) - 1] - continue - elif first_str == "if": - condition = EVAL(ast_native[1], env) - - if isinstance(condition, MalNil) or ( - isinstance(condition, MalBoolean) and condition.native() is False - ): - if len(ast_native) >= 4: - ast = ast_native[3] - continue + 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: - return MalNil() - else: - ast = ast_native[2] - continue - elif first_str == "fn*": - raw_ast = ast_native[2] - raw_params = ast_native[1] - - def fn(args: List[MalExpression]) -> MalExpression: - f_ast = raw_ast - f_env = Env(outer=env, binds=raw_params.native(), exprs=args) - return EVAL(f_ast, f_env) - - return MalFunctionRaw(fn=fn, ast=raw_ast, params=raw_params, env=env) - else: - f, *args = (EVAL(form, env) for form in ast_native) - if isinstance(f, MalFunctionRaw): - ast = f.ast() - - env = Env( - outer=f.env(), - binds=f.params().native(), - exprs=args, - ) - continue - elif isinstance(f, MalFunctionCompiled): - return f.call(args) - else: - raise MalInvalidArgumentException(f, "not a function") - - -def PRINT(x: MalExpression) -> str: - return str(x) - - -def rep(x: str) -> str: - return PRINT(EVAL(READ(x), repl_env)) - - -if __name__ == "__main__": - # repl loop - eof: bool = False - while not eof: + match eval_(first, env): + 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. + + rep('(def! not (fn* (a) (if a false true)))', repl_env) + + while True: try: - line = input("user> ") - readline.add_history(line) - try: - print(rep(line)) - except MalUnknownSymbolException as e: - print("'" + e.func + "' not found") - except MalSyntaxException as e: - print("ERROR: invalid syntax: " + str(e)) + print(rep(mal_readline.input_('user> '), repl_env)) except EOFError: - eof = True + break + # pylint: disable-next=broad-exception-caught + except Exception as exc: + traceback.print_exception(exc, limit=10) + + +if __name__ == '__main__': + main() diff --git a/impls/python.2/step6_file.py b/impls/python.2/step6_file.py index ac6c9d6e15..a0c99e4f7a 100644 --- a/impls/python.2/step6_file.py +++ b/impls/python.2/step6_file.py @@ -1,175 +1,167 @@ -import readline import sys -from typing import List, Dict +import traceback +from collections.abc import Sequence import core -import reader -from env import Env -from mal_types import MalExpression, MalSymbol -from mal_types import ( - MalList, - MalNil, - MalBoolean, - MalFunctionCompiled, - MalFunctionRaw, - MalAtom, - MalVector, - MalHash_map, -) -from mal_types import ( - MalUnknownSymbolException, - MalSyntaxException, - MalInvalidArgumentException, - MalString, -) - -repl_env = Env(None) -for key in core.ns: - repl_env.set(key, core.ns[key]) - - -def eval_func(args: List[MalExpression]) -> MalExpression: - a0 = args[0] - assert isinstance(a0, MalExpression) - return EVAL(a0, repl_env) +from env import call_env -repl_env.set("eval", MalFunctionCompiled(lambda args: eval_func(args))) +import mal_readline +from mal_types import (Boolean, Env, Error, Fn, Form, List, + Map, Nil, String, Symbol, TCOEnv, + Vector, pr_seq) -def swap(args: List[MalExpression]) -> MalExpression: - atom = args[0] - assert isinstance(atom, MalAtom) - func = args[1] - atom.reset(EVAL(MalList([func, atom.native()] + args[2:]), repl_env)) - return atom.native() - - -def READ(x: str) -> MalExpression: - return reader.read(x) - +import reader -def EVAL(ast: MalExpression, env: Env) -> MalExpression: +# 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)) + + +specials = { + 'def!': eval_def, + 'let*': eval_let, + 'do': eval_do, + 'if': eval_if, + 'fn*': eval_fn, +} + + +def eval_(ast: Form, env: Env) -> Form: while True: - dbgeval = env.get("DEBUG-EVAL") - if (dbgeval is not None - and not isinstance(dbgeval, MalNil) - and (not isinstance(dbgeval, MalBoolean) or dbgeval.native())): - print("EVAL: " + str(ast)) - ast_native = ast.native() - if isinstance(ast, MalSymbol): - key = str(ast) - val = env.get(key) - if val is None: raise MalUnknownSymbolException(key) - return val - if isinstance(ast, MalVector): - return MalVector([EVAL(x, env) for x in ast_native]) - if isinstance(ast, MalHash_map): - new_dict = {} # type: Dict[str, MalExpression] - for key in ast_native: - new_dict[key] = EVAL(ast_native[key], env) - return MalHash_map(new_dict) - if not isinstance(ast, MalList): - return ast - elif len(ast_native) == 0: - return ast - - first_str = str(ast_native[0]) - if first_str == "def!": - name: str = str(ast_native[1]) - value: MalExpression = EVAL(ast_native[2], env) - return env.set(name, value) - elif first_str == "let*": - assert len(ast_native) == 3 - let_env = Env(env) - bindings: MalExpression = ast_native[1] - assert isinstance(bindings, MalList) or isinstance(bindings, MalVector) - bindings_list: List[MalExpression] = bindings.native() - assert len(bindings_list) % 2 == 0 - for i in range(0, len(bindings_list), 2): - assert isinstance(bindings_list[i], MalSymbol) - assert isinstance(bindings_list[i + 1], MalExpression) - let_env.set(str(bindings_list[i]), EVAL(bindings_list[i + 1], let_env)) - env = let_env - ast = ast_native[2] - continue - elif first_str == "do": - for x in range(1, len(ast_native) - 1): - EVAL(ast_native[x], env) - ast = ast_native[len(ast_native) - 1] - continue - elif first_str == "if": - condition = EVAL(ast_native[1], env) - - if isinstance(condition, MalNil) or ( - isinstance(condition, MalBoolean) and condition.native() is False - ): - if len(ast_native) >= 4: - ast = ast_native[3] - continue + 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: - return MalNil() - else: - ast = ast_native[2] - continue - elif first_str == "fn*": - raw_ast = ast_native[2] - raw_params = ast_native[1] - - def fn(args: List[MalExpression]) -> MalExpression: - f_ast = raw_ast - f_env = Env(outer=env, binds=raw_params.native(), exprs=args) - return EVAL(f_ast, f_env) - - return MalFunctionRaw(fn=fn, ast=raw_ast, params=raw_params, env=env) - else: - f, *args = (EVAL(form, env) for form in ast_native) - if isinstance(f, MalFunctionRaw): - ast = f.ast() - - env = Env( - outer=f.env(), - binds=f.params().native(), - exprs=args, - ) - continue - elif isinstance(f, MalFunctionCompiled): - return f.call(args) - else: - raise MalInvalidArgumentException(f, "not a function") - - -def PRINT(x: MalExpression) -> str: - return str(x) - - -def rep(x: str) -> str: - return PRINT(EVAL(READ(x), repl_env)) - - -if __name__ == "__main__": - # repl loop - eof: bool = False - rep( - '(def! load-file (fn* (f) (eval (read-string (str "(do " (slurp f) "\nnil)")))))' - ) - mal_argv = MalList([MalString(x) for x in sys.argv[2:]]) - repl_env.set("*ARGV*", mal_argv) - - if len(sys.argv) >= 2: - file_str = sys.argv[1] - rep('(load-file "' + file_str + '")') - exit(0) - - while not eof: - try: - line = input("user> ") - readline.add_history(line) - try: - print(rep(line)) - except MalUnknownSymbolException as e: - print("'" + e.func + "' not found") - except MalSyntaxException as e: - print("ERROR: invalid syntax: " + str(e)) - except EOFError: - eof = True + match eval_(first, env): + 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) + 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() diff --git a/impls/python.2/step7_quote.py b/impls/python.2/step7_quote.py index b441a73390..2c9eebe06d 100644 --- a/impls/python.2/step7_quote.py +++ b/impls/python.2/step7_quote.py @@ -1,213 +1,216 @@ import functools -import readline import sys -from typing import List, Dict +import traceback +from collections.abc import Sequence import core + +from env import call_env + +import mal_readline + +from mal_types import (Boolean, Env, Error, Fn, Form, List, + Map, Nil, String, Symbol, TCOEnv, + Vector, pr_seq) + import reader -from env import Env -from mal_types import MalExpression, MalSymbol -from mal_types import ( - MalList, - MalNil, - MalBoolean, - MalFunctionCompiled, - MalFunctionRaw, - MalAtom, - MalVector, - MalHash_map, -) -from mal_types import ( - MalUnknownSymbolException, - MalSyntaxException, - MalInvalidArgumentException, - MalString, -) - -repl_env = Env(None) -for key in core.ns: - repl_env.set(key, core.ns[key]) - - -def eval_func(args: List[MalExpression]) -> MalExpression: - a0 = args[0] - assert isinstance(a0, MalExpression) - return EVAL(a0, repl_env) - - -repl_env.set("eval", MalFunctionCompiled(lambda args: eval_func(args))) - - -def swap(args: List[MalExpression]) -> MalExpression: - atom = args[0] - assert isinstance(atom, MalAtom) - func = args[1] - atom.reset(EVAL(MalList([func, atom.native()] + args[2:]), repl_env)) - return atom.native() - - -def READ(x: str) -> MalExpression: - return reader.read(x) - - -def qq_loop(acc: MalList, elt: MalExpression) -> MalList: - if isinstance(elt, MalList): - lst = elt.native() - if len(lst) == 2: - fst = lst[0] - if isinstance(fst, MalSymbol) and fst.native() == u"splice-unquote": - return MalList([MalSymbol(u"concat"), lst[1], acc]) - return MalList([MalSymbol(u"cons"), quasiquote(elt), acc]) - -def qq_foldr(xs: List[MalExpression]) -> MalList: - return functools.reduce(qq_loop, reversed(xs), MalList([])) - -def quasiquote(ast: MalExpression) -> MalExpression: - if isinstance(ast, MalList): - lst = ast.native() - if len(lst) == 2: - fst = lst[0] - if isinstance(fst, MalSymbol) and fst.native() == u'unquote': - return lst[1] - return qq_foldr(lst) - elif isinstance(ast, MalVector): - return MalList([MalSymbol("vec"), qq_foldr(ast.native())]) - elif isinstance(ast, MalSymbol) or isinstance(ast, MalHash_map): - return MalList([MalSymbol("quote"), ast]) - else: - return ast - - -def EVAL(ast: MalExpression, env: Env) -> MalExpression: - while True: - dbgeval = env.get("DEBUG-EVAL") - if (dbgeval is not None - and not isinstance(dbgeval, MalNil) - and (not isinstance(dbgeval, MalBoolean) or dbgeval.native())): - print("EVAL: " + str(ast)) - ast_native = ast.native() - if isinstance(ast, MalSymbol): - key = str(ast) - val = env.get(key) - if val is None: raise MalUnknownSymbolException(key) - return val - if isinstance(ast, MalVector): - return MalVector([EVAL(x, env) for x in ast_native]) - if isinstance(ast, MalHash_map): - new_dict = {} # type: Dict[str, MalExpression] - for key in ast_native: - new_dict[key] = EVAL(ast_native[key], env) - return MalHash_map(new_dict) - if not isinstance(ast, MalList): - return ast - elif len(ast_native) == 0: + +# 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 _: return ast - first_str = str(ast_native[0]) - if first_str == "def!": - name: str = str(ast_native[1]) - value: MalExpression = EVAL(ast_native[2], env) - return env.set(name, value) - elif first_str == "let*": - assert len(ast_native) == 3 - let_env = Env(env) - bindings: MalExpression = ast_native[1] - assert isinstance(bindings, MalList) or isinstance(bindings, MalVector) - bindings_list: List[MalExpression] = bindings.native() - assert len(bindings_list) % 2 == 0 - for i in range(0, len(bindings_list), 2): - assert isinstance(bindings_list[i], MalSymbol) - assert isinstance(bindings_list[i + 1], MalExpression) - let_env.set(str(bindings_list[i]), EVAL(bindings_list[i + 1], let_env)) - env = let_env - ast = ast_native[2] - continue - elif first_str == "do": - for x in range(1, len(ast_native) - 1): - EVAL(ast_native[x], env) - ast = ast_native[len(ast_native) - 1] - continue - elif first_str == "if": - condition = EVAL(ast_native[1], env) - - if isinstance(condition, MalNil) or ( - isinstance(condition, MalBoolean) and condition.native() is False - ): - if len(ast_native) >= 4: - ast = ast_native[3] - continue + +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)) + + +specials = { + 'def!': eval_def, + 'let*': eval_let, + 'do': eval_do, + 'if': eval_if, + 'fn*': eval_fn, + 'quote': eval_quote, + 'quasiquote': eval_quasiquote, +} + + +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: - return MalNil() - else: - ast = ast_native[2] - continue - elif first_str == "fn*": - raw_ast = ast_native[2] - raw_params = ast_native[1] - - def fn(args: List[MalExpression]) -> MalExpression: - f_ast = raw_ast - f_env = Env(outer=env, binds=raw_params.native(), exprs=args) - return EVAL(f_ast, f_env) - - return MalFunctionRaw(fn=fn, ast=raw_ast, params=raw_params, env=env) - elif first_str == "quote": - return ( - MalList(ast_native[1].native()) - if isinstance(ast_native[1], MalVector) - else ast_native[1] - ) - elif first_str == "quasiquote": - ast = quasiquote(ast_native[1]) - continue - else: - f, *args = (EVAL(form, env) for form in ast_native) - if isinstance(f, MalFunctionRaw): - ast = f.ast() - - env = Env( - outer=f.env(), - binds=f.params().native(), - exprs=args, - ) - continue - elif isinstance(f, MalFunctionCompiled): - return f.call(args) - else: - raise MalInvalidArgumentException(f, "not a function") - - -def PRINT(x: MalExpression) -> str: - return str(x) - - -def rep(x: str) -> str: - return PRINT(EVAL(READ(x), repl_env)) - - -if __name__ == "__main__": - # repl loop - eof: bool = False - rep( - '(def! load-file (fn* (f) (eval (read-string (str "(do " (slurp f) "\nnil)")))))' - ) - mal_argv = MalList([MalString(x) for x in sys.argv[2:]]) - repl_env.set("*ARGV*", mal_argv) - - if len(sys.argv) >= 2: - file_str = sys.argv[1] - rep('(load-file "' + file_str + '")') - exit(0) - - while not eof: - try: - line = input("user> ") - readline.add_history(line) - try: - print(rep(line)) - except MalUnknownSymbolException as e: - print("'" + e.func + "' not found") - except MalSyntaxException as e: - print("ERROR: invalid syntax: " + str(e)) - except EOFError: - eof = True + match eval_(first, env): + 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) + 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() diff --git a/impls/python.2/step8_macros.py b/impls/python.2/step8_macros.py index d34ee0efe2..9fcd1ad34c 100644 --- a/impls/python.2/step8_macros.py +++ b/impls/python.2/step8_macros.py @@ -1,238 +1,236 @@ import functools -import readline import sys -from typing import List, Dict +import traceback +from collections.abc import Sequence import core + +from env import call_env + +import mal_readline + +from mal_types import (Boolean, Env, Error, Fn, Form, List, Macro, + Map, Nil, String, Symbol, TCOEnv, + Vector, pr_seq) + import reader -from env import Env -from mal_types import MalExpression, MalSymbol -from mal_types import ( - MalList, - MalNil, - MalBoolean, - MalFunctionCompiled, - MalFunctionRaw, - MalAtom, - MalVector, - MalHash_map, -) -from mal_types import ( - MalUnknownSymbolException, - MalSyntaxException, - MalInvalidArgumentException, - MalString, - MalException, -) - - -def READ(x: str) -> MalExpression: - return reader.read(x) - - -def qq_loop(acc: MalList, elt: MalExpression) -> MalList: - if isinstance(elt, MalList): - lst = elt.native() - if len(lst) == 2: - fst = lst[0] - if isinstance(fst, MalSymbol) and fst.native() == u"splice-unquote": - return MalList([MalSymbol(u"concat"), lst[1], acc]) - return MalList([MalSymbol(u"cons"), quasiquote(elt), acc]) - -def qq_foldr(xs: List[MalExpression]) -> MalList: - return functools.reduce(qq_loop, reversed(xs), MalList([])) - -def quasiquote(ast: MalExpression) -> MalExpression: - if isinstance(ast, MalList): - lst = ast.native() - if len(lst) == 2: - fst = lst[0] - if isinstance(fst, MalSymbol) and fst.native() == u'unquote': - return lst[1] - return qq_foldr(lst) - elif isinstance(ast, MalVector): - return MalList([MalSymbol("vec"), qq_foldr(ast.native())]) - elif isinstance(ast, MalSymbol) or isinstance(ast, MalHash_map): - return MalList([MalSymbol("quote"), ast]) - else: - return ast - - -def EVAL(ast: MalExpression, env: Env) -> MalExpression: - while True: - dbgeval = env.get("DEBUG-EVAL") - if (dbgeval is not None - and not isinstance(dbgeval, MalNil) - and (not isinstance(dbgeval, MalBoolean) or dbgeval.native())): - print("EVAL: " + str(ast)) - ast_native = ast.native() - if isinstance(ast, MalSymbol): - key = str(ast) - val = env.get(key) - if val is None: raise MalUnknownSymbolException(key) - return val - if isinstance(ast, MalVector): - return MalVector([EVAL(x, env) for x in ast_native]) - if isinstance(ast, MalHash_map): - new_dict = {} # type: Dict[str, MalExpression] - for key in ast_native: - new_dict[key] = EVAL(ast_native[key], env) - return MalHash_map(new_dict) - if not isinstance(ast, MalList): - return ast - elif len(ast_native) == 0: + +# 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 _: return ast - first_str = str(ast_native[0]) - if first_str == "def!": - name: str = str(ast_native[1]) - value: MalExpression = EVAL(ast_native[2], env) - return env.set(name, value) - if first_str == "defmacro!": - name = str(ast_native[1]) - value = EVAL(ast_native[2], env) - assert isinstance(value, MalFunctionCompiled) or isinstance( - value, MalFunctionRaw - ) - value.make_macro() - return env.set(name, value) - elif first_str == "let*": - assert len(ast_native) == 3 - let_env = Env(env) - bindings: MalExpression = ast_native[1] - assert isinstance(bindings, MalList) or isinstance(bindings, MalVector) - bindings_list: List[MalExpression] = bindings.native() - assert len(bindings_list) % 2 == 0 - for i in range(0, len(bindings_list), 2): - assert isinstance(bindings_list[i], MalSymbol) - assert isinstance(bindings_list[i + 1], MalExpression) - let_env.set(str(bindings_list[i]), EVAL(bindings_list[i + 1], let_env)) - env = let_env - ast = ast_native[2] - continue - elif first_str == "do": - for x in range(1, len(ast_native) - 1): - EVAL(ast_native[x], env) - ast = ast_native[len(ast_native) - 1] - continue - elif first_str == "if": - condition = EVAL(ast_native[1], env) - - if isinstance(condition, MalNil) or ( - isinstance(condition, MalBoolean) and condition.native() is False - ): - if len(ast_native) >= 4: - ast = ast_native[3] - continue + +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)) + + +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, +} + + +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: - return MalNil() - else: - ast = ast_native[2] - continue - elif first_str == "fn*": - raw_ast = ast_native[2] - raw_params = ast_native[1] - - def fn(args: List[MalExpression]) -> MalExpression: - f_ast = raw_ast - f_env = Env(outer=env, binds=raw_params.native(), exprs=args) - return EVAL(f_ast, f_env) - - return MalFunctionRaw(fn=fn, ast=raw_ast, params=raw_params, env=env) - elif first_str == "quote": - return ( - MalList(ast_native[1].native()) - if isinstance(ast_native[1], MalVector) - else ast_native[1] - ) - elif first_str == "quasiquote": - ast = quasiquote(ast_native[1]) - continue - else: - f = EVAL(ast_native[0], env) - if isinstance(f, (MalFunctionCompiled, MalFunctionRaw)) and f.is_macro(): - ast = f.call(ast_native[1:]) - continue - args = [EVAL(ast_native[i], env) for i in range(1, len(ast_native))] - if isinstance(f, MalFunctionRaw): - ast = f.ast() - - env = Env( - outer=f.env(), - binds=f.params().native(), - exprs=args, - ) - continue - elif isinstance(f, MalFunctionCompiled): - return f.call(args) - else: - raise MalInvalidArgumentException(f, "not a function") - - -def PRINT(x: MalExpression) -> str: - return str(x) - - -def rep(x: str, env: Env) -> str: - return PRINT(EVAL(READ(x), env)) - - -def init_repl_env() -> Env: - def eval_func(args: List[MalExpression], env: Env) -> MalExpression: - a0 = args[0] - assert isinstance(a0, MalExpression) - return EVAL(a0, env) - - def swap(args: List[MalExpression], env: Env) -> MalExpression: - atom = args[0] - assert isinstance(atom, MalAtom) - func = args[1] - atom.reset(EVAL(MalList([func, atom.native()] + args[2:]), env)) - return atom.native() - - repl_env = Env(None) - for key in core.ns: - repl_env.set(key, core.ns[key]) - - repl_env.set("eval", MalFunctionCompiled(lambda args: eval_func(args, repl_env))) - - rep( - '(def! load-file (fn* (f) (eval (read-string (str "(do " (slurp f) "\nnil)")))))', - repl_env, - ) - mal_argv = MalList([MalString(x) for x in sys.argv[2:]]) - repl_env.set("*ARGV*", mal_argv) - - 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, - ) - - return repl_env - - -if __name__ == "__main__": - # repl loop - eof: bool = False - repl_env = init_repl_env() - - if len(sys.argv) >= 2: - file_str = sys.argv[1] - rep('(load-file "' + file_str + '")', repl_env) - exit(0) - - while not eof: - try: - line = input("user> ") - readline.add_history(line) - try: - print(rep(line, repl_env)) - except MalUnknownSymbolException as e: - print("'" + e.func + "' not found") - except MalSyntaxException as e: - print("ERROR: invalid syntax: " + str(e)) - except MalException as e: - print("ERROR: " + str(e)) - - except EOFError: - eof = True + 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() diff --git a/impls/python.2/step9_try.py b/impls/python.2/step9_try.py index 7ce8444475..699b4d1d40 100644 --- a/impls/python.2/step9_try.py +++ b/impls/python.2/step9_try.py @@ -1,253 +1,252 @@ import functools -import readline import sys -from typing import List, Dict +import traceback +from collections.abc import Sequence import core + +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) + import reader -from env import Env -from mal_types import MalExpression, MalSymbol, MalException -from mal_types import ( - MalList, - MalNil, - MalBoolean, - MalFunctionCompiled, - MalFunctionRaw, - MalAtom, - MalVector, - MalHash_map, -) -from mal_types import MalUnknownSymbolException, MalInvalidArgumentException, MalString - - -def READ(x: str) -> MalExpression: - return reader.read(x) - - -def qq_loop(acc: MalList, elt: MalExpression) -> MalList: - if isinstance(elt, MalList): - lst = elt.native() - if len(lst) == 2: - fst = lst[0] - if isinstance(fst, MalSymbol) and fst.native() == u"splice-unquote": - return MalList([MalSymbol(u"concat"), lst[1], acc]) - return MalList([MalSymbol(u"cons"), quasiquote(elt), acc]) - -def qq_foldr(xs: List[MalExpression]) -> MalList: - return functools.reduce(qq_loop, reversed(xs), MalList([])) - -def quasiquote(ast: MalExpression) -> MalExpression: - if isinstance(ast, MalList): - lst = ast.native() - if len(lst) == 2: - fst = lst[0] - if isinstance(fst, MalSymbol) and fst.native() == u'unquote': - return lst[1] - return qq_foldr(lst) - elif isinstance(ast, MalVector): - return MalList([MalSymbol("vec"), qq_foldr(ast.native())]) - elif isinstance(ast, MalSymbol) or isinstance(ast, MalHash_map): - return MalList([MalSymbol("quote"), ast]) - else: - return ast - - -def EVAL(ast: MalExpression, env: Env) -> MalExpression: - while True: - dbgeval = env.get("DEBUG-EVAL") - if (dbgeval is not None - and not isinstance(dbgeval, MalNil) - and (not isinstance(dbgeval, MalBoolean) or dbgeval.native())): - print("EVAL: " + str(ast)) - ast_native = ast.native() - if isinstance(ast, MalSymbol): - key = str(ast) - val = env.get(key) - if val is None: raise MalUnknownSymbolException(key) - return val - if isinstance(ast, MalVector): - return MalVector([EVAL(x, env) for x in ast_native]) - if isinstance(ast, MalHash_map): - new_dict = {} # type: Dict[str, MalExpression] - for key in ast_native: - new_dict[key] = EVAL(ast_native[key], env) - return MalHash_map(new_dict) - if not isinstance(ast, MalList): - return ast - elif len(ast_native) == 0: + +# 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 _: return ast - first_str = str(ast_native[0]) - if first_str == "def!": - name: str = str(ast_native[1]) - value: MalExpression = EVAL(ast_native[2], env) - return env.set(name, value) - if first_str == "defmacro!": - name = str(ast_native[1]) - value = EVAL(ast_native[2], env) - assert isinstance(value, MalFunctionCompiled) or isinstance( - value, MalFunctionRaw - ) - value.make_macro() - return env.set(name, value) - elif first_str == "let*": - assert len(ast_native) == 3 - let_env = Env(env) - bindings: MalExpression = ast_native[1] - assert isinstance(bindings, MalList) or isinstance(bindings, MalVector) - bindings_list: List[MalExpression] = bindings.native() - assert len(bindings_list) % 2 == 0 - for i in range(0, len(bindings_list), 2): - assert isinstance(bindings_list[i], MalSymbol) - assert isinstance(bindings_list[i + 1], MalExpression) - let_env.set(str(bindings_list[i]), EVAL(bindings_list[i + 1], let_env)) - env = let_env - ast = ast_native[2] - continue - elif first_str == "do": - for x in range(1, len(ast_native) - 1): - EVAL(ast_native[x], env) - ast = ast_native[len(ast_native) - 1] - continue - elif first_str == "if": - condition = EVAL(ast_native[1], env) - - if isinstance(condition, MalNil) or ( - isinstance(condition, MalBoolean) and condition.native() is False - ): - if len(ast_native) >= 4: - ast = ast_native[3] - continue - else: - return MalNil() - else: - ast = ast_native[2] - continue - elif first_str == "fn*": - raw_ast = ast_native[2] - raw_params = ast_native[1] - - def fn(args: List[MalExpression]) -> MalExpression: - f_ast = raw_ast - f_env = Env(outer=env, binds=raw_params.native(), exprs=args) - return EVAL(f_ast, f_env) - - return MalFunctionRaw(fn=fn, ast=raw_ast, params=raw_params, env=env) - elif first_str == "quote": - return ( - MalList(ast_native[1].native()) - if isinstance(ast_native[1], MalVector) - else ast_native[1] - ) - elif first_str == "quasiquote": - ast = quasiquote(ast_native[1]) - continue - elif first_str == "try*": + +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])]: try: - return EVAL(ast_native[1], env) - except MalException as e: - if len(ast_native) < 3: - raise e - catch_block = ast_native[2] - assert ( - isinstance(catch_block, MalList) - and isinstance(catch_block.native()[0], MalSymbol) - and str(catch_block.native()[0]) == "catch*" - and len(catch_block.native()) == 3 - ) - exception_symbol = catch_block.native()[1] - assert isinstance(exception_symbol, MalSymbol) - env = Env(env) - env.set(str(exception_symbol), e.native()) - ast = catch_block.native()[2] - continue - else: - f = EVAL(ast_native[0], env) - if isinstance(f, (MalFunctionCompiled, MalFunctionRaw)) and f.is_macro(): - ast = f.call(ast_native[1:]) - continue - args = [EVAL(ast_native[i], env) for i in range(1, len(ast_native))] - if isinstance(f, MalFunctionRaw): - ast = f.ast() - - env = Env( - outer=f.env(), - binds=f.params().native(), - exprs=args, - ) - continue - elif isinstance(f, MalFunctionCompiled): - return f.call(args) - else: - raise MalInvalidArgumentException(f, "not a function") - - -def PRINT(x: MalExpression) -> str: - return str(x) - - -def rep(x: str, env: Env) -> str: - return PRINT(EVAL(READ(x), env)) - - -def init_repl_env() -> Env: - def eval_func(args: List[MalExpression], env: Env) -> MalExpression: - a0 = args[0] - assert isinstance(a0, MalExpression) - return EVAL(a0, env) - - def swap(args: List[MalExpression], env: Env) -> MalExpression: - atom = args[0] - assert isinstance(atom, MalAtom) - func = args[1] - atom.reset(EVAL(MalList([func, atom.native()] + args[2:]), env)) - return atom.native() - - repl_env = Env(None) - for key in core.ns: - repl_env.set(key, core.ns[key]) - - repl_env.set("eval", MalFunctionCompiled(lambda args: eval_func(args, repl_env))) - - rep( - '(def! load-file (fn* (f) (eval (read-string (str "(do " (slurp f) "\nnil)")))))', - repl_env, - ) - - mal_argv = MalList([MalString(x) for x in sys.argv[2:]]) - repl_env.set("*ARGV*", mal_argv) - - 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, - ) - - return repl_env - - -def rep_handling_exceptions(line: str, repl_env: Env) -> str: - try: - return rep(line, repl_env) - except MalUnknownSymbolException as e: - return "'" + e.func + "' not found" - except MalException as e: - return "ERROR: " + str(e) - - -if __name__ == "__main__": - # repl loop - eof: bool = False - repl_env = init_repl_env() - - if len(sys.argv) >= 2: - file_str = sys.argv[1] - rep_handling_exceptions('(load-file "' + file_str + '")', repl_env) - exit(0) - - while not eof: - try: - line = input("user> ") - readline.add_history(line) - print(rep_handling_exceptions(line, repl_env)) - except EOFError: - eof = True + 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() diff --git a/impls/python.2/stepA_mal.py b/impls/python.2/stepA_mal.py index 065b10e121..01ef8a24d5 100644 --- a/impls/python.2/stepA_mal.py +++ b/impls/python.2/stepA_mal.py @@ -1,252 +1,259 @@ +# pylint: disable=invalid-name +# Disabled because the file name contains a capital letter. Ideally, +# we would check the rest of the module, but this does not matter much +# as step9 is almost identical. + import functools -import readline import sys -from typing import List, Dict +import traceback +from collections.abc import Sequence import core + +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) + import reader -from env import Env -from mal_types import ( - MalExpression, - MalSymbol, - MalException, - MalList, - MalNil, - MalBoolean, - MalFunctionCompiled, - MalFunctionRaw, - MalVector, - MalHash_map, - MalUnknownSymbolException, - MalInvalidArgumentException, - MalString, -) - - -def READ(x: str) -> MalExpression: - return reader.read(x) - - -def qq_loop(acc: MalList, elt: MalExpression) -> MalList: - if isinstance(elt, MalList): - lst = elt.native() - if len(lst) == 2: - fst = lst[0] - if isinstance(fst, MalSymbol) and fst.native() == u"splice-unquote": - return MalList([MalSymbol(u"concat"), lst[1], acc]) - return MalList([MalSymbol(u"cons"), quasiquote(elt), acc]) - -def qq_foldr(xs: List[MalExpression]) -> MalList: - return functools.reduce(qq_loop, reversed(xs), MalList([])) - -def quasiquote(ast: MalExpression) -> MalExpression: - if isinstance(ast, MalList): - lst = ast.native() - if len(lst) == 2: - fst = lst[0] - if isinstance(fst, MalSymbol) and fst.native() == u'unquote': - return lst[1] - return qq_foldr(lst) - elif isinstance(ast, MalVector): - return MalList([MalSymbol("vec"), qq_foldr(ast.native())]) - elif isinstance(ast, MalSymbol) or isinstance(ast, MalHash_map): - return MalList([MalSymbol("quote"), ast]) - else: - return ast - - -def EVAL(ast: MalExpression, env: Env) -> MalExpression: - while True: - dbgeval = env.get("DEBUG-EVAL") - if (dbgeval is not None - and not isinstance(dbgeval, MalNil) - and (not isinstance(dbgeval, MalBoolean) or dbgeval.native())): - print("EVAL: " + str(ast)) - ast_native = ast.native() - if isinstance(ast, MalSymbol): - key = str(ast) - val = env.get(key) - if val is None: raise MalUnknownSymbolException(key) - return val - if isinstance(ast, MalVector): - return MalVector([EVAL(x, env) for x in ast_native]) - if isinstance(ast, MalHash_map): - new_dict = {} # type: Dict[str, MalExpression] - for key in ast_native: - new_dict[key] = EVAL(ast_native[key], env) - return MalHash_map(new_dict) - if not isinstance(ast, MalList): - return ast - elif len(ast_native) == 0: + +# 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 _: return ast - first_str = str(ast_native[0]) - if first_str == "def!": - name: str = str(ast_native[1]) - value: MalExpression = EVAL(ast_native[2], env) - return env.set(name, value) - if first_str == "defmacro!": - name = str(ast_native[1]) - value = EVAL(ast_native[2], env) - assert isinstance(value, MalFunctionCompiled) or isinstance( - value, MalFunctionRaw - ) - value.make_macro() - return env.set(name, value) - elif first_str == "let*": - assert len(ast_native) == 3 - let_env = Env(env) - bindings: MalExpression = ast_native[1] - assert isinstance(bindings, MalList) or isinstance(bindings, MalVector) - bindings_list: List[MalExpression] = bindings.native() - assert len(bindings_list) % 2 == 0 - for i in range(0, len(bindings_list), 2): - assert isinstance(bindings_list[i], MalSymbol) - assert isinstance(bindings_list[i + 1], MalExpression) - let_env.set(str(bindings_list[i]), EVAL(bindings_list[i + 1], let_env)) - env = let_env - ast = ast_native[2] - continue - elif first_str == "do": - for x in range(1, len(ast_native) - 1): - EVAL(ast_native[x], env) - ast = ast_native[len(ast_native) - 1] - continue - elif first_str == "if": - condition = EVAL(ast_native[1], env) - - if isinstance(condition, MalNil) or ( - isinstance(condition, MalBoolean) and condition.native() is False - ): - if len(ast_native) >= 4: - ast = ast_native[3] - continue - else: - return MalNil() - else: - ast = ast_native[2] - continue - elif first_str == "fn*": - raw_ast = ast_native[2] - raw_params = ast_native[1] - - def fn(args: List[MalExpression]) -> MalExpression: - f_ast = raw_ast - f_env = Env(outer=env, binds=raw_params.native(), exprs=args) - return EVAL(f_ast, f_env) - - return MalFunctionRaw(fn=fn, ast=raw_ast, params=raw_params, env=env) - elif first_str == "quote": - return ( - MalList(ast_native[1].native()) - if isinstance(ast_native[1], MalVector) - else ast_native[1] - ) - elif first_str == "quasiquote": - ast = quasiquote(ast_native[1]) - continue - elif first_str == "try*": + +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])]: try: - return EVAL(ast_native[1], env) - except MalException as e: - if len(ast_native) < 3: - raise e - catch_block = ast_native[2] - assert ( - isinstance(catch_block, MalList) - and isinstance(catch_block.native()[0], MalSymbol) - and str(catch_block.native()[0]) == "catch*" - and len(catch_block.native()) == 3 - ) - exception_symbol = catch_block.native()[1] - assert isinstance(exception_symbol, MalSymbol) - env = Env(env) - env.set(str(exception_symbol), e.native()) - ast = catch_block.native()[2] - continue - else: - f = EVAL(ast_native[0], env) - if isinstance(f, (MalFunctionCompiled, MalFunctionRaw)) and f.is_macro(): - ast = f.call(ast_native[1:]) - continue - args = [EVAL(ast_native[i], env) for i in range(1, len(ast_native))] - if isinstance(f, MalFunctionRaw): - ast = f.ast() - - env = Env( - outer=f.env(), - binds=f.params().native(), - exprs=args, - ) - continue - elif isinstance(f, MalFunctionCompiled): - return f.call(args) - else: - raise MalInvalidArgumentException(f, "not a function") - - -def PRINT(x: MalExpression) -> str: - return str(x) - - -def rep(x: str, env: Env) -> str: - return PRINT(EVAL(READ(x), env)) - - -def init_repl_env() -> Env: - def eval_func(args: List[MalExpression], env: Env) -> MalExpression: - a0 = args[0] - assert isinstance(a0, MalExpression) - return EVAL(a0, env) - - env = Env(None) - for key in core.ns: - env.set(key, core.ns[key]) - - env.set("eval", MalFunctionCompiled(lambda args: eval_func(args, env))) - rep('(def! *host-language* "python.2")', env) - - rep( - '(def! load-file (fn* (f) (eval (read-string (str "(do " (slurp f) "\nnil)")))))', - env, - ) - - mal_argv = MalList([MalString(x) for x in sys.argv[2:]]) - env.set("*ARGV*", mal_argv) - - 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)))))))", - env, - ) - - return env - - -def rep_handling_exceptions(line: str, repl_env: Env) -> str: - try: - return rep(line, repl_env) - except MalUnknownSymbolException as e: - return "'" + e.func + "' not found" - except MalException as e: - return "ERROR: " + str(e) - - -if __name__ == "__main__": - # repl loop - eof: bool = False - repl_env = init_repl_env() - - if len(sys.argv) >= 2: - file_str = sys.argv[1] - rep_handling_exceptions('(load-file "' + file_str + '")', repl_env) - exit(0) - - rep('(println (str "Mal [" *host-language* "]"))', repl_env) - - while not eof: - try: - line = input("user> ") - readline.add_history(line) - print(rep_handling_exceptions(line, repl_env)) - except EOFError: - eof = True + 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) + rep('(def! *host-language* "python.2")', 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() + rep('(println (str "Mal [" *host-language* "]"))', repl_env) + 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() diff --git a/impls/python.2/tests/step5_tco.mal b/impls/python.2/tests/step5_tco.mal new file mode 100644 index 0000000000..d20df25db7 --- /dev/null +++ b/impls/python.2/tests/step5_tco.mal @@ -0,0 +1,15 @@ +;; Test recursive non-tail call function + +(def! sum-to (fn* (n) (if (= n 0) 0 (+ n (sum-to (- n 1)))))) + +(sum-to 10) +;=>55 + +;;; no try* yet, so test completion of side-effects +(def! res1 nil) +;=>nil +;;; For implementations without their own TCO this should fail and +;;; leave res1 unchanged +(def! res1 (sum-to 10000)) +res1 +;=>nil diff --git a/impls/python.2/tests/stepA_mal.mal b/impls/python.2/tests/stepA_mal.mal new file mode 100644 index 0000000000..c5007053be --- /dev/null +++ b/impls/python.2/tests/stepA_mal.mal @@ -0,0 +1,32 @@ +*host-language* +;=>"python.2" + +;; Testing Python interop + +;; Testing Python expressions + +(py* "7") +;=>7 + +(py* "'7'") +;=>"7" + +(py* "[7,8,9]") +;=>(7 8 9) + +(py* "' '.join(f'X{c}Y' for c in 'abc')") +;=>"XaY XbY XcY" + +(py* "list(1 + x for x in range(1, 4))") +;=>(2 3 4) + +;; Testing Python statements + +(py!* "print('hello')") +;/hello +;=>nil + +(py!* "foo = 19 % 4") +;=>nil +(py* "foo") +;=>3 diff --git a/impls/tests/step4_if_fn_do.mal b/impls/tests/step4_if_fn_do.mal index 4f19c8b71d..c4dd1dc1b0 100644 --- a/impls/tests/step4_if_fn_do.mal +++ b/impls/tests/step4_if_fn_do.mal @@ -71,10 +71,6 @@ ;=>false (= 2 (+ 1 1)) ;=>true -(= nil 1) -;=>false -(= nil nil) -;=>true (> 2 1) ;=>true @@ -105,19 +101,60 @@ ;=>true -;; Testing equality +;; Testing equality and the representation of nil false true (= 1 1) ;=>true (= 0 0) ;=>true (= 1 0) ;=>false -(= true true) + +(= nil nil) ;=>true +(= nil false) +;=>false +(= nil true) +;=>false +(= nil 0) +;=>false +(= nil 1) +;=>false +(= nil "") +;=>false +(= nil ()) +;=>false +(= nil []) +;=>false + +(= false nil) +;=>false (= false false) ;=>true -(= nil nil) +(= false true) +;=>false +(= false 0) +;=>false +(= false 1) +;=>false +(= false "") +;=>false +(= false ()) +;=>false + +(= true nil) +;=>false +(= true false) +;=>false +(= true true) ;=>true +(= true 0) +;=>false +(= true 1) +;=>false +(= true "") +;=>false +(= true ()) +;=>false (= (list) (list)) ;=>true diff --git a/impls/tests/step9_try.mal b/impls/tests/step9_try.mal index 345811d7cb..01ee642593 100644 --- a/impls/tests/step9_try.mal +++ b/impls/tests/step9_try.mal @@ -52,20 +52,44 @@ (nil? nil) ;=>true +(nil? false) +;=>false (nil? true) ;=>false +(nil? ()) +;=>false +(nil? 0) +;=>false +(true? nil) +;=>false +(true? false) +;=>false (true? true) ;=>true -(true? false) +(true? 1) ;=>false (true? true?) ;=>false +(false? nil) +;=>false (false? false) ;=>true (false? true) ;=>false +(false? "") +;=>false +(false? 0) +;=>false +(false? ()) +;=>false +(false? []) +;=>false +(false? {}) +;=>false +(false? nil) +;=>false ;; Testing apply function with core functions (apply + (list 2 3)) diff --git a/impls/tests/stepA_mal.mal b/impls/tests/stepA_mal.mal index 0a92264163..2967cdbca4 100644 --- a/impls/tests/stepA_mal.mal +++ b/impls/tests/stepA_mal.mal @@ -278,6 +278,19 @@ (meta +) ;=>nil +;; Metadata should not break equality. +(= [1] ^2 [1]) +;=>true + +(= '(1) ^2 '(1)) +;=>true + +(= {"a" 1} ^2 {"a" 1}) +;=>true + +(= '(1) ^2 [1]) +;=>true + ;; Loading sumdown from computations.mal (load-file "../tests/computations.mal") ;=>nil