From 1754a511f0333ca7a96eab171348dd1d27f02c44 Mon Sep 17 00:00:00 2001 From: Luciano Ramalho Date: Tue, 29 Jun 2021 12:21:27 -0300 Subject: [PATCH] updated lis.py examples --- 02-array-seq/lispy/LICENSE | 21 ++ 02-array-seq/lispy/README.md | 36 ++ 02-array-seq/lispy/original/lis.py | 135 ++++++++ 02-array-seq/lispy/original/lispy.py | 316 ++++++++++++++++++ 02-array-seq/lispy/original/lispytest.py | 122 +++++++ .../lispy/{ => py3.10}/examples_test.py | 0 02-array-seq/lispy/{ => py3.10}/lis.py | 10 +- 02-array-seq/lispy/{ => py3.10}/lis_test.py | 6 +- 02-array-seq/lispy/{ => py3.10}/meta_test.py | 0 9 files changed, 639 insertions(+), 7 deletions(-) create mode 100644 02-array-seq/lispy/LICENSE create mode 100644 02-array-seq/lispy/README.md create mode 100644 02-array-seq/lispy/original/lis.py create mode 100644 02-array-seq/lispy/original/lispy.py create mode 100644 02-array-seq/lispy/original/lispytest.py rename 02-array-seq/lispy/{ => py3.10}/examples_test.py (100%) rename 02-array-seq/lispy/{ => py3.10}/lis.py (95%) rename 02-array-seq/lispy/{ => py3.10}/lis_test.py (97%) rename 02-array-seq/lispy/{ => py3.10}/meta_test.py (100%) diff --git a/02-array-seq/lispy/LICENSE b/02-array-seq/lispy/LICENSE new file mode 100644 index 0000000..ca550a2 --- /dev/null +++ b/02-array-seq/lispy/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2010-2017 Peter Norvig + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/02-array-seq/lispy/README.md b/02-array-seq/lispy/README.md new file mode 100644 index 0000000..0c11fd8 --- /dev/null +++ b/02-array-seq/lispy/README.md @@ -0,0 +1,36 @@ +# Norvig's originals and updates + +This directory contains: + +* `original/`: +Norvig's [`lis.py`](https://github.com/norvig/pytudes/blob/c33cd6835a506a57d9fe73e3a8317d49babb13e8/py/lis.py), +[`lispy.py`](https://github.com/norvig/pytudes/blob/c33cd6835a506a57d9fe73e3a8317d49babb13e8/py/lispy.py), and the `lispytest.py` custom test script for testing both; +* `py3.10/`: `lis.py` with type hints, pattern matching, and minor edits—requires Python 3.10. + +The `py3.10/` directory also has `lis_test.py` to run with +[pytest](https://docs.pytest.org), including all the +[`lis_tests` suite](https://github.com/norvig/pytudes/blob/60168bce8cdfacf57c92a5b2979f0b2e95367753/py/lispytest.py#L5) +from `original/lispytest.py`, +and additional separate tests for each expression and special form handled by `evaluate`. + + +## Provenance, Copyright and License + +`lis.py` is +[published](https://github.com/norvig/pytudes/blob/c33cd6835a506a57d9fe73e3a8317d49babb13e8/py/lis.py) +in the [norvig/pytudes](https://github.com/norvig/pytudes) repository on Github. +The copyright holder is Peter Norvig and the code is licensed under the +[MIT license](https://github.com/norvig/pytudes/blob/60168bce8cdfacf57c92a5b2979f0b2e95367753/LICENSE). + + +## Changes to Norvig's code + +I made small changes to the programs in `original/`: + +* In `lis.py`: + * The `Procedure` class accepts a list of expressions as the `body`, and `__call__` evaluates all those expressions in order, returning the value of the last. This is consistent with Scheme's `lambda` syntax and provided a useful example for pattern matching. + * In the `elif` block for `'lambda'`, I added the `*` in front of the `*body` variable in the tuple unpacking to capture the expressions as a list, before calling the `Procedure` constructor. + +* In `lispy.py` I made [changes and a pull request](https://github.com/norvig/pytudes/pull/106) to make it run on Python 3. + +_Luciano Ramalho
June 29, 2021_ \ No newline at end of file diff --git a/02-array-seq/lispy/original/lis.py b/02-array-seq/lispy/original/lis.py new file mode 100644 index 0000000..6e43fa2 --- /dev/null +++ b/02-array-seq/lispy/original/lis.py @@ -0,0 +1,135 @@ +################ Lispy: Scheme Interpreter in Python 3.3+ + +## (c) Peter Norvig, 2010-18; See http://norvig.com/lispy.html + +################ Imports and Types + +import math +import operator as op +from collections import ChainMap as Environment + +Symbol = str # A Lisp Symbol is implemented as a Python str +List = list # A Lisp List is implemented as a Python list +Number = (int, float) # A Lisp Number is implemented as a Python int or float + +class Procedure(object): + "A user-defined Scheme procedure." + def __init__(self, parms, body, env): + self.parms, self.body, self.env = parms, body, env + def __call__(self, *args): + env = Environment(dict(zip(self.parms, args)), self.env) + for exp in self.body: + result = eval(exp, env) + return result + + +################ Global Environment + +def standard_env(): + "An environment with some Scheme standard procedures." + env = {} + env.update(vars(math)) # sin, cos, sqrt, pi, ... + env.update({ + '+':op.add, '-':op.sub, '*':op.mul, '/':op.truediv, + '>':op.gt, '<':op.lt, '>=':op.ge, '<=':op.le, '=':op.eq, + 'abs': abs, + 'append': op.add, + 'apply': lambda proc, args: proc(*args), + 'begin': lambda *x: x[-1], + 'car': lambda x: x[0], + 'cdr': lambda x: x[1:], + 'cons': lambda x,y: [x] + y, + 'eq?': op.is_, + 'equal?': op.eq, + 'length': len, + 'list': lambda *x: list(x), + 'list?': lambda x: isinstance(x,list), + 'map': lambda *args: list(map(*args)), + 'max': max, + 'min': min, + 'not': op.not_, + 'null?': lambda x: x == [], + 'number?': lambda x: isinstance(x, Number), + 'procedure?': callable, + 'round': round, + 'symbol?': lambda x: isinstance(x, Symbol), + }) + return env + +global_env = standard_env() + +################ Parsing: parse, tokenize, and read_from_tokens + +def parse(program): + "Read a Scheme expression from a string." + return read_from_tokens(tokenize(program)) + +def tokenize(s): + "Convert a string into a list of tokens." + return s.replace('(',' ( ').replace(')',' ) ').split() + +def read_from_tokens(tokens): + "Read an expression from a sequence of tokens." + if len(tokens) == 0: + raise SyntaxError('unexpected EOF while reading') + token = tokens.pop(0) + if '(' == token: + L = [] + while tokens[0] != ')': + L.append(read_from_tokens(tokens)) + tokens.pop(0) # pop off ')' + return L + elif ')' == token: + raise SyntaxError('unexpected )') + else: + return atom(token) + +def atom(token): + "Numbers become numbers; every other token is a symbol." + try: return int(token) + except ValueError: + try: return float(token) + except ValueError: + return Symbol(token) + +################ Interaction: A REPL + +def repl(prompt='lis.py> '): + "A prompt-read-eval-print loop." + while True: + val = eval(parse(input(prompt))) + if val is not None: + print(lispstr(val)) + +def lispstr(exp): + "Convert a Python object back into a Lisp-readable string." + if isinstance(exp, List): + return '(' + ' '.join(map(lispstr, exp)) + ')' + else: + return str(exp) + +################ eval + +def eval(x, env=global_env): + "Evaluate an expression in an environment." + if isinstance(x, Symbol): # variable reference + return env[x] + elif not isinstance(x, List): # constant literal + return x + elif x[0] == 'quote': # (quote exp) + (_, exp) = x + return exp + elif x[0] == 'if': # (if test conseq alt) + (_, test, conseq, alt) = x + exp = (conseq if eval(test, env) else alt) + return eval(exp, env) + elif x[0] == 'define': # (define var exp) + (_, var, exp) = x + env[var] = eval(exp, env) + elif x[0] == 'lambda': # (lambda (var...) body) + (_, parms, *body) = x + return Procedure(parms, body, env) + else: # (proc arg...) + proc = eval(x[0], env) + args = [eval(exp, env) for exp in x[1:]] + return proc(*args) diff --git a/02-array-seq/lispy/original/lispy.py b/02-array-seq/lispy/original/lispy.py new file mode 100644 index 0000000..b17341c --- /dev/null +++ b/02-array-seq/lispy/original/lispy.py @@ -0,0 +1,316 @@ +################ Scheme Interpreter in Python + +## (c) Peter Norvig, 2010; See http://norvig.com/lispy2.html + +################ Symbol, Procedure, classes + +import re, sys, io + +class Symbol(str): pass + +def Sym(s, symbol_table={}): + "Find or create unique Symbol entry for str s in symbol table." + if s not in symbol_table: symbol_table[s] = Symbol(s) + return symbol_table[s] + +_quote, _if, _set, _define, _lambda, _begin, _definemacro, = map(Sym, +"quote if set! define lambda begin define-macro".split()) + +_quasiquote, _unquote, _unquotesplicing = map(Sym, +"quasiquote unquote unquote-splicing".split()) + +class Procedure: + "A user-defined Scheme procedure." + def __init__(self, parms, exp, env): + self.parms, self.exp, self.env = parms, exp, env + def __call__(self, *args): + return eval(self.exp, Env(self.parms, args, self.env)) + +################ parse, read, and user interaction + +def parse(inport): + "Parse a program: read and expand/error-check it." + # Backwards compatibility: given a str, convert it to an InPort + if isinstance(inport, str): inport = InPort(io.StringIO(inport)) + return expand(read(inport), toplevel=True) + +eof_object = Symbol('#') # Note: uninterned; can't be read + +class InPort: + "An input port. Retains a line of chars." + tokenizer = r"""\s*(,@|[('`,)]|"(?:[\\].|[^\\"])*"|;.*|[^\s('"`,;)]*)(.*)""" + def __init__(self, file): + self.file = file; self.line = '' + def next_token(self): + "Return the next token, reading new text into line buffer if needed." + while True: + if self.line == '': self.line = self.file.readline() + if self.line == '': return eof_object + token, self.line = re.match(InPort.tokenizer, self.line).groups() + if token != '' and not token.startswith(';'): + return token + +def readchar(inport): + "Read the next character from an input port." + if inport.line != '': + ch, inport.line = inport.line[0], inport.line[1:] + return ch + else: + return inport.file.read(1) or eof_object + +def read(inport): + "Read a Scheme expression from an input port." + def read_ahead(token): + if '(' == token: + L = [] + while True: + token = inport.next_token() + if token == ')': return L + else: L.append(read_ahead(token)) + elif ')' == token: raise SyntaxError('unexpected )') + elif token in quotes: return [quotes[token], read(inport)] + elif token is eof_object: raise SyntaxError('unexpected EOF in list') + else: return atom(token) + # body of read: + token1 = inport.next_token() + return eof_object if token1 is eof_object else read_ahead(token1) + +quotes = {"'":_quote, "`":_quasiquote, ",":_unquote, ",@":_unquotesplicing} + +def atom(token): + 'Numbers become numbers; #t and #f are booleans; "..." string; otherwise Symbol.' + if token == '#t': return True + elif token == '#f': return False + elif token[0] == '"': return token[1:-1] + try: return int(token) + except ValueError: + try: return float(token) + except ValueError: + try: return complex(token.replace('i', 'j', 1)) + except ValueError: + return Sym(token) + +def to_string(x): + "Convert a Python object back into a Lisp-readable string." + if x is True: return "#t" + elif x is False: return "#f" + elif isa(x, Symbol): return x + elif isa(x, str): return repr(x) + elif isa(x, list): return '('+' '.join(map(to_string, x))+')' + elif isa(x, complex): return str(x).replace('j', 'i') + else: return str(x) + +def load(filename): + "Eval every expression from a file." + repl(None, InPort(open(filename)), None) + +def repl(prompt='lispy> ', inport=InPort(sys.stdin), out=sys.stdout): + "A prompt-read-eval-print loop." + sys.stderr.write("Lispy version 2.0\n") + while True: + try: + if prompt: sys.stderr.write(prompt) + x = parse(inport) + if x is eof_object: return + val = eval(x) + if val is not None and out: print(to_string(val), file=out) + except Exception as e: + print('%s: %s' % (type(e).__name__, e)) + +################ Environment class + +class Env(dict): + "An environment: a dict of {'var':val} pairs, with an outer Env." + def __init__(self, parms=(), args=(), outer=None): + # Bind parm list to corresponding args, or single parm to list of args + self.outer = outer + if isa(parms, Symbol): + self.update({parms:list(args)}) + else: + if len(args) != len(parms): + raise TypeError('expected %s, given %s, ' + % (to_string(parms), to_string(args))) + self.update(zip(parms,args)) + def find(self, var): + "Find the innermost Env where var appears." + if var in self: return self + elif self.outer is None: raise LookupError(var) + else: return self.outer.find(var) + +def is_pair(x): return x != [] and isa(x, list) +def cons(x, y): return [x]+y + +def callcc(proc): + "Call proc with current continuation; escape only" + ball = RuntimeWarning("Sorry, can't continue this continuation any longer.") + def throw(retval): ball.retval = retval; raise ball + try: + return proc(throw) + except RuntimeWarning as w: + if w is ball: return ball.retval + else: raise w + +def add_globals(self): + "Add some Scheme standard procedures." + import math, cmath, operator as op + self.update(vars(math)) + self.update(vars(cmath)) + self.update({ + '+':op.add, '-':op.sub, '*':op.mul, '/':op.truediv, 'not':op.not_, + '>':op.gt, '<':op.lt, '>=':op.ge, '<=':op.le, '=':op.eq, + 'equal?':op.eq, 'eq?':op.is_, 'length':len, 'cons':cons, + 'car':lambda x:x[0], 'cdr':lambda x:x[1:], 'append':op.add, + 'list':lambda *x:list(x), 'list?': lambda x:isa(x,list), + 'null?':lambda x:x==[], 'symbol?':lambda x: isa(x, Symbol), + 'boolean?':lambda x: isa(x, bool), 'pair?':is_pair, + 'port?': lambda x:isa(x,file), 'apply':lambda proc,l: proc(*l), + 'eval':lambda x: eval(expand(x)), 'load':lambda fn: load(fn), 'call/cc':callcc, + 'open-input-file':open,'close-input-port':lambda p: p.file.close(), + 'open-output-file':lambda f:open(f,'w'), 'close-output-port':lambda p: p.close(), + 'eof-object?':lambda x:x is eof_object, 'read-char':readchar, + 'read':read, 'write':lambda x,port=sys.stdout:port.write(to_string(x)), + 'display':lambda x,port=sys.stdout:port.write(x if isa(x,str) else to_string(x))}) + return self + +isa = isinstance + +global_env = add_globals(Env()) + +################ eval (tail recursive) + +def eval(x, env=global_env): + "Evaluate an expression in an environment." + while True: + if isa(x, Symbol): # variable reference + return env.find(x)[x] + elif not isa(x, list): # constant literal + return x + elif x[0] is _quote: # (quote exp) + (_, exp) = x + return exp + elif x[0] is _if: # (if test conseq alt) + (_, test, conseq, alt) = x + x = (conseq if eval(test, env) else alt) + elif x[0] is _set: # (set! var exp) + (_, var, exp) = x + env.find(var)[var] = eval(exp, env) + return None + elif x[0] is _define: # (define var exp) + (_, var, exp) = x + env[var] = eval(exp, env) + return None + elif x[0] is _lambda: # (lambda (var*) exp) + (_, vars, exp) = x + return Procedure(vars, exp, env) + elif x[0] is _begin: # (begin exp+) + for exp in x[1:-1]: + eval(exp, env) + x = x[-1] + else: # (proc exp*) + exps = [eval(exp, env) for exp in x] + proc = exps.pop(0) + if isa(proc, Procedure): + x = proc.exp + env = Env(proc.parms, exps, proc.env) + else: + return proc(*exps) + +################ expand + +def expand(x, toplevel=False): + "Walk tree of x, making optimizations/fixes, and signaling SyntaxError." + require(x, x!=[]) # () => Error + if not isa(x, list): # constant => unchanged + return x + elif x[0] is _quote: # (quote exp) + require(x, len(x)==2) + return x + elif x[0] is _if: + if len(x)==3: x = x + [None] # (if t c) => (if t c None) + require(x, len(x)==4) + return list(map(expand, x)) + elif x[0] is _set: + require(x, len(x)==3); + var = x[1] # (set! non-var exp) => Error + require(x, isa(var, Symbol), "can set! only a symbol") + return [_set, var, expand(x[2])] + elif x[0] is _define or x[0] is _definemacro: + require(x, len(x)>=3) + _def, v, body = x[0], x[1], x[2:] + if isa(v, list) and v: # (define (f args) body) + f, args = v[0], v[1:] # => (define f (lambda (args) body)) + return expand([_def, f, [_lambda, args]+body]) + else: + require(x, len(x)==3) # (define non-var/list exp) => Error + require(x, isa(v, Symbol), "can define only a symbol") + exp = expand(x[2]) + if _def is _definemacro: + require(x, toplevel, "define-macro only allowed at top level") + proc = eval(exp) + require(x, callable(proc), "macro must be a procedure") + macro_table[v] = proc # (define-macro v proc) + return None # => None; add v:proc to macro_table + return [_define, v, exp] + elif x[0] is _begin: + if len(x)==1: return None # (begin) => None + else: return [expand(xi, toplevel) for xi in x] + elif x[0] is _lambda: # (lambda (x) e1 e2) + require(x, len(x)>=3) # => (lambda (x) (begin e1 e2)) + vars, body = x[1], x[2:] + require(x, (isa(vars, list) and all(isa(v, Symbol) for v in vars)) + or isa(vars, Symbol), "illegal lambda argument list") + exp = body[0] if len(body) == 1 else [_begin] + body + return [_lambda, vars, expand(exp)] + elif x[0] is _quasiquote: # `x => expand_quasiquote(x) + require(x, len(x)==2) + return expand_quasiquote(x[1]) + elif isa(x[0], Symbol) and x[0] in macro_table: + return expand(macro_table[x[0]](*x[1:]), toplevel) # (m arg...) + else: # => macroexpand if m isa macro + return list(map(expand, x)) # (f arg...) => expand each + +def require(x, predicate, msg="wrong length"): + "Signal a syntax error if predicate is false." + if not predicate: raise SyntaxError(to_string(x)+': '+msg) + +_append, _cons, _let = map(Sym, "append cons let".split()) + +def expand_quasiquote(x): + """Expand `x => 'x; `,x => x; `(,@x y) => (append x y) """ + if not is_pair(x): + return [_quote, x] + require(x, x[0] is not _unquotesplicing, "can't splice here") + if x[0] is _unquote: + require(x, len(x)==2) + return x[1] + elif is_pair(x[0]) and x[0][0] is _unquotesplicing: + require(x[0], len(x[0])==2) + return [_append, x[0][1], expand_quasiquote(x[1:])] + else: + return [_cons, expand_quasiquote(x[0]), expand_quasiquote(x[1:])] + +def let(*args): + args = list(args) + x = cons(_let, args) + require(x, len(args)>1) + bindings, body = args[0], args[1:] + require(x, all(isa(b, list) and len(b)==2 and isa(b[0], Symbol) + for b in bindings), "illegal binding list") + vars, vals = zip(*bindings) + return [[_lambda, list(vars)]+list(map(expand, body))] + list(map(expand, vals)) + +macro_table = {_let:let} ## More macros can go here + +eval(parse("""(begin + +(define-macro and (lambda args + (if (null? args) #t + (if (= (length args) 1) (car args) + `(if ,(car args) (and ,@(cdr args)) #f))))) + +;; More macros can also go here + +)""")) + +if __name__ == '__main__': + repl() diff --git a/02-array-seq/lispy/original/lispytest.py b/02-array-seq/lispy/original/lispytest.py new file mode 100644 index 0000000..b324ff3 --- /dev/null +++ b/02-array-seq/lispy/original/lispytest.py @@ -0,0 +1,122 @@ +from __future__ import print_function + +################ Tests for lis.py and lispy.py + +lis_tests = [ + ("(quote (testing 1 (2.0) -3.14e159))", ['testing', 1, [2.0], -3.14e159]), + ("(+ 2 2)", 4), + ("(+ (* 2 100) (* 1 10))", 210), + ("(if (> 6 5) (+ 1 1) (+ 2 2))", 2), + ("(if (< 6 5) (+ 1 1) (+ 2 2))", 4), + ("(define x 3)", None), ("x", 3), ("(+ x x)", 6), + ("((lambda (x) (+ x x)) 5)", 10), + ("(define twice (lambda (x) (* 2 x)))", None), ("(twice 5)", 10), + ("(define compose (lambda (f g) (lambda (x) (f (g x)))))", None), + ("((compose list twice) 5)", [10]), + ("(define repeat (lambda (f) (compose f f)))", None), + ("((repeat twice) 5)", 20), ("((repeat (repeat twice)) 5)", 80), + ("(define fact (lambda (n) (if (<= n 1) 1 (* n (fact (- n 1))))))", None), + ("(fact 3)", 6), + ("(fact 50)", 30414093201713378043612608166064768844377641568960512000000000000), + ("(define abs (lambda (n) ((if (> n 0) + -) 0 n)))", None), + ("(list (abs -3) (abs 0) (abs 3))", [3, 0, 3]), + ("""(define combine (lambda (f) + (lambda (x y) + (if (null? x) (quote ()) + (f (list (car x) (car y)) + ((combine f) (cdr x) (cdr y)))))))""", None), + ("(define zip (combine cons))", None), + ("(zip (list 1 2 3 4) (list 5 6 7 8))", [[1, 5], [2, 6], [3, 7], [4, 8]]), + ("""(define riff-shuffle (lambda (deck) (begin + (define take (lambda (n seq) (if (<= n 0) (quote ()) (cons (car seq) (take (- n 1) (cdr seq)))))) + (define drop (lambda (n seq) (if (<= n 0) seq (drop (- n 1) (cdr seq))))) + (define mid (lambda (seq) (/ (length seq) 2))) + ((combine append) (take (mid deck) deck) (drop (mid deck) deck)))))""", None), + ("(riff-shuffle (list 1 2 3 4 5 6 7 8))", [1, 5, 2, 6, 3, 7, 4, 8]), + ("((repeat riff-shuffle) (list 1 2 3 4 5 6 7 8))", [1, 3, 5, 7, 2, 4, 6, 8]), + ("(riff-shuffle (riff-shuffle (riff-shuffle (list 1 2 3 4 5 6 7 8))))", [1,2,3,4,5,6,7,8]), + ] + +lispy_tests = [ + ("()", SyntaxError), ("(set! x)", SyntaxError), + ("(define 3 4)", SyntaxError), + ("(quote 1 2)", SyntaxError), ("(if 1 2 3 4)", SyntaxError), + ("(lambda 3 3)", SyntaxError), ("(lambda (x))", SyntaxError), + ("""(if (= 1 2) (define-macro a 'a) + (define-macro a 'b))""", SyntaxError), + ("(define (twice x) (* 2 x))", None), ("(twice 2)", 4), + ("(twice 2 2)", TypeError), + ("(define lyst (lambda items items))", None), + ("(lyst 1 2 3 (+ 2 2))", [1,2,3,4]), + ("(if 1 2)", 2), + ("(if (= 3 4) 2)", None), + ("(begin (define x 1) (set! x (+ x 1)) (+ x 1))", 3), + ("(define ((account bal) amt) (set! bal (+ bal amt)) bal)", None), + ("(define a1 (account 100))", None), + ("(a1 0)", 100), ("(a1 10)", 110), ("(a1 10)", 120), + ("""(define (newton guess function derivative epsilon) + (define guess2 (- guess (/ (function guess) (derivative guess)))) + (if (< (abs (- guess guess2)) epsilon) guess2 + (newton guess2 function derivative epsilon)))""", None), + ("""(define (square-root a) + (newton 1 (lambda (x) (- (* x x) a)) (lambda (x) (* 2 x)) 1e-8))""", None), + ("(> (square-root 200.) 14.14213)", True), + ("(< (square-root 200.) 14.14215)", True), + ("(= (square-root 200.) (sqrt 200.))", True), + ("""(define (sum-squares-range start end) + (define (sumsq-acc start end acc) + (if (> start end) acc (sumsq-acc (+ start 1) end (+ (* start start) acc)))) + (sumsq-acc start end 0))""", None), + ("(sum-squares-range 1 3000)", 9004500500), ## Tests tail recursion + ("(call/cc (lambda (throw) (+ 5 (* 10 (throw 1))))) ;; throw", 1), + ("(call/cc (lambda (throw) (+ 5 (* 10 1)))) ;; do not throw", 15), + ("""(call/cc (lambda (throw) + (+ 5 (* 10 (call/cc (lambda (escape) (* 100 (escape 3)))))))) ; 1 level""", 35), + ("""(call/cc (lambda (throw) + (+ 5 (* 10 (call/cc (lambda (escape) (* 100 (throw 3)))))))) ; 2 levels""", 3), + ("""(call/cc (lambda (throw) + (+ 5 (* 10 (call/cc (lambda (escape) (* 100 1))))))) ; 0 levels""", 1005), + ("(* 1i 1i)", -1), ("(sqrt -1)", 1j), + ("(let ((a 1) (b 2)) (+ a b))", 3), + ("(let ((a 1) (b 2 3)) (+ a b))", SyntaxError), + ("(and 1 2 3)", 3), ("(and (> 2 1) 2 3)", 3), ("(and)", True), + ("(and (> 2 1) (> 2 3))", False), + ("(define-macro unless (lambda args `(if (not ,(car args)) (begin ,@(cdr args))))) ; test `", None), + ("(unless (= 2 (+ 1 1)) (display 2) 3 4)", None), + (r'(unless (= 4 (+ 1 1)) (display 2) (display "\n") 3 4)', 4), + ("(quote x)", 'x'), + ("(quote (1 2 three))", [1, 2, 'three']), + ("'x", 'x'), + ("'(one 2 3)", ['one', 2, 3]), + ("(define L (list 1 2 3))", None), + ("`(testing ,@L testing)", ['testing',1,2,3,'testing']), + ("`(testing ,L testing)", ['testing',[1,2,3],'testing']), + ("`,@L", SyntaxError), + ("""'(1 ;test comments ' + ;skip this line + 2 ; more ; comments ; ) ) + 3) ; final comment""", [1,2,3]), + ] + +def test(tests, name=''): + "For each (exp, expected) test case, see if eval(parse(exp)) == expected." + fails = 0 + for (x, expected) in tests: + try: + result = eval(parse(x)) + print(x, '=>', lispstr(result)) + ok = (result == expected) + except Exception as e: + print(x, '=raises=>', type(e).__name__, e) + ok = isinstance(expected, type) and issubclass(expected, Exception) and isinstance(e, expected) + if not ok: + fails += 1 + print('FAIL!!! Expected', expected) + print('%s %s: %d out of %d tests fail.' % ('*'*45, name, fails, len(tests))) + +if __name__ == '__main__': + from lis import * + test(lis_tests, 'lis.py') + from lispy import * + test(lis_tests+lispy_tests, 'lispy.py') + diff --git a/02-array-seq/lispy/examples_test.py b/02-array-seq/lispy/py3.10/examples_test.py similarity index 100% rename from 02-array-seq/lispy/examples_test.py rename to 02-array-seq/lispy/py3.10/examples_test.py diff --git a/02-array-seq/lispy/lis.py b/02-array-seq/lispy/py3.10/lis.py similarity index 95% rename from 02-array-seq/lispy/lis.py rename to 02-array-seq/lispy/py3.10/lis.py index e37ba23..7ae0a9d 100644 --- a/02-array-seq/lispy/lis.py +++ b/02-array-seq/lispy/py3.10/lis.py @@ -23,7 +23,7 @@ Environment: TypeAlias = MutableMapping[Symbol, object] class Procedure: "A user-defined Scheme procedure." - def __init__(self, parms: list[Symbol], body: Expression, env: Environment): + def __init__(self, parms: list[Symbol], body: list[Expression], env: Environment): self.parms = parms self.body = body self.env = env @@ -31,7 +31,9 @@ class Procedure: def __call__(self, *args: Expression) -> Any: local_env = dict(zip(self.parms, args)) env: Environment = ChainMap(local_env, self.env) - return evaluate(self.body, env) + for exp in self.body: + result = evaluate(exp, env) + return result ################ global environment @@ -160,9 +162,9 @@ def evaluate(exp: Expression, env: Environment) -> Any: return evaluate(alternative, env) case ['define', Symbol(var), value_exp]: env[var] = evaluate(value_exp, env) - case ['define', [Symbol(name), *parms], body]: + case ['define', [Symbol(name), *parms], *body]: env[name] = Procedure(parms, body, env) - case ['lambda', [*parms], body]: + case ['lambda', [*parms], *body]: return Procedure(parms, body, env) case [op, *args]: proc = evaluate(op, env) diff --git a/02-array-seq/lispy/lis_test.py b/02-array-seq/lispy/py3.10/lis_test.py similarity index 97% rename from 02-array-seq/lispy/lis_test.py rename to 02-array-seq/lispy/py3.10/lis_test.py index e220e74..3688888 100644 --- a/02-array-seq/lispy/lis_test.py +++ b/02-array-seq/lispy/py3.10/lis_test.py @@ -12,7 +12,7 @@ from lis import parse, evaluate, Expression, Environment, standard_env ('(sum 1 2 3)', ['sum', 1, 2, 3]), ('(+ (* 2 100) (* 1 10))', ['+', ['*', 2, 100], ['*', 1, 10]]), ('99 100', 99), # parse stops at the first complete expression - ('(a)(b)', ['a']), + ('(a)(b)', ['a']), ]) def test_parse(source: str, expected: Expression) -> None: got = parse(source) @@ -122,7 +122,7 @@ def test_lambda(std_env: Environment) -> None: source = '(lambda (a b) (if (>= a b) a b))' func = evaluate(parse(source), std_env) assert func.parms == ['a', 'b'] - assert func.body == ['if', ['>=', 'a', 'b'], 'a', 'b'] + assert func.body == [['if', ['>=', 'a', 'b'], 'a', 'b']] assert func.env is std_env assert func(1, 2) == 2 assert func(3, 2) == 3 @@ -176,7 +176,7 @@ def test_define_function(std_env: Environment) -> None: assert got is None max_fn = std_env['max'] assert max_fn.parms == ['a', 'b'] - assert max_fn.body == ['if', ['>=', 'a', 'b'], 'a', 'b'] + assert max_fn.body == [['if', ['>=', 'a', 'b'], 'a', 'b']] assert max_fn.env is std_env assert max_fn(1, 2) == 2 assert max_fn(3, 2) == 3 diff --git a/02-array-seq/lispy/meta_test.py b/02-array-seq/lispy/py3.10/meta_test.py similarity index 100% rename from 02-array-seq/lispy/meta_test.py rename to 02-array-seq/lispy/py3.10/meta_test.py