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..8e6001a69f 100644 --- a/impls/python.2/Dockerfile +++ b/impls/python.2/Dockerfile @@ -1,4 +1,4 @@ -FROM ubuntu:bionic +FROM ubuntu:24.04 MAINTAINER Joel Martin ########################################################## @@ -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 b3a5b1ed8c..2e36ce5814 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