updated from Atlas

This commit is contained in:
Luciano Ramalho 2021-09-18 13:18:17 -03:00
parent 6527037ae7
commit 3ecfb212c6
6 changed files with 438 additions and 114 deletions

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:
@ -119,11 +120,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))
@ -140,7 +141,10 @@ def lispstr(exp: object) -> str:
################ Evaluator
# tag::EVALUATE[]
KEYWORDS = ['quote', 'if', 'lambda', 'define', 'set!']
KEYWORDS = {'quote', 'if', 'lambda', 'define', 'set!'}
def is_keyword(s: Any) -> bool:
return isinstance(s, Symbol) and s in KEYWORDS
def evaluate(exp: Expression, env: Environment) -> Any:
"Evaluate an expression in an environment."
@ -149,8 +153,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 +168,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 not is_keyword(func_exp):
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