sync from Atlas

This commit is contained in:
Luciano Ramalho 2021-09-20 10:37:26 -03:00
parent 6527037ae7
commit 2f2f87d4fb
16 changed files with 1730 additions and 115 deletions

View File

@ -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())
<built-in function add>
>>> 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
<lis.Procedure object at 0x...>
>>> 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
<lis.Procedure object at 0x...>
>>> 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[]

219
02-array-seq/lispy/py3.10/lis.py Executable file
View File

@ -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:])

View File

@ -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

View File

@ -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)))

View File

@ -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`.

View File

@ -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())
<built-in function add>
>>> 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
<lis.Procedure object at 0x...>
>>> 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
<lis.Procedure object at 0x...>
>>> 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[]

View File

@ -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:])

View File

@ -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

View File

@ -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[]
# end::RUN_AVERAGER[]

View File

@ -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 _:

View File

@ -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)

View File

@ -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())
<built-in function add>
>>> 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
<lis.Procedure object at 0x...>
>>> 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
<lis.Procedure object at 0x...>
>>> 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[]

View File

@ -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:])

View File

@ -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

View File

@ -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[]

View File

@ -14,7 +14,7 @@ class Foo:
@property
def bar(self):
'''The bar attribute'''
"""The bar attribute"""
return self.__dict__['bar']
@bar.setter