From 2f2f87d4fb297869cb5763ec9b2ffcad3e841d20 Mon Sep 17 00:00:00 2001 From: Luciano Ramalho Date: Mon, 20 Sep 2021 10:37:26 -0300 Subject: [PATCH 1/2] sync from Atlas --- 02-array-seq/lispy/py3.10/examples_test.py | 258 ++++++++++++++++++++ 02-array-seq/lispy/py3.10/lis.py | 219 +++++++++++++++++ 02-array-seq/lispy/py3.10/lis_test.py | 180 ++++++++++++++ 02-array-seq/lispy/py3.10/quicksort.scm | 17 ++ 02-array-seq/lispy/py3.9/README.md | 33 +++ 02-array-seq/lispy/py3.9/examples_test.py | 196 +++++++++++++++ 02-array-seq/lispy/py3.9/lis.py | 209 ++++++++++++++++ 02-array-seq/lispy/py3.9/lis_test.py | 180 ++++++++++++++ 18-with-match/lispy/py3.10/examples_test.py | 43 ++-- 18-with-match/lispy/py3.10/lis.py | 14 +- 18-with-match/lispy/py3.10/lis_test.py | 6 +- 18-with-match/lispy/py3.9/examples_test.py | 258 ++++++++++++++++++++ 18-with-match/lispy/py3.9/lis.py | 207 ++++++++++------ 18-with-match/lispy/py3.9/lis_test.py | 20 +- 18-with-match/mirror_gen.py | 3 + 22-dyn-attr-prop/doc_property.py | 2 +- 16 files changed, 1730 insertions(+), 115 deletions(-) create mode 100644 02-array-seq/lispy/py3.10/examples_test.py create mode 100755 02-array-seq/lispy/py3.10/lis.py create mode 100644 02-array-seq/lispy/py3.10/lis_test.py create mode 100644 02-array-seq/lispy/py3.10/quicksort.scm create mode 100644 02-array-seq/lispy/py3.9/README.md create mode 100644 02-array-seq/lispy/py3.9/examples_test.py create mode 100644 02-array-seq/lispy/py3.9/lis.py create mode 100644 02-array-seq/lispy/py3.9/lis_test.py create mode 100644 18-with-match/lispy/py3.9/examples_test.py diff --git a/02-array-seq/lispy/py3.10/examples_test.py b/02-array-seq/lispy/py3.10/examples_test.py new file mode 100644 index 0000000..ba6bd10 --- /dev/null +++ b/02-array-seq/lispy/py3.10/examples_test.py @@ -0,0 +1,258 @@ +""" +Doctests for `parse` +-------------------- + +# tag::PARSE[] +>>> from lis import parse +>>> parse('1.5') +1.5 +>>> parse('ni!') +'ni!' +>>> parse('(gcd 18 45)') +['gcd', 18, 45] +>>> parse(''' +... (define double +... (lambda (n) +... (* n 2))) +... ''') +['define', 'double', ['lambda', ['n'], ['*', 'n', 2]]] + +# end::PARSE[] + +Doctest for `Environment` +------------------------- + +# tag::ENVIRONMENT[] +>>> from lis import Environment +>>> inner_env = {'a': 2} +>>> outer_env = {'a': 0, 'b': 1} +>>> env = Environment(inner_env, outer_env) +>>> env['a'] = 111 # <1> +>>> env['c'] = 222 +>>> env +Environment({'a': 111, 'c': 222}, {'a': 0, 'b': 1}) +>>> env.change('b', 333) # <2> +>>> env +Environment({'a': 111, 'c': 222}, {'a': 0, 'b': 333}) + +# end::ENVIRONMENT[] + +Doctests for `evaluate` +----------------------- + +# tag::EVAL_NUMBER[] +>>> from lis import parse, evaluate, standard_env +>>> evaluate(parse('1.5'), {}) +1.5 + +# end::EVAL_NUMBER[] + +# tag::EVAL_SYMBOL[] +>>> from lis import standard_env +>>> evaluate(parse('+'), standard_env()) + +>>> evaluate(parse('ni!'), standard_env()) +Traceback (most recent call last): + ... +KeyError: 'ni!' + +# end::EVAL_SYMBOL[] + + +# tag::EVAL_QUOTE[] +>>> evaluate(parse('(quote no-such-name)'), standard_env()) +'no-such-name' +>>> evaluate(parse('(quote (99 bottles of beer))'), standard_env()) +[99, 'bottles', 'of', 'beer'] +>>> evaluate(parse('(quote (/ 10 0))'), standard_env()) +['/', 10, 0] + +# end::EVAL_QUOTE[] + +# tag::EVAL_IF[] +>>> evaluate(parse('(if (= 3 3) 1 0))'), standard_env()) +1 +>>> evaluate(parse('(if (= 3 4) 1 0))'), standard_env()) +0 + +# end::EVAL_IF[] + + +# tag::EVAL_LAMBDA[] +>>> expr = '(lambda (a b) (* (/ a b) 100))' +>>> f = evaluate(parse(expr), standard_env()) +>>> f # doctest: +ELLIPSIS + +>>> f(15, 20) +75.0 + +# end::EVAL_LAMBDA[] + +# tag::EVAL_DEFINE[] +>>> global_env = standard_env() +>>> evaluate(parse('(define answer (* 7 6))'), global_env) +>>> global_env['answer'] +42 + +# end::EVAL_DEFINE[] + +# tag::EVAL_DEFUN[] +>>> global_env = standard_env() +>>> percent = '(define (% a b) (* (/ a b) 100))' +>>> evaluate(parse(percent), global_env) +>>> global_env['%'] # doctest: +ELLIPSIS + +>>> global_env['%'](170, 200) +85.0 + +# end::EVAL_DEFUN[] + +function call: + +# tag::EVAL_CALL[] +>>> evaluate(parse('(% (* 12 14) (- 500 100))'), global_env) +42.0 + +# end::EVAL_CALL[] + +# tag::EVAL_SYNTAX_ERROR[] +>>> evaluate(parse('(lambda is not like this)'), standard_env()) +Traceback (most recent call last): + ... +SyntaxError: (lambda is not like this) + +# end::EVAL_SYNTAX_ERROR[] + +""" + +import math + +from lis import run + + +fact_src = """ +(define (! n) + (if (< n 2) + 1 + (* n (! (- n 1))) + ) +) +(! 42) +""" +def test_factorial(): + got = run(fact_src) + assert got == 1405006117752879898543142606244511569936384000000000 + assert got == math.factorial(42) + + +gcd_src = """ +(define (mod m n) + (- m (* n (// m n)))) +(define (gcd m n) + (if (= n 0) + m + (gcd n (mod m n)))) +(display (gcd 18 45)) +""" +def test_gcd(capsys): + run(gcd_src) + captured = capsys.readouterr() + assert captured.out == '9\n' + + +quicksort_src = """ +(define (quicksort lst) + (if (null? lst) + lst + (begin + (define pivot (car lst)) + (define rest (cdr lst)) + (append + (quicksort + (filter (lambda (x) (< x pivot)) rest)) + (list pivot) + (quicksort + (filter (lambda (x) (>= x pivot)) rest))) + ) + ) +) +(quicksort (list 2 1 6 3 4 0 8 9 7 5)) +""" +def test_quicksort(): + got = run(quicksort_src) + assert got == [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] + + +# Example from Structure and Interpretation of Computer Programs +# https://mitpress.mit.edu/sites/default/files/sicp/full-text/sicp/book/node12.html + +newton_src = """ +(define (sqrt x) + (sqrt-iter 1.0 x)) +(define (sqrt-iter guess x) + (if (good-enough? guess x) + guess + (sqrt-iter (improve guess x) x))) +(define (good-enough? guess x) + (< (abs (- (* guess guess) x)) 0.001)) +(define (improve guess x) + (average guess (/ x guess))) +(define (average x y) + (/ (+ x y) 2)) +(sqrt 123454321) +""" +def test_newton(): + got = run(newton_src) + assert math.isclose(got, 11111) + + +closure_src = """ +(define (make-adder increment) + (lambda (x) (+ increment x)) +) +(define inc (make-adder 1)) +(inc 99) +""" +def test_closure(): + got = run(closure_src) + assert got == 100 + +closure_with_change_src = """ +(define (make-counter) + (define n 0) + (lambda () + (set! n (+ n 1)) + n) +) +(define counter (make-counter)) +(display (counter)) +(display (counter)) +(display (counter)) +""" +def test_closure_with_change(capsys): + run(closure_with_change_src) + captured = capsys.readouterr() + assert captured.out == '1\n2\n3\n' + + + +# tag::RUN_AVERAGER[] +closure_averager_src = """ +(define (make-averager) + (define count 0) + (define total 0) + (lambda (new-value) + (set! count (+ count 1)) + (set! total (+ total new-value)) + (/ total count) + ) +) +(define avg (make-averager)) +(avg 10) +(avg 11) +(avg 15) +""" +def test_closure_averager(): + got = run(closure_averager_src) + assert got == 12.0 +# end::RUN_AVERAGER[] diff --git a/02-array-seq/lispy/py3.10/lis.py b/02-array-seq/lispy/py3.10/lis.py new file mode 100755 index 0000000..cffb9a1 --- /dev/null +++ b/02-array-seq/lispy/py3.10/lis.py @@ -0,0 +1,219 @@ +#!/usr/bin/env python + +################ Lispy: Scheme Interpreter in Python 3.10 + +## (c) Peter Norvig, 2010-18; See http://norvig.com/lispy.html +## Minor edits for Fluent Python, Second Edition (O'Reilly, 2021) +## by Luciano Ramalho, adding type hints and pattern matching. + + +################ Imports and Types + +# tag::IMPORTS[] +import math +import operator as op +from collections import ChainMap +from itertools import chain +from typing import Any, TypeAlias, NoReturn + +Symbol: TypeAlias = str +Atom: TypeAlias = float | int | Symbol +Expression: TypeAlias = Atom | list +# end::IMPORTS[] + + +################ Parsing: parse, tokenize, and read_from_tokens + +def parse(program: str) -> Expression: + "Read a Scheme expression from a string." + return read_from_tokens(tokenize(program)) + +def tokenize(s: str) -> list[str]: + "Convert a string into a list of tokens." + return s.replace('(', ' ( ').replace(')', ' ) ').split() + +def read_from_tokens(tokens: list[str]) -> Expression: + "Read an expression from a sequence of tokens." + if len(tokens) == 0: + raise SyntaxError('unexpected EOF while reading') + token = tokens.pop(0) + if '(' == token: + exp = [] + while tokens[0] != ')': + exp.append(read_from_tokens(tokens)) + tokens.pop(0) # discard ')' + return exp + elif ')' == token: + raise SyntaxError('unexpected )') + else: + return parse_atom(token) + +def parse_atom(token: str) -> Atom: + "Numbers become numbers; every other token is a symbol." + try: + return int(token) + except ValueError: + try: + return float(token) + except ValueError: + return Symbol(token) + + +################ Global Environment + +# tag::ENV_CLASS[] +class Environment(ChainMap[Symbol, Any]): + "A ChainMap that allows changing an item in-place." + + def change(self, key: Symbol, value: object) -> None: + "Find where key is defined and change the value there." + for map in self.maps: + if key in map: + map[key] = value + return + raise KeyError(key) +# end::ENV_CLASS[] + + +def standard_env() -> Environment: + "An environment with some Scheme standard procedures." + env = Environment() + env.update(vars(math)) # sin, cos, sqrt, pi, ... + env.update({ + '+': op.add, + '-': op.sub, + '*': op.mul, + '/': op.truediv, + '//': op.floordiv, + '>': op.gt, + '<': op.lt, + '>=': op.ge, + '<=': op.le, + '=': op.eq, + 'abs': abs, + 'append': lambda *args: list(chain(*args)), + '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, + 'display': lambda x: print(lispstr(x)), + 'eq?': op.is_, + 'equal?': op.eq, + 'filter': lambda *args: list(filter(*args)), + '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, (int, float)), + 'procedure?': callable, + 'round': round, + 'symbol?': lambda x: isinstance(x, Symbol), + }) + return env + + +################ Interaction: A REPL + +# tag::REPL[] +def repl(prompt: str = 'lis.py> ') -> NoReturn: + "A prompt-read-eval-print loop." + global_env = standard_env() + while True: + ast = parse(input(prompt)) + val = evaluate(ast, global_env) + if val is not None: + print(lispstr(val)) + +def lispstr(exp: object) -> str: + "Convert a Python object back into a Lisp-readable string." + if isinstance(exp, list): + return '(' + ' '.join(map(lispstr, exp)) + ')' + else: + return str(exp) +# end::REPL[] + + +################ Evaluator + +KEYWORDS = ['quote', 'if', 'lambda', 'define', 'set!'] + +# tag::EVAL_MATCH_TOP[] +def evaluate(exp: Expression, env: Environment) -> Any: + "Evaluate an expression in an environment." + match exp: +# end::EVAL_MATCH_TOP[] + case int(x) | float(x): + return x + case Symbol(name): + return env[name] +# tag::EVAL_MATCH_MIDDLE[] + case ['quote', x]: # <1> + return x + case ['if', test, consequence, alternative]: # <2> + if evaluate(test, env): + return evaluate(consequence, env) + else: + return evaluate(alternative, env) + case ['lambda', [*parms], *body] if body: # <3> + return Procedure(parms, body, env) + case ['define', Symbol(name), value_exp]: # <4> + env[name] = evaluate(value_exp, env) +# end::EVAL_MATCH_MIDDLE[] + case ['define', [Symbol(name), *parms], *body] if body: + env[name] = Procedure(parms, body, env) + case ['set!', Symbol(name), value_exp]: + env.change(name, evaluate(value_exp, env)) + case [func_exp, *args] if func_exp not in KEYWORDS: + proc = evaluate(func_exp, env) + values = [evaluate(arg, env) for arg in args] + return proc(*values) +# tag::EVAL_MATCH_BOTTOM[] + case _: # <5> + raise SyntaxError(lispstr(exp)) +# end::EVAL_MATCH_BOTTOM[] + +# tag::PROCEDURE[] +class Procedure: + "A user-defined Scheme procedure." + + def __init__( # <1> + self, parms: list[Symbol], body: list[Expression], env: Environment + ): + self.parms = parms # <2> + self.body = body + self.env = env + + def __call__(self, *args: Expression) -> Any: # <3> + local_env = dict(zip(self.parms, args)) # <4> + env = Environment(local_env, self.env) # <5> + for exp in self.body: # <6> + result = evaluate(exp, env) + return result # <7> +# end::PROCEDURE[] + + +################ command-line interface + +def run(source: str) -> Any: + global_env = standard_env() + tokens = tokenize(source) + while tokens: + exp = read_from_tokens(tokens) + result = evaluate(exp, global_env) + return result + +def main(args: list[str]) -> None: + if len(args) == 1: + with open(args[0]) as fp: + run(fp.read()) + else: + repl() + +if __name__ == '__main__': + import sys + main(sys.argv[1:]) diff --git a/02-array-seq/lispy/py3.10/lis_test.py b/02-array-seq/lispy/py3.10/lis_test.py new file mode 100644 index 0000000..03dc15c --- /dev/null +++ b/02-array-seq/lispy/py3.10/lis_test.py @@ -0,0 +1,180 @@ +from typing import Optional + +from pytest import mark, fixture + +from lis import parse, evaluate, Expression, Environment, standard_env + +############################################################# tests for parse + +@mark.parametrize( 'source, expected', [ + ('7', 7), + ('x', 'x'), + ('(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']), +]) +def test_parse(source: str, expected: Expression) -> None: + got = parse(source) + assert got == expected + + +########################################################## tests for evaluate + +# Norvig's tests are not isolated: they assume the +# same environment from first to last test. +global_env_for_first_test = standard_env() + +@mark.parametrize( 'source, expected', [ + ("(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]), +]) +def test_evaluate(source: str, expected: Optional[Expression]) -> None: + got = evaluate(parse(source), global_env_for_first_test) + assert got == expected + + +@fixture +def std_env() -> Environment: + return standard_env() + +# tests for cases in evaluate + +def test_evaluate_variable() -> None: + env = Environment({'x': 10}) + source = 'x' + expected = 10 + got = evaluate(parse(source), env) + assert got == expected + + +def test_evaluate_literal(std_env: Environment) -> None: + source = '3.3' + expected = 3.3 + got = evaluate(parse(source), std_env) + assert got == expected + + +def test_evaluate_quote(std_env: Environment) -> None: + source = '(quote (1.1 is not 1))' + expected = [1.1, 'is', 'not', 1] + got = evaluate(parse(source), std_env) + assert got == expected + + +def test_evaluate_if_true(std_env: Environment) -> None: + source = '(if 1 10 no-such-thing)' + expected = 10 + got = evaluate(parse(source), std_env) + assert got == expected + + +def test_evaluate_if_false(std_env: Environment) -> None: + source = '(if 0 no-such-thing 20)' + expected = 20 + got = evaluate(parse(source), std_env) + assert got == expected + + +def test_define(std_env: Environment) -> None: + source = '(define answer (* 6 7))' + got = evaluate(parse(source), std_env) + assert got is None + assert std_env['answer'] == 42 + + +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.env is std_env + assert func(1, 2) == 2 + assert func(3, 2) == 3 + + +def test_begin(std_env: Environment) -> None: + source = """ + (begin + (define x (* 2 3)) + (* x 7) + ) + """ + got = evaluate(parse(source), std_env) + assert got == 42 + + +def test_invocation_builtin_car(std_env: Environment) -> None: + source = '(car (quote (11 22 33)))' + got = evaluate(parse(source), std_env) + assert got == 11 + + +def test_invocation_builtin_append(std_env: Environment) -> None: + source = '(append (quote (a b)) (quote (c d)))' + got = evaluate(parse(source), std_env) + assert got == ['a', 'b', 'c', 'd'] + + +def test_invocation_builtin_map(std_env: Environment) -> None: + source = '(map (lambda (x) (* x 2)) (quote (1 2 3))))' + got = evaluate(parse(source), std_env) + assert got == [2, 4, 6] + + +def test_invocation_user_procedure(std_env: Environment) -> None: + source = """ + (begin + (define max (lambda (a b) (if (>= a b) a b))) + (max 22 11) + ) + """ + got = evaluate(parse(source), std_env) + assert got == 22 + + +def test_define_function(std_env: Environment) -> None: + source = '(define (max a b) (if (>= a b) a b))' + got = evaluate(parse(source), std_env) + 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.env is std_env + assert max_fn(1, 2) == 2 + assert max_fn(3, 2) == 3 diff --git a/02-array-seq/lispy/py3.10/quicksort.scm b/02-array-seq/lispy/py3.10/quicksort.scm new file mode 100644 index 0000000..08dd596 --- /dev/null +++ b/02-array-seq/lispy/py3.10/quicksort.scm @@ -0,0 +1,17 @@ +(define (quicksort lst) + (if (null? lst) + lst + (begin + (define pivot (car lst)) + (define rest (cdr lst)) + (append + (quicksort + (filter (lambda (x) (< x pivot)) rest)) + (list pivot) + (quicksort + (filter (lambda (x) (>= x pivot)) rest))) + ) + ) +) +(display + (quicksort (list 2 1 6 3 4 0 8 9 7 5))) diff --git a/02-array-seq/lispy/py3.9/README.md b/02-array-seq/lispy/py3.9/README.md new file mode 100644 index 0000000..fdb1f08 --- /dev/null +++ b/02-array-seq/lispy/py3.9/README.md @@ -0,0 +1,33 @@ +# Changes from the original + +While adapting Peter Norvig's [lis.py](https://github.com/norvig/pytudes/blob/705c0a335c1811a203e79587d7d41865cf7f41c7/py/lis.py) for +use in _Fluent Python, Second Edition_, I made a few changes for didactic reasons. + +_Luciano Ramalho_ + +## Major changes + +* Make the `lambda` form accept more than one expression as the body. This is consistent with [_Scheme_ syntax](https://web.mit.edu/scheme_v9.2/doc/mit-scheme-ref/Lambda-Expressions.html), and provides a useful example for the book. To implement this: + * In `Procedure.__call__`: evaluate `self.body` as a list of expressions, instead of a single expression. Return the value of the last expression. + * In `evaluate()`: when processing `lambda`, unpack expression into `(_, parms, *body)`, to accept a list of expressions as the body. +* Remove the `global_env` global `dict`. It is only used as a default value for the `env` parameter in `evaluate()`, but it is unsafe to use mutable data structures as parameter default values. To implement this: + * In `repl()`: create local variable `global_env` and pass it as the `env` paramater of `evaluate()`. + * In `evaluate()`, remove `global_env` default value for `env`. +* Rewrite the custom test script +[lispytest.py](https://github.com/norvig/pytudes/blob/705c0a335c1811a203e79587d7d41865cf7f41c7/py/lispytest.py) as +[lis_test.py](https://github.com/fluentpython/example-code-2e/blob/master/02-array-seq/lispy/py3.9/lis_test.py): +a standard [pytest](https://docs.pytest.org) test suite including new test cases, preserving all Norvig's test cases for +[lis.py](https://github.com/norvig/pytudes/blob/705c0a335c1811a203e79587d7d41865cf7f41c7/py/lis.py) +but removing the test cases for the features implemented only in +[lispy.py](https://github.com/norvig/pytudes/blob/705c0a335c1811a203e79587d7d41865cf7f41c7/py/lispy.py). + + +## Minor changes + +Cosmetic changes to make the code look more familiar to +Python programmers, the audience of _Fluent Python_. + +* Rename `eval()` to `evaluate()`, to avoid confusion with Python's `eval` built-in function. +* Refer to the list class as `list` instead of aliasing as `List`, to avoid confusion with `typing.List` which is often imported as `List`. +* Import `collections.ChainMap` as `ChainMap` instead of `Environment`. + diff --git a/02-array-seq/lispy/py3.9/examples_test.py b/02-array-seq/lispy/py3.9/examples_test.py new file mode 100644 index 0000000..bdcda48 --- /dev/null +++ b/02-array-seq/lispy/py3.9/examples_test.py @@ -0,0 +1,196 @@ +""" +Doctests for `parse` +-------------------- + +# tag::PARSE[] +>>> from lis import parse +>>> parse('1.5') +1.5 +>>> parse('ni!') +'ni!' +>>> parse('(gcd 18 45)') +['gcd', 18, 45] +>>> parse(''' +... (define double +... (lambda (n) +... (* n 2))) +... ''') +['define', 'double', ['lambda', ['n'], ['*', 'n', 2]]] + +# end::PARSE[] + +Doctest for `Environment` +------------------------- + +# tag::ENVIRONMENT[] +>>> from lis import Environment +>>> inner_env = {'a': 2} +>>> outer_env = {'a': 0, 'b': 1} +>>> env = Environment(inner_env, outer_env) +>>> env['a'] = 111 # <1> +>>> env['c'] = 222 +>>> env +Environment({'a': 111, 'c': 222}, {'a': 0, 'b': 1}) +>>> env.change('b', 333) # <2> +>>> env +Environment({'a': 111, 'c': 222}, {'a': 0, 'b': 333}) + +# end::ENVIRONMENT[] + +Doctests for `evaluate` +----------------------- + +# tag::EVAL_NUMBER[] +>>> from lis import parse, evaluate, standard_env +>>> evaluate(parse('1.5'), {}) +1.5 + +# end::EVAL_NUMBER[] + +# tag::EVAL_SYMBOL[] +>>> from lis import standard_env +>>> evaluate(parse('+'), standard_env()) + +>>> evaluate(parse('ni!'), standard_env()) +Traceback (most recent call last): + ... +KeyError: 'ni!' + +# end::EVAL_SYMBOL[] + + +# tag::EVAL_QUOTE[] +>>> evaluate(parse('(quote no-such-name)'), standard_env()) +'no-such-name' +>>> evaluate(parse('(quote (99 bottles of beer))'), standard_env()) +[99, 'bottles', 'of', 'beer'] +>>> evaluate(parse('(quote (/ 10 0))'), standard_env()) +['/', 10, 0] + +# end::EVAL_QUOTE[] + +# tag::EVAL_IF[] +>>> evaluate(parse('(if (= 3 3) 1 0))'), standard_env()) +1 +>>> evaluate(parse('(if (= 3 4) 1 0))'), standard_env()) +0 + +# end::EVAL_IF[] + + +# tag::EVAL_LAMBDA[] +>>> expr = '(lambda (a b) (* (/ a b) 100))' +>>> f = evaluate(parse(expr), standard_env()) +>>> f # doctest: +ELLIPSIS + +>>> f(15, 20) +75.0 + +# end::EVAL_LAMBDA[] + +# tag::EVAL_DEFINE[] +>>> global_env = standard_env() +>>> evaluate(parse('(define answer (* 7 6))'), global_env) +>>> global_env['answer'] +42 + +# end::EVAL_DEFINE[] + +# tag::EVAL_DEFUN[] +>>> global_env = standard_env() +>>> percent = '(define % (lambda (a b) (* (/ a b) 100)))' +>>> evaluate(parse(percent), global_env) +>>> global_env['%'] # doctest: +ELLIPSIS + +>>> global_env['%'](170, 200) +85.0 + +# end::EVAL_DEFUN[] + +function call: + +# tag::EVAL_CALL[] +>>> evaluate(parse('(% (* 12 14) (- 500 100))'), global_env) +42.0 + +# end::EVAL_CALL[] + +""" + +import math + +from lis import run + + +fact_src = """ +(define ! + (lambda (n) + (if (< n 2) + 1 + (* n (! (- n 1))) + ) + ) +) +(! 42) +""" +def test_factorial(): + got = run(fact_src) + assert got == 1405006117752879898543142606244511569936384000000000 + assert got == math.factorial(42) + +closure_src = """ +(define make-adder + (lambda (increment) + (lambda (x) (+ increment x)) + ) +) +(define inc (make-adder 1)) +(inc 99) +""" +def test_closure(): + got = run(closure_src) + assert got == 100 + +closure_with_change_src = """ +(define make-counter + (lambda () + (define n 0) + (lambda () + (set! n (+ n 1)) + n) + ) +) +(define counter (make-counter)) +(display (counter)) +(display (counter)) +(display (counter)) +""" +def test_closure_with_change(capsys): + run(closure_with_change_src) + captured = capsys.readouterr() + assert captured.out == '1\n2\n3\n' + + + +# tag::RUN_AVERAGER[] +closure_averager_src = """ +(define make-averager + (lambda () + (define count 0) + (define total 0) + (lambda (new-value) + (set! count (+ count 1)) + (set! total (+ total new-value)) + (/ total count) + ) + ) +) +(define avg (make-averager)) +(avg 10) +(avg 11) +(avg 15) +""" +def test_closure_averager(): + got = run(closure_averager_src) + assert got == 12.0 +# end::RUN_AVERAGER[] \ No newline at end of file diff --git a/02-array-seq/lispy/py3.9/lis.py b/02-array-seq/lispy/py3.9/lis.py new file mode 100644 index 0000000..843d4cc --- /dev/null +++ b/02-array-seq/lispy/py3.9/lis.py @@ -0,0 +1,209 @@ +#!/usr/bin/env python + +################ Lispy: Scheme Interpreter in Python 3.9 + +## (c) Peter Norvig, 2010-18; See http://norvig.com/lispy.html +## Minor edits for Fluent Python, Second Edition (O'Reilly, 2021) +## by Luciano Ramalho, adding type hints and pattern matching. + + +################ Imports and Types + +import math +import operator as op +from collections import ChainMap +from itertools import chain +from typing import Any, Union, NoReturn + +Symbol = str +Atom = Union[float, int, Symbol] +Expression = Union[Atom, list] + + +################ Parsing: parse, tokenize, and read_from_tokens + +def parse(program: str) -> Expression: + "Read a Scheme expression from a string." + return read_from_tokens(tokenize(program)) + +def tokenize(s: str) -> list[str]: + "Convert a string into a list of tokens." + return s.replace('(', ' ( ').replace(')', ' ) ').split() + +def read_from_tokens(tokens: list[str]) -> Expression: + "Read an expression from a sequence of tokens." + if len(tokens) == 0: + raise SyntaxError('unexpected EOF while reading') + token = tokens.pop(0) + if '(' == token: + exp = [] + while tokens[0] != ')': + exp.append(read_from_tokens(tokens)) + tokens.pop(0) # discard ')' + return exp + elif ')' == token: + raise SyntaxError('unexpected )') + else: + return parse_atom(token) + +def parse_atom(token: str) -> Atom: + "Numbers become numbers; every other token is a symbol." + try: + return int(token) + except ValueError: + try: + return float(token) + except ValueError: + return Symbol(token) + + +################ Global Environment + +class Environment(ChainMap[Symbol, Any]): + "A ChainMap that allows changing an item in-place." + + def change(self, key: Symbol, value: object) -> None: + "Find where key is defined and change the value there." + for map in self.maps: + if key in map: + map[key] = value # type: ignore[index] + return + raise KeyError(key) + + +def standard_env() -> Environment: + "An environment with some Scheme standard procedures." + env = Environment() + env.update(vars(math)) # sin, cos, sqrt, pi, ... + env.update({ + '+': op.add, + '-': op.sub, + '*': op.mul, + '/': op.truediv, + '//': op.floordiv, + '>': op.gt, + '<': op.lt, + '>=': op.ge, + '<=': op.le, + '=': op.eq, + 'abs': abs, + 'append': lambda *args: list(chain(*args)), + '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, + 'display': lambda x: print(lispstr(x)), + 'eq?': op.is_, + 'equal?': op.eq, + 'filter': lambda *args: list(filter(*args)), + '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, (int, float)), + 'procedure?': callable, + 'round': round, + 'symbol?': lambda x: isinstance(x, Symbol), + }) + return env + + +################ Interaction: A REPL + +def repl(prompt: str = 'lis.py> ') -> NoReturn: + "A prompt-read-eval-print loop." + global_env = standard_env() + while True: + ast = parse(input(prompt)) + val = evaluate(ast, global_env) + if val is not None: + print(lispstr(val)) + +def lispstr(exp: object) -> str: + "Convert a Python object back into a Lisp-readable string." + if isinstance(exp, list): + return '(' + ' '.join(map(lispstr, exp)) + ')' + else: + return str(exp) + + +################ Evaluator + +# tag::EVAL_IF_TOP[] +def evaluate(exp: Expression, env: Environment) -> Any: + "Evaluate an expression in an environment." + if isinstance(exp, Symbol): # variable reference + return env[exp] +# end::EVAL_IF_TOP[] + elif not isinstance(exp, list): # constant literal + return exp +# tag::EVAL_IF_MIDDLE[] + elif exp[0] == 'quote': # (quote exp) + (_, x) = exp + return x + elif exp[0] == 'if': # (if test conseq alt) + (_, test, consequence, alternative) = exp + if evaluate(test, env): + return evaluate(consequence, env) + else: + return evaluate(alternative, env) + elif exp[0] == 'lambda': # (lambda (parm…) body…) + (_, parms, *body) = exp + return Procedure(parms, body, env) + elif exp[0] == 'define': + (_, name, value_exp) = exp + env[name] = evaluate(value_exp, env) +# end::EVAL_IF_MIDDLE[] + elif exp[0] == 'set!': + (_, name, value_exp) = exp + env.change(name, evaluate(value_exp, env)) + else: # (proc arg…) + (func_exp, *args) = exp + proc = evaluate(func_exp, env) + args = [evaluate(arg, env) for arg in args] + return proc(*args) + + +class Procedure: + "A user-defined Scheme procedure." + + def __init__( + self, parms: list[Symbol], body: list[Expression], env: Environment + ): + self.parms = parms + self.body = body + self.env = env + + def __call__(self, *args: Expression) -> Any: + local_env = dict(zip(self.parms, args)) + env = Environment(local_env, self.env) + for exp in self.body: + result = evaluate(exp, env) + return result + + +################ command-line interface + +def run(source: str) -> Any: + global_env = standard_env() + tokens = tokenize(source) + while tokens: + exp = read_from_tokens(tokens) + result = evaluate(exp, global_env) + return result + +def main(args: list[str]) -> None: + if len(args) == 1: + with open(args[0]) as fp: + run(fp.read()) + else: + repl() + +if __name__ == '__main__': + import sys + main(sys.argv[1:]) diff --git a/02-array-seq/lispy/py3.9/lis_test.py b/02-array-seq/lispy/py3.9/lis_test.py new file mode 100644 index 0000000..64bafb4 --- /dev/null +++ b/02-array-seq/lispy/py3.9/lis_test.py @@ -0,0 +1,180 @@ +from typing import Optional + +from pytest import mark, fixture + +from lis import parse, evaluate, Expression, Environment, standard_env + +############################################################# tests for parse + +@mark.parametrize( 'source, expected', [ + ('7', 7), + ('x', 'x'), + ('(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']), +]) +def test_parse(source: str, expected: Expression) -> None: + got = parse(source) + assert got == expected + + +########################################################## tests for evaluate + +# Norvig's tests are not isolated: they assume the +# same environment from first to last test. +global_env_for_first_test = standard_env() + +@mark.parametrize( 'source, expected', [ + ("(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]), +]) +def test_evaluate(source: str, expected: Optional[Expression]) -> None: + got = evaluate(parse(source), global_env_for_first_test) + assert got == expected + + +@fixture +def std_env() -> Environment: + return standard_env() + +# tests for cases in evaluate + +def test_evaluate_variable() -> None: + env = Environment({'x': 10}) + source = 'x' + expected = 10 + got = evaluate(parse(source), env) + assert got == expected + + +def test_evaluate_literal(std_env: Environment) -> None: + source = '3.3' + expected = 3.3 + got = evaluate(parse(source), std_env) + assert got == expected + + +def test_evaluate_quote(std_env: Environment) -> None: + source = '(quote (1.1 is not 1))' + expected = [1.1, 'is', 'not', 1] + got = evaluate(parse(source), std_env) + assert got == expected + + +def test_evaluate_if_true(std_env: Environment) -> None: + source = '(if 1 10 no-such-thing)' + expected = 10 + got = evaluate(parse(source), std_env) + assert got == expected + + +def test_evaluate_if_false(std_env: Environment) -> None: + source = '(if 0 no-such-thing 20)' + expected = 20 + got = evaluate(parse(source), std_env) + assert got == expected + + +def test_define(std_env: Environment) -> None: + source = '(define answer (* 6 7))' + got = evaluate(parse(source), std_env) + assert got is None + assert std_env['answer'] == 42 + + +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.env is std_env + assert func(1, 2) == 2 + assert func(3, 2) == 3 + + +def test_begin(std_env: Environment) -> None: + source = """ + (begin + (define x (* 2 3)) + (* x 7) + ) + """ + got = evaluate(parse(source), std_env) + assert got == 42 + + +def test_invocation_builtin_car(std_env: Environment) -> None: + source = '(car (quote (11 22 33)))' + got = evaluate(parse(source), std_env) + assert got == 11 + + +def test_invocation_builtin_append(std_env: Environment) -> None: + source = '(append (quote (a b)) (quote (c d)))' + got = evaluate(parse(source), std_env) + assert got == ['a', 'b', 'c', 'd'] + + +def test_invocation_builtin_map(std_env: Environment) -> None: + source = '(map (lambda (x) (* x 2)) (quote (1 2 3))))' + got = evaluate(parse(source), std_env) + assert got == [2, 4, 6] + + +def test_invocation_user_procedure(std_env: Environment) -> None: + source = """ + (begin + (define max (lambda (a b) (if (>= a b) a b))) + (max 22 11) + ) + """ + got = evaluate(parse(source), std_env) + assert got == 22 + + +def test_define_function(std_env: Environment) -> None: + source = '(define max (lambda (a b) (if (>= a b) a b)))' + got = evaluate(parse(source), std_env) + 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.env is std_env + assert max_fn(1, 2) == 2 + assert max_fn(3, 2) == 3 diff --git a/18-with-match/lispy/py3.10/examples_test.py b/18-with-match/lispy/py3.10/examples_test.py index ed4b68a..ba6bd10 100644 --- a/18-with-match/lispy/py3.10/examples_test.py +++ b/18-with-match/lispy/py3.10/examples_test.py @@ -2,16 +2,12 @@ Doctests for `parse` -------------------- -# tag::PARSE_ATOM[] +# tag::PARSE[] >>> from lis import parse >>> parse('1.5') 1.5 >>> parse('ni!') 'ni!' - -# end::PARSE_ATOM[] - -# tag::PARSE_LIST[] >>> parse('(gcd 18 45)') ['gcd', 18, 45] >>> parse(''' @@ -21,15 +17,15 @@ Doctests for `parse` ... ''') ['define', 'double', ['lambda', ['n'], ['*', 'n', 2]]] -# end::PARSE_LIST[] +# end::PARSE[] Doctest for `Environment` ------------------------- # tag::ENVIRONMENT[] >>> from lis import Environment ->>> outer_env = {'a': 0, 'b': 1} >>> inner_env = {'a': 2} +>>> outer_env = {'a': 0, 'b': 1} >>> env = Environment(inner_env, outer_env) >>> env['a'] = 111 # <1> >>> env['c'] = 222 @@ -64,11 +60,11 @@ KeyError: 'ni!' # tag::EVAL_QUOTE[] ->>> evaluate(parse('(quote no-such-name)'), {}) +>>> evaluate(parse('(quote no-such-name)'), standard_env()) 'no-such-name' ->>> evaluate(parse('(quote (99 bottles of beer))'), {}) +>>> evaluate(parse('(quote (99 bottles of beer))'), standard_env()) [99, 'bottles', 'of', 'beer'] ->>> evaluate(parse('(quote (/ 10 0))'), {}) +>>> evaluate(parse('(quote (/ 10 0))'), standard_env()) ['/', 10, 0] # end::EVAL_QUOTE[] @@ -156,11 +152,12 @@ gcd_src = """ (if (= n 0) m (gcd n (mod m n)))) -(gcd 18 45) +(display (gcd 18 45)) """ -def test_gcd(): - got = run(gcd_src) - assert got == 9 +def test_gcd(capsys): + run(gcd_src) + captured = capsys.readouterr() + assert captured.out == '9\n' quicksort_src = """ @@ -216,7 +213,7 @@ closure_src = """ (define inc (make-adder 1)) (inc 99) """ -def test_newton(): +def test_closure(): got = run(closure_src) assert got == 100 @@ -228,13 +225,15 @@ closure_with_change_src = """ n) ) (define counter (make-counter)) -(counter) -(counter) -(counter) +(display (counter)) +(display (counter)) +(display (counter)) """ -def test_closure_with_change(): - got = run(closure_with_change_src) - assert got == 3 +def test_closure_with_change(capsys): + run(closure_with_change_src) + captured = capsys.readouterr() + assert captured.out == '1\n2\n3\n' + # tag::RUN_AVERAGER[] @@ -256,4 +255,4 @@ closure_averager_src = """ def test_closure_averager(): got = run(closure_averager_src) assert got == 12.0 -# end::RUN_AVERAGER[] \ No newline at end of file +# end::RUN_AVERAGER[] diff --git a/18-with-match/lispy/py3.10/lis.py b/18-with-match/lispy/py3.10/lis.py index 4a5195c..e05ee9c 100755 --- a/18-with-match/lispy/py3.10/lis.py +++ b/18-with-match/lispy/py3.10/lis.py @@ -58,10 +58,11 @@ def parse_atom(token: str) -> Atom: except ValueError: return Symbol(token) + ################ Global Environment # tag::ENV_CLASS[] -class Environment(ChainMap): +class Environment(ChainMap[Symbol, Any]): "A ChainMap that allows changing an item in-place." def change(self, key: Symbol, value: object) -> None: @@ -73,7 +74,6 @@ class Environment(ChainMap): raise KeyError(key) # end::ENV_CLASS[] - def standard_env() -> Environment: "An environment with some Scheme standard procedures." env = Environment() @@ -119,11 +119,11 @@ def standard_env() -> Environment: ################ Interaction: A REPL # tag::REPL[] -def repl() -> NoReturn: +def repl(prompt: str = 'lis.py> ') -> NoReturn: "A prompt-read-eval-print loop." global_env = standard_env() while True: - ast = parse(input('lis.py> ')) + ast = parse(input(prompt)) val = evaluate(ast, global_env) if val is not None: print(lispstr(val)) @@ -149,8 +149,6 @@ def evaluate(exp: Expression, env: Environment) -> Any: return x case Symbol(var): return env[var] - case []: - return [] case ['quote', x]: return x case ['if', test, consequence, alternative]: @@ -166,8 +164,8 @@ def evaluate(exp: Expression, env: Environment) -> Any: env[name] = Procedure(parms, body, env) case ['set!', Symbol(var), value_exp]: env.change(var, evaluate(value_exp, env)) - case [op, *args] if op not in KEYWORDS: - proc = evaluate(op, env) + case [func_exp, *args] if func_exp not in KEYWORDS: + proc = evaluate(func_exp, env) values = [evaluate(arg, env) for arg in args] return proc(*values) case _: diff --git a/18-with-match/lispy/py3.10/lis_test.py b/18-with-match/lispy/py3.10/lis_test.py index 3688888..03dc15c 100644 --- a/18-with-match/lispy/py3.10/lis_test.py +++ b/18-with-match/lispy/py3.10/lis_test.py @@ -73,10 +73,10 @@ def test_evaluate(source: str, expected: Optional[Expression]) -> None: def std_env() -> Environment: return standard_env() -# tests for each of the cases in evaluate +# tests for cases in evaluate def test_evaluate_variable() -> None: - env: Environment = dict(x=10) + env = Environment({'x': 10}) source = 'x' expected = 10 got = evaluate(parse(source), env) @@ -168,8 +168,6 @@ def test_invocation_user_procedure(std_env: Environment) -> None: assert got == 22 -###################################### for py3.10/lis.py only - def test_define_function(std_env: Environment) -> None: source = '(define (max a b) (if (>= a b) a b))' got = evaluate(parse(source), std_env) diff --git a/18-with-match/lispy/py3.9/examples_test.py b/18-with-match/lispy/py3.9/examples_test.py new file mode 100644 index 0000000..c632662 --- /dev/null +++ b/18-with-match/lispy/py3.9/examples_test.py @@ -0,0 +1,258 @@ +""" +Doctests for `parse` +-------------------- + +# tag::PARSE[] +>>> from lis import parse +>>> parse('1.5') +1.5 +>>> parse('ni!') +'ni!' +>>> parse('(gcd 18 45)') +['gcd', 18, 45] +>>> parse(''' +... (define double +... (lambda (n) +... (* n 2))) +... ''') +['define', 'double', ['lambda', ['n'], ['*', 'n', 2]]] + +# end::PARSE[] + +Doctest for `Environment` +------------------------- + +# tag::ENVIRONMENT[] +>>> from lis import Environment +>>> inner_env = {'a': 2} +>>> outer_env = {'a': 0, 'b': 1} +>>> env = Environment(inner_env, outer_env) +>>> env['a'] = 111 # <1> +>>> env['c'] = 222 +>>> env +Environment({'a': 111, 'c': 222}, {'a': 0, 'b': 1}) +>>> env.change('b', 333) # <2> +>>> env +Environment({'a': 111, 'c': 222}, {'a': 0, 'b': 333}) + +# end::ENVIRONMENT[] + +Doctests for `evaluate` +----------------------- + +# tag::EVAL_NUMBER[] +>>> from lis import parse, evaluate, standard_env +>>> evaluate(parse('1.5'), {}) +1.5 + +# end::EVAL_NUMBER[] + +# tag::EVAL_SYMBOL[] +>>> from lis import standard_env +>>> evaluate(parse('+'), standard_env()) + +>>> evaluate(parse('ni!'), standard_env()) +Traceback (most recent call last): + ... +KeyError: 'ni!' + +# end::EVAL_SYMBOL[] + + +# tag::EVAL_QUOTE[] +>>> evaluate(parse('(quote no-such-name)'), standard_env()) +'no-such-name' +>>> evaluate(parse('(quote (99 bottles of beer))'), standard_env()) +[99, 'bottles', 'of', 'beer'] +>>> evaluate(parse('(quote (/ 10 0))'), standard_env()) +['/', 10, 0] + +# end::EVAL_QUOTE[] + +# tag::EVAL_IF[] +>>> evaluate(parse('(if (= 3 3) 1 0))'), standard_env()) +1 +>>> evaluate(parse('(if (= 3 4) 1 0))'), standard_env()) +0 + +# end::EVAL_IF[] + + +# tag::EVAL_LAMBDA[] +>>> expr = '(lambda (a b) (* (/ a b) 100))' +>>> f = evaluate(parse(expr), standard_env()) +>>> f # doctest: +ELLIPSIS + +>>> f(15, 20) +75.0 + +# end::EVAL_LAMBDA[] + +# tag::EVAL_DEFINE[] +>>> global_env = standard_env() +>>> evaluate(parse('(define answer (* 7 6))'), global_env) +>>> global_env['answer'] +42 + +# end::EVAL_DEFINE[] + +# tag::EVAL_DEFUN[] +>>> global_env = standard_env() +>>> percent = '(define (% a b) (* (/ a b) 100))' +>>> evaluate(parse(percent), global_env) +>>> global_env['%'] # doctest: +ELLIPSIS + +>>> global_env['%'](170, 200) +85.0 + +# end::EVAL_DEFUN[] + +function call: + +# tag::EVAL_CALL[] +>>> evaluate(parse('(% (* 12 14) (- 500 100))'), global_env) +42.0 + +# end::EVAL_CALL[] + +# tag::EVAL_SYNTAX_ERROR[] +>>> evaluate(parse('(lambda is not like this)'), standard_env()) +Traceback (most recent call last): + ... +SyntaxError: (lambda is not like this) + +# end::EVAL_SYNTAX_ERROR[] + +""" + +import math + +from lis import run + + +fact_src = """ +(define (! n) + (if (< n 2) + 1 + (* n (! (- n 1))) + ) +) +(! 42) +""" +def test_factorial(): + got = run(fact_src) + assert got == 1405006117752879898543142606244511569936384000000000 + assert got == math.factorial(42) + + +gcd_src = """ +(define (mod m n) + (- m (* n (// m n)))) +(define (gcd m n) + (if (= n 0) + m + (gcd n (mod m n)))) +(display (gcd 18 45)) +""" +def test_gcd(capsys): + run(gcd_src) + captured = capsys.readouterr() + assert captured.out == '9\n' + + +quicksort_src = """ +(define (quicksort lst) + (if (null? lst) + lst + (begin + (define pivot (car lst)) + (define rest (cdr lst)) + (append + (quicksort + (filter (lambda (x) (< x pivot)) rest)) + (list pivot) + (quicksort + (filter (lambda (x) (>= x pivot)) rest))) + ) + ) +) +(quicksort (list 2 1 6 3 4 0 8 9 7 5)) +""" +def test_quicksort(): + got = run(quicksort_src) + assert got == [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] + + +# Example from Structure and Interpretation of Computer Programs +# https://mitpress.mit.edu/sites/default/files/sicp/full-text/sicp/book/node12.html + +newton_src = """ +(define (sqrt x) + (sqrt-iter 1.0 x)) +(define (sqrt-iter guess x) + (if (good-enough? guess x) + guess + (sqrt-iter (improve guess x) x))) +(define (good-enough? guess x) + (< (abs (- (* guess guess) x)) 0.001)) +(define (improve guess x) + (average guess (/ x guess))) +(define (average x y) + (/ (+ x y) 2)) +(sqrt 123454321) +""" +def test_newton(): + got = run(newton_src) + assert math.isclose(got, 11111) + + +closure_src = """ +(define (make-adder increment) + (lambda (x) (+ increment x)) +) +(define inc (make-adder 1)) +(inc 99) +""" +def test_closure(): + got = run(closure_src) + assert got == 100 + +closure_with_change_src = """ +(define (make-counter) + (define n 0) + (lambda () + (set! n (+ n 1)) + n) +) +(define counter (make-counter)) +(display (counter)) +(display (counter)) +(display (counter)) +""" +def test_closure_with_change(capsys): + run(closure_with_change_src) + captured = capsys.readouterr() + assert captured.out == '1\n2\n3\n' + + + +# tag::RUN_AVERAGER[] +closure_averager_src = """ +(define (make-averager) + (define count 0) + (define total 0) + (lambda (new-value) + (set! count (+ count 1)) + (set! total (+ total new-value)) + (/ total count) + ) +) +(define avg (make-averager)) +(avg 10) +(avg 11) +(avg 15) +""" +def test_closure_averager(): + got = run(closure_averager_src) + assert got == 12.0 +# end::RUN_AVERAGER[] \ No newline at end of file diff --git a/18-with-match/lispy/py3.9/lis.py b/18-with-match/lispy/py3.9/lis.py index 9e4dec1..e8881cd 100644 --- a/18-with-match/lispy/py3.9/lis.py +++ b/18-with-match/lispy/py3.9/lis.py @@ -1,73 +1,24 @@ +#!/usr/bin/env python + ################ Lispy: Scheme Interpreter in Python 3.9 ## (c) Peter Norvig, 2010-18; See http://norvig.com/lispy.html ## Minor edits for Fluent Python, Second Edition (O'Reilly, 2021) ## by Luciano Ramalho, adding type hints and pattern matching. + ################ Imports and Types import math import operator as op from collections import ChainMap -from collections.abc import MutableMapping, Iterator from itertools import chain -from typing import Any, Union +from typing import Any, Union, NoReturn Symbol = str Atom = Union[float, int, Symbol] Expression = Union[Atom, list] -Environment = MutableMapping[Symbol, object] - - -class Procedure: - "A user-defined Scheme procedure." - - def __init__(self, parms: list[Symbol], body: list[Expression], env: Environment): - self.parms = parms - self.body = body - self.env = env - - def __call__(self, *args: Expression) -> Any: - local_env = dict(zip(self.parms, args)) - env: Environment = ChainMap(local_env, self.env) - for exp in self.body: - result = evaluate(exp, env) - return result - - -################ Global Environment - -def standard_env() -> Environment: - "An environment with some Scheme standard procedures." - env: Environment = {} - 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, (int, float)), - 'procedure?': callable, - 'round': round, - 'symbol?': lambda x: isinstance(x, Symbol), - }) - return env ################ Parsing: parse, tokenize, and read_from_tokens @@ -75,12 +26,10 @@ def parse(program: str) -> Expression: "Read a Scheme expression from a string." return read_from_tokens(tokenize(program)) - def tokenize(s: str) -> list[str]: "Convert a string into a list of tokens." return s.replace('(', ' ( ').replace(')', ' ) ').split() - def read_from_tokens(tokens: list[str]) -> Expression: "Read an expression from a sequence of tokens." if len(tokens) == 0: @@ -97,7 +46,6 @@ def read_from_tokens(tokens: list[str]) -> Expression: else: return parse_atom(token) - def parse_atom(token: str) -> Atom: "Numbers become numbers; every other token is a symbol." try: @@ -109,17 +57,73 @@ def parse_atom(token: str) -> Atom: return Symbol(token) +################ Global Environment + +class Environment(ChainMap[Symbol, Any]): + "A ChainMap that allows changing an item in-place." + + def change(self, key: Symbol, value: object) -> None: + "Find where key is defined and change the value there." + for map in self.maps: + if key in map: + map[key] = value # type: ignore[index] + return + raise KeyError(key) + + +def standard_env() -> Environment: + "An environment with some Scheme standard procedures." + env = Environment() + env.update(vars(math)) # sin, cos, sqrt, pi, ... + env.update({ + '+': op.add, + '-': op.sub, + '*': op.mul, + '/': op.truediv, + '//': op.floordiv, + '>': op.gt, + '<': op.lt, + '>=': op.ge, + '<=': op.le, + '=': op.eq, + 'abs': abs, + 'append': lambda *args: list(chain(*args)), + '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, + 'display': lambda x: print(lispstr(x)), + 'eq?': op.is_, + 'equal?': op.eq, + 'filter': lambda *args: list(filter(*args)), + '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, (int, float)), + 'procedure?': callable, + 'round': round, + 'symbol?': lambda x: isinstance(x, Symbol), + }) + return env + + ################ Interaction: A REPL -def repl(prompt: str = 'lis.py> ') -> None: +def repl(prompt: str = 'lis.py> ') -> NoReturn: "A prompt-read-eval-print loop." global_env = standard_env() while True: - val = evaluate(parse(input(prompt)), global_env) + ast = parse(input(prompt)) + val = evaluate(ast, global_env) if val is not None: print(lispstr(val)) - def lispstr(exp: object) -> str: "Convert a Python object back into a Lisp-readable string." if isinstance(exp, list): @@ -128,30 +132,81 @@ def lispstr(exp: object) -> str: return str(exp) -################ eval +################ Evaluator -def evaluate(x: Expression, env: Environment) -> Any: +def evaluate(exp: Expression, env: Environment) -> Any: "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 + if isinstance(exp, Symbol): # variable reference + return env[exp] + elif not isinstance(exp, list): # constant literal return exp - elif x[0] == 'if': # (if test conseq alt) - (_, test, consequence, alternative) = x + elif exp[0] == 'quote': # (quote exp) + (_, x) = exp + return x + elif exp[0] == 'if': # (if test conseq alt) + (_, test, consequence, alternative) = exp if evaluate(test, env): return evaluate(consequence, env) else: return evaluate(alternative, env) - elif x[0] == 'define': # (define name exp) - (_, name, exp) = x - env[name] = evaluate(exp, env) - elif x[0] == 'lambda': # (lambda (parm…) body) - (_, parms, *body) = x + elif exp[0] == 'lambda': # (lambda (parm…) body…) + (_, parms, *body) = exp + if not isinstance(parms, list): + raise SyntaxError(lispstr(exp)) return Procedure(parms, body, env) + elif exp[0] == 'define': + (_, name_exp, *rest) = exp + if isinstance(name_exp, Symbol): # (define name exp) + value_exp = rest[0] + env[name_exp] = evaluate(value_exp, env) + else: # (define (name parm…) body…) + name, *parms = name_exp + env[name] = Procedure(parms, rest, env) + elif exp[0] == 'set!': + (_, var, value_exp) = exp + env.change(var, evaluate(value_exp, env)) else: # (proc arg…) - proc = evaluate(x[0], env) - args = [evaluate(exp, env) for exp in x[1:]] + (func_exp, *args) = exp + proc = evaluate(func_exp, env) + args = [evaluate(arg, env) for arg in args] return proc(*args) + + +class Procedure: + "A user-defined Scheme procedure." + + def __init__( + self, parms: list[Symbol], body: list[Expression], env: Environment + ): + self.parms = parms + self.body = body + self.env = env + + def __call__(self, *args: Expression) -> Any: + local_env = dict(zip(self.parms, args)) + env = Environment(local_env, self.env) + for exp in self.body: + result = evaluate(exp, env) + return result + + +################ command-line interface + +def run(source: str) -> Any: + global_env = standard_env() + tokens = tokenize(source) + while tokens: + exp = read_from_tokens(tokens) + result = evaluate(exp, global_env) + return result + +def main(args: list[str]) -> None: + if len(args) == 1: + with open(args[0]) as fp: + run(fp.read()) + else: + repl() + +if __name__ == '__main__': + import sys + main(sys.argv[1:]) diff --git a/18-with-match/lispy/py3.9/lis_test.py b/18-with-match/lispy/py3.9/lis_test.py index 106ac24..03dc15c 100644 --- a/18-with-match/lispy/py3.9/lis_test.py +++ b/18-with-match/lispy/py3.9/lis_test.py @@ -1,8 +1,8 @@ -from typing import Any, Optional +from typing import Optional from pytest import mark, fixture -from lis import parse, evaluate, standard_env, Symbol, Environment, Expression +from lis import parse, evaluate, Expression, Environment, standard_env ############################################################# tests for parse @@ -73,10 +73,10 @@ def test_evaluate(source: str, expected: Optional[Expression]) -> None: def std_env() -> Environment: return standard_env() -# tests for each of the cases in evaluate +# tests for cases in evaluate def test_evaluate_variable() -> None: - env: Environment = dict(x=10) + env = Environment({'x': 10}) source = 'x' expected = 10 got = evaluate(parse(source), env) @@ -166,3 +166,15 @@ def test_invocation_user_procedure(std_env: Environment) -> None: """ got = evaluate(parse(source), std_env) assert got == 22 + + +def test_define_function(std_env: Environment) -> None: + source = '(define (max a b) (if (>= a b) a b))' + got = evaluate(parse(source), std_env) + 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.env is std_env + assert max_fn(1, 2) == 2 + assert max_fn(3, 2) == 3 diff --git a/18-with-match/mirror_gen.py b/18-with-match/mirror_gen.py index 6dfaf02..2758045 100644 --- a/18-with-match/mirror_gen.py +++ b/18-with-match/mirror_gen.py @@ -15,6 +15,9 @@ While active, the context manager reverses text output to YKCOWREBBAJ >>> what 'JABBERWOCKY' + >>> print('back to normal') + back to normal + # end::MIRROR_GEN_DEMO_1[] diff --git a/22-dyn-attr-prop/doc_property.py b/22-dyn-attr-prop/doc_property.py index b1ba351..5b653ef 100644 --- a/22-dyn-attr-prop/doc_property.py +++ b/22-dyn-attr-prop/doc_property.py @@ -14,7 +14,7 @@ class Foo: @property def bar(self): - '''The bar attribute''' + """The bar attribute""" return self.__dict__['bar'] @bar.setter From ade9577a55f1337ee5ca2b85d40f8eafc161d648 Mon Sep 17 00:00:00 2001 From: Luciano Ramalho Date: Mon, 20 Sep 2021 23:31:45 -0300 Subject: [PATCH 2/2] sync from Atlas --- 02-array-seq/lispy/py3.10/lis.py | 4 ++-- 02-array-seq/lispy/py3.9/lis.py | 4 ++-- 18-with-match/lispy/py3.10/examples_test.py | 1 - 18-with-match/lispy/py3.10/lis.py | 21 +++++++------------ 18-with-match/lispy/py3.9/lis.py | 4 ++-- .../primes/spinner_prime_async_nap.py | 15 ++++++------- 6 files changed, 21 insertions(+), 28 deletions(-) diff --git a/02-array-seq/lispy/py3.10/lis.py b/02-array-seq/lispy/py3.10/lis.py index cffb9a1..150fca7 100755 --- a/02-array-seq/lispy/py3.10/lis.py +++ b/02-array-seq/lispy/py3.10/lis.py @@ -122,7 +122,7 @@ def standard_env() -> Environment: # tag::REPL[] def repl(prompt: str = 'lis.py> ') -> NoReturn: "A prompt-read-eval-print loop." - global_env = standard_env() + global_env = Environment({}, standard_env()) while True: ast = parse(input(prompt)) val = evaluate(ast, global_env) @@ -200,7 +200,7 @@ class Procedure: ################ command-line interface def run(source: str) -> Any: - global_env = standard_env() + global_env = Environment({}, standard_env()) tokens = tokenize(source) while tokens: exp = read_from_tokens(tokens) diff --git a/02-array-seq/lispy/py3.9/lis.py b/02-array-seq/lispy/py3.9/lis.py index 843d4cc..201b41e 100644 --- a/02-array-seq/lispy/py3.9/lis.py +++ b/02-array-seq/lispy/py3.9/lis.py @@ -117,7 +117,7 @@ def standard_env() -> Environment: def repl(prompt: str = 'lis.py> ') -> NoReturn: "A prompt-read-eval-print loop." - global_env = standard_env() + global_env = Environment({}, standard_env()) while True: ast = parse(input(prompt)) val = evaluate(ast, global_env) @@ -190,7 +190,7 @@ class Procedure: ################ command-line interface def run(source: str) -> Any: - global_env = standard_env() + global_env = Environment({}, standard_env()) tokens = tokenize(source) while tokens: exp = read_from_tokens(tokens) diff --git a/18-with-match/lispy/py3.10/examples_test.py b/18-with-match/lispy/py3.10/examples_test.py index ba6bd10..2807596 100644 --- a/18-with-match/lispy/py3.10/examples_test.py +++ b/18-with-match/lispy/py3.10/examples_test.py @@ -48,7 +48,6 @@ Doctests for `evaluate` # end::EVAL_NUMBER[] # tag::EVAL_SYMBOL[] ->>> from lis import standard_env >>> evaluate(parse('+'), standard_env()) >>> evaluate(parse('ni!'), standard_env()) diff --git a/18-with-match/lispy/py3.10/lis.py b/18-with-match/lispy/py3.10/lis.py index 0feeadc..30ad8a8 100755 --- a/18-with-match/lispy/py3.10/lis.py +++ b/18-with-match/lispy/py3.10/lis.py @@ -121,7 +121,7 @@ def standard_env() -> Environment: # tag::REPL[] def repl(prompt: str = 'lis.py> ') -> NoReturn: "A prompt-read-eval-print loop." - global_env = standard_env() + global_env = Environment({}, standard_env()) while True: ast = parse(input(prompt)) val = evaluate(ast, global_env) @@ -140,10 +140,7 @@ def lispstr(exp: object) -> str: ################ Evaluator # tag::EVALUATE[] -KEYWORDS = {'quote', 'if', 'lambda', 'define', 'set!'} - -def is_keyword(s: Any) -> bool: - return isinstance(s, Symbol) and s in KEYWORDS +KEYWORDS = ['quote', 'if', 'lambda', 'define', 'set!'] def evaluate(exp: Expression, env: Environment) -> Any: "Evaluate an expression in an environment." @@ -161,17 +158,13 @@ def evaluate(exp: Expression, env: Environment) -> Any: return evaluate(alternative, env) case ['lambda', [*parms], *body] if body: return Procedure(parms, body, env) - case ['define', Symbol(var), value_exp]: - env[var] = evaluate(value_exp, env) + case ['define', Symbol(name), value_exp]: + env[name] = evaluate(value_exp, env) case ['define', [Symbol(name), *parms], *body] if body: env[name] = Procedure(parms, body, env) - case ['set!', Symbol(var), value_exp]: - env.change(var, evaluate(value_exp, env)) -<<<<<<< HEAD + case ['set!', Symbol(name), value_exp]: + env.change(name, evaluate(value_exp, env)) case [func_exp, *args] if func_exp not in KEYWORDS: -======= - case [func_exp, *args] if not is_keyword(func_exp): ->>>>>>> 3ecfb212c6273122797c76876d6b373b2cb94fa6 proc = evaluate(func_exp, env) values = [evaluate(arg, env) for arg in args] return proc(*values) @@ -202,7 +195,7 @@ class Procedure: ################ command-line interface def run(source: str) -> Any: - global_env = standard_env() + global_env = Environment({}, standard_env()) tokens = tokenize(source) while tokens: exp = read_from_tokens(tokens) diff --git a/18-with-match/lispy/py3.9/lis.py b/18-with-match/lispy/py3.9/lis.py index e8881cd..6ab8a19 100644 --- a/18-with-match/lispy/py3.9/lis.py +++ b/18-with-match/lispy/py3.9/lis.py @@ -117,7 +117,7 @@ def standard_env() -> Environment: def repl(prompt: str = 'lis.py> ') -> NoReturn: "A prompt-read-eval-print loop." - global_env = standard_env() + global_env = Environment({}, standard_env()) while True: ast = parse(input(prompt)) val = evaluate(ast, global_env) @@ -193,7 +193,7 @@ class Procedure: ################ command-line interface def run(source: str) -> Any: - global_env = standard_env() + global_env = Environment({}, standard_env()) tokens = tokenize(source) while tokens: exp = read_from_tokens(tokens) diff --git a/19-concurrency/primes/spinner_prime_async_nap.py b/19-concurrency/primes/spinner_prime_async_nap.py index 4d6fa47..b6e6518 100644 --- a/19-concurrency/primes/spinner_prime_async_nap.py +++ b/19-concurrency/primes/spinner_prime_async_nap.py @@ -7,6 +7,7 @@ import asyncio import itertools import math +import functools # tag::PRIME_NAP[] async def is_prime(n): @@ -21,8 +22,8 @@ async def is_prime(n): for i in range(3, root + 1, 2): if n % i == 0: return False - if i % 100_000 == 1: # <2> - await asyncio.sleep(0) + if i % 100_000 == 1: + await asyncio.sleep(0) # <1> return True # end::PRIME_NAP[] @@ -39,13 +40,13 @@ async def spin(msg: str) -> None: print(f'\r{blanks}\r', end='') async def check(n: int) -> int: - return await is_prime(n) # <4> + return await is_prime(n) async def supervisor(n: int) -> int: - spinner = asyncio.create_task(spin('thinking!')) # <1> - print('spinner object:', spinner) # <2> - result = await check(n) # <3> - spinner.cancel() # <5> + spinner = asyncio.create_task(spin('thinking!')) + print('spinner object:', spinner) + result = await check(n) + spinner.cancel() return result def main() -> None: