moved lispy from 02 to 18

This commit is contained in:
Luciano Ramalho
2021-09-15 22:48:08 -03:00
parent 00e4741926
commit 6527037ae7
38 changed files with 590 additions and 1219 deletions

View File

@@ -1,109 +0,0 @@
"""
Doctests for `parse`:
>>> from lis import parse
# tag::PARSE_DEMO[]
>>> parse('1.5') # <1>
1.5
>>> parse('set!') # <2>
'set!'
>>> parse('(gcd 18 44)') # <3>
['gcd', 18, 44]
>>> parse('(- m (* n (// m n)))') # <4>
['-', 'm', ['*', 'n', ['//', 'm', 'n']]]
# end::PARSE_DEMO[]
"""
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))))
(gcd 18 45)
"""
def test_gcd():
got = run(gcd_src)
assert got == 9
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_newton():
got = run(closure_src)
assert got == 100

View File

@@ -1,64 +0,0 @@
import operator as op
from lis import run
env_scm = """
(define standard-env (list
(list (quote +) +)
(list (quote -) -)
))
standard-env
"""
def test_env_build():
got = run(env_scm)
assert got == [['+', op.add], ['-', op.sub]]
scan_scm = """
(define l (quote (a b c)))
(define (scan what where)
(if (null? where)
()
(if (eq? what (car where))
what
(scan what (cdr where))))
)
"""
def test_scan():
source = scan_scm + '(scan (quote a) l )'
got = run(source)
assert got == 'a'
def test_scan_not_found():
source = scan_scm + '(scan (quote z) l )'
got = run(source)
assert got == []
lookup_scm = """
(define env (list
(list (quote +) +)
(list (quote -) -)
))
(define (lookup what where)
(if (null? where)
()
(if (eq? what (car (car where)))
(car (cdr (car where)))
(lookup what (cdr where))))
)
"""
def test_lookup():
source = lookup_scm + '(lookup (quote +) env)'
got = run(source)
assert got == op.add
def test_lookup_not_found():
source = lookup_scm + '(lookup (quote z) env )'
got = run(source)
assert got == []

View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2010-2017 Peter Norvig
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -0,0 +1,36 @@
# Norvig's originals and updates
This directory contains:
* `original/`:
Norvig's [`lis.py`](https://github.com/norvig/pytudes/blob/c33cd6835a506a57d9fe73e3a8317d49babb13e8/py/lis.py),
[`lispy.py`](https://github.com/norvig/pytudes/blob/c33cd6835a506a57d9fe73e3a8317d49babb13e8/py/lispy.py), and the `lispytest.py` custom test script for testing both;
* `py3.10/`: `lis.py` with type hints, pattern matching, and minor edits—requires Python 3.10.
The `py3.10/` directory also has `lis_test.py` to run with
[pytest](https://docs.pytest.org), including the
[`lis_tests` suite](https://github.com/norvig/pytudes/blob/60168bce8cdfacf57c92a5b2979f0b2e95367753/py/lispytest.py#L5)
from `original/lispytest.py`,
and additional separate tests for each expression and special form handled by `evaluate`.
## Provenance, Copyright and License
`lis.py` is
[published](https://github.com/norvig/pytudes/blob/c33cd6835a506a57d9fe73e3a8317d49babb13e8/py/lis.py)
in the [norvig/pytudes](https://github.com/norvig/pytudes) repository on Github.
The copyright holder is Peter Norvig and the code is licensed under the
[MIT license](https://github.com/norvig/pytudes/blob/60168bce8cdfacf57c92a5b2979f0b2e95367753/LICENSE).
## Changes to Norvig's code
I made small changes to the programs in `original/`:
* In `lis.py`:
* The `Procedure` class accepts a list of expressions as the `body`, and `__call__` evaluates those expressions in order, and returns the value of the last. This is consistent with Scheme's `lambda` syntax and provided a useful example for pattern matching.
* In the `elif` block for `'lambda'`, I added the `*` in front of the `*body` variable in the tuple unpacking to capture the expressions as a list, before calling the `Procedure` constructor.
* In `lispy.py` I made [changes and a pull request](https://github.com/norvig/pytudes/pull/106) to make it run on Python 3.
_Luciano Ramalho<br/>June 29, 2021_

View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2010-2017 Peter Norvig
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -0,0 +1,8 @@
# Source of the originals
* [lis.py](https://raw.githubusercontent.com/norvig/pytudes/705c0a335c1811a203e79587d7d41865cf7f41c7/py/lis.py)
* [lispy.py](https://raw.githubusercontent.com/norvig/pytudes/705c0a335c1811a203e79587d7d41865cf7f41c7/py/lispy.py)
* [lispytest.py](https://raw.githubusercontent.com/norvig/pytudes/705c0a335c1811a203e79587d7d41865cf7f41c7/py/lispytest.py)

View File

@@ -0,0 +1,132 @@
################ Lispy: Scheme Interpreter in Python 3.3+
## (c) Peter Norvig, 2010-18; See http://norvig.com/lispy.html
################ Imports and Types
import math
import operator as op
from collections import ChainMap as Environment
Symbol = str # A Lisp Symbol is implemented as a Python str
List = list # A Lisp List is implemented as a Python list
Number = (int, float) # A Lisp Number is implemented as a Python int or float
class Procedure(object):
"A user-defined Scheme procedure."
def __init__(self, parms, body, env):
self.parms, self.body, self.env = parms, body, env
def __call__(self, *args):
env = Environment(dict(zip(self.parms, args)), self.env)
return eval(self.body, env)
################ Global Environment
def standard_env():
"An environment with some Scheme standard procedures."
env = {}
env.update(vars(math)) # sin, cos, sqrt, pi, ...
env.update({
'+':op.add, '-':op.sub, '*':op.mul, '/':op.truediv,
'>':op.gt, '<':op.lt, '>=':op.ge, '<=':op.le, '=':op.eq,
'abs': abs,
'append': op.add,
'apply': lambda proc, args: proc(*args),
'begin': lambda *x: x[-1],
'car': lambda x: x[0],
'cdr': lambda x: x[1:],
'cons': lambda x,y: [x] + y,
'eq?': op.is_,
'equal?': op.eq,
'length': len,
'list': lambda *x: list(x),
'list?': lambda x: isinstance(x,list),
'map': lambda *args: list(map(*args)),
'max': max,
'min': min,
'not': op.not_,
'null?': lambda x: x == [],
'number?': lambda x: isinstance(x, Number),
'procedure?': callable,
'round': round,
'symbol?': lambda x: isinstance(x, Symbol),
})
return env
global_env = standard_env()
################ Parsing: parse, tokenize, and read_from_tokens
def parse(program):
"Read a Scheme expression from a string."
return read_from_tokens(tokenize(program))
def tokenize(s):
"Convert a string into a list of tokens."
return s.replace('(',' ( ').replace(')',' ) ').split()
def read_from_tokens(tokens):
"Read an expression from a sequence of tokens."
if len(tokens) == 0:
raise SyntaxError('unexpected EOF while reading')
token = tokens.pop(0)
if '(' == token:
L = []
while tokens[0] != ')':
L.append(read_from_tokens(tokens))
tokens.pop(0) # pop off ')'
return L
elif ')' == token:
raise SyntaxError('unexpected )')
else:
return atom(token)
def atom(token):
"Numbers become numbers; every other token is a symbol."
try: return int(token)
except ValueError:
try: return float(token)
except ValueError:
return Symbol(token)
################ Interaction: A REPL
def repl(prompt='lis.py> '):
"A prompt-read-eval-print loop."
while True:
val = eval(parse(input(prompt)))
if val is not None:
print(lispstr(val))
def lispstr(exp):
"Convert a Python object back into a Lisp-readable string."
if isinstance(exp, List):
return '(' + ' '.join(map(lispstr, exp)) + ')'
else:
return str(exp)
################ eval
def eval(x, env=global_env):
"Evaluate an expression in an environment."
if isinstance(x, Symbol): # variable reference
return env[x]
elif not isinstance(x, List): # constant literal
return x
elif x[0] == 'quote': # (quote exp)
(_, exp) = x
return exp
elif x[0] == 'if': # (if test conseq alt)
(_, test, conseq, alt) = x
exp = (conseq if eval(test, env) else alt)
return eval(exp, env)
elif x[0] == 'define': # (define var exp)
(_, var, exp) = x
env[var] = eval(exp, env)
elif x[0] == 'lambda': # (lambda (var...) body)
(_, parms, body) = x
return Procedure(parms, body, env)
else: # (proc arg...)
proc = eval(x[0], env)
args = [eval(exp, env) for exp in x[1:]]
return proc(*args)

View File

@@ -0,0 +1,316 @@
################ Scheme Interpreter in Python
## (c) Peter Norvig, 2010; See http://norvig.com/lispy2.html
################ Symbol, Procedure, classes
import re, sys, io
class Symbol(str): pass
def Sym(s, symbol_table={}):
"Find or create unique Symbol entry for str s in symbol table."
if s not in symbol_table: symbol_table[s] = Symbol(s)
return symbol_table[s]
_quote, _if, _set, _define, _lambda, _begin, _definemacro, = map(Sym,
"quote if set! define lambda begin define-macro".split())
_quasiquote, _unquote, _unquotesplicing = map(Sym,
"quasiquote unquote unquote-splicing".split())
class Procedure:
"A user-defined Scheme procedure."
def __init__(self, parms, exp, env):
self.parms, self.exp, self.env = parms, exp, env
def __call__(self, *args):
return eval(self.exp, Env(self.parms, args, self.env))
################ parse, read, and user interaction
def parse(inport):
"Parse a program: read and expand/error-check it."
# Backwards compatibility: given a str, convert it to an InPort
if isinstance(inport, str): inport = InPort(io.StringIO(inport))
return expand(read(inport), toplevel=True)
eof_object = Symbol('#<eof-object>') # Note: uninterned; can't be read
class InPort:
"An input port. Retains a line of chars."
tokenizer = r"""\s*(,@|[('`,)]|"(?:[\\].|[^\\"])*"|;.*|[^\s('"`,;)]*)(.*)"""
def __init__(self, file):
self.file = file; self.line = ''
def next_token(self):
"Return the next token, reading new text into line buffer if needed."
while True:
if self.line == '': self.line = self.file.readline()
if self.line == '': return eof_object
token, self.line = re.match(InPort.tokenizer, self.line).groups()
if token != '' and not token.startswith(';'):
return token
def readchar(inport):
"Read the next character from an input port."
if inport.line != '':
ch, inport.line = inport.line[0], inport.line[1:]
return ch
else:
return inport.file.read(1) or eof_object
def read(inport):
"Read a Scheme expression from an input port."
def read_ahead(token):
if '(' == token:
L = []
while True:
token = inport.next_token()
if token == ')': return L
else: L.append(read_ahead(token))
elif ')' == token: raise SyntaxError('unexpected )')
elif token in quotes: return [quotes[token], read(inport)]
elif token is eof_object: raise SyntaxError('unexpected EOF in list')
else: return atom(token)
# body of read:
token1 = inport.next_token()
return eof_object if token1 is eof_object else read_ahead(token1)
quotes = {"'":_quote, "`":_quasiquote, ",":_unquote, ",@":_unquotesplicing}
def atom(token):
'Numbers become numbers; #t and #f are booleans; "..." string; otherwise Symbol.'
if token == '#t': return True
elif token == '#f': return False
elif token[0] == '"': return token[1:-1]
try: return int(token)
except ValueError:
try: return float(token)
except ValueError:
try: return complex(token.replace('i', 'j', 1))
except ValueError:
return Sym(token)
def to_string(x):
"Convert a Python object back into a Lisp-readable string."
if x is True: return "#t"
elif x is False: return "#f"
elif isa(x, Symbol): return x
elif isa(x, str): return repr(x)
elif isa(x, list): return '('+' '.join(map(to_string, x))+')'
elif isa(x, complex): return str(x).replace('j', 'i')
else: return str(x)
def load(filename):
"Eval every expression from a file."
repl(None, InPort(open(filename)), None)
def repl(prompt='lispy> ', inport=InPort(sys.stdin), out=sys.stdout):
"A prompt-read-eval-print loop."
sys.stderr.write("Lispy version 2.0\n")
while True:
try:
if prompt: sys.stderr.write(prompt)
x = parse(inport)
if x is eof_object: return
val = eval(x)
if val is not None and out: print(to_string(val), file=out)
except Exception as e:
print('%s: %s' % (type(e).__name__, e))
################ Environment class
class Env(dict):
"An environment: a dict of {'var':val} pairs, with an outer Env."
def __init__(self, parms=(), args=(), outer=None):
# Bind parm list to corresponding args, or single parm to list of args
self.outer = outer
if isa(parms, Symbol):
self.update({parms:list(args)})
else:
if len(args) != len(parms):
raise TypeError('expected %s, given %s, '
% (to_string(parms), to_string(args)))
self.update(zip(parms,args))
def find(self, var):
"Find the innermost Env where var appears."
if var in self: return self
elif self.outer is None: raise LookupError(var)
else: return self.outer.find(var)
def is_pair(x): return x != [] and isa(x, list)
def cons(x, y): return [x]+y
def callcc(proc):
"Call proc with current continuation; escape only"
ball = RuntimeWarning("Sorry, can't continue this continuation any longer.")
def throw(retval): ball.retval = retval; raise ball
try:
return proc(throw)
except RuntimeWarning as w:
if w is ball: return ball.retval
else: raise w
def add_globals(self):
"Add some Scheme standard procedures."
import math, cmath, operator as op
self.update(vars(math))
self.update(vars(cmath))
self.update({
'+':op.add, '-':op.sub, '*':op.mul, '/':op.truediv, 'not':op.not_,
'>':op.gt, '<':op.lt, '>=':op.ge, '<=':op.le, '=':op.eq,
'equal?':op.eq, 'eq?':op.is_, 'length':len, 'cons':cons,
'car':lambda x:x[0], 'cdr':lambda x:x[1:], 'append':op.add,
'list':lambda *x:list(x), 'list?': lambda x:isa(x,list),
'null?':lambda x:x==[], 'symbol?':lambda x: isa(x, Symbol),
'boolean?':lambda x: isa(x, bool), 'pair?':is_pair,
'port?': lambda x:isa(x,file), 'apply':lambda proc,l: proc(*l),
'eval':lambda x: eval(expand(x)), 'load':lambda fn: load(fn), 'call/cc':callcc,
'open-input-file':open,'close-input-port':lambda p: p.file.close(),
'open-output-file':lambda f:open(f,'w'), 'close-output-port':lambda p: p.close(),
'eof-object?':lambda x:x is eof_object, 'read-char':readchar,
'read':read, 'write':lambda x,port=sys.stdout:port.write(to_string(x)),
'display':lambda x,port=sys.stdout:port.write(x if isa(x,str) else to_string(x))})
return self
isa = isinstance
global_env = add_globals(Env())
################ eval (tail recursive)
def eval(x, env=global_env):
"Evaluate an expression in an environment."
while True:
if isa(x, Symbol): # variable reference
return env.find(x)[x]
elif not isa(x, list): # constant literal
return x
elif x[0] is _quote: # (quote exp)
(_, exp) = x
return exp
elif x[0] is _if: # (if test conseq alt)
(_, test, conseq, alt) = x
x = (conseq if eval(test, env) else alt)
elif x[0] is _set: # (set! var exp)
(_, var, exp) = x
env.find(var)[var] = eval(exp, env)
return None
elif x[0] is _define: # (define var exp)
(_, var, exp) = x
env[var] = eval(exp, env)
return None
elif x[0] is _lambda: # (lambda (var*) exp)
(_, vars, exp) = x
return Procedure(vars, exp, env)
elif x[0] is _begin: # (begin exp+)
for exp in x[1:-1]:
eval(exp, env)
x = x[-1]
else: # (proc exp*)
exps = [eval(exp, env) for exp in x]
proc = exps.pop(0)
if isa(proc, Procedure):
x = proc.exp
env = Env(proc.parms, exps, proc.env)
else:
return proc(*exps)
################ expand
def expand(x, toplevel=False):
"Walk tree of x, making optimizations/fixes, and signaling SyntaxError."
require(x, x!=[]) # () => Error
if not isa(x, list): # constant => unchanged
return x
elif x[0] is _quote: # (quote exp)
require(x, len(x)==2)
return x
elif x[0] is _if:
if len(x)==3: x = x + [None] # (if t c) => (if t c None)
require(x, len(x)==4)
return list(map(expand, x))
elif x[0] is _set:
require(x, len(x)==3);
var = x[1] # (set! non-var exp) => Error
require(x, isa(var, Symbol), "can set! only a symbol")
return [_set, var, expand(x[2])]
elif x[0] is _define or x[0] is _definemacro:
require(x, len(x)>=3)
_def, v, body = x[0], x[1], x[2:]
if isa(v, list) and v: # (define (f args) body)
f, args = v[0], v[1:] # => (define f (lambda (args) body))
return expand([_def, f, [_lambda, args]+body])
else:
require(x, len(x)==3) # (define non-var/list exp) => Error
require(x, isa(v, Symbol), "can define only a symbol")
exp = expand(x[2])
if _def is _definemacro:
require(x, toplevel, "define-macro only allowed at top level")
proc = eval(exp)
require(x, callable(proc), "macro must be a procedure")
macro_table[v] = proc # (define-macro v proc)
return None # => None; add v:proc to macro_table
return [_define, v, exp]
elif x[0] is _begin:
if len(x)==1: return None # (begin) => None
else: return [expand(xi, toplevel) for xi in x]
elif x[0] is _lambda: # (lambda (x) e1 e2)
require(x, len(x)>=3) # => (lambda (x) (begin e1 e2))
vars, body = x[1], x[2:]
require(x, (isa(vars, list) and all(isa(v, Symbol) for v in vars))
or isa(vars, Symbol), "illegal lambda argument list")
exp = body[0] if len(body) == 1 else [_begin] + body
return [_lambda, vars, expand(exp)]
elif x[0] is _quasiquote: # `x => expand_quasiquote(x)
require(x, len(x)==2)
return expand_quasiquote(x[1])
elif isa(x[0], Symbol) and x[0] in macro_table:
return expand(macro_table[x[0]](*x[1:]), toplevel) # (m arg...)
else: # => macroexpand if m isa macro
return list(map(expand, x)) # (f arg...) => expand each
def require(x, predicate, msg="wrong length"):
"Signal a syntax error if predicate is false."
if not predicate: raise SyntaxError(to_string(x)+': '+msg)
_append, _cons, _let = map(Sym, "append cons let".split())
def expand_quasiquote(x):
"""Expand `x => 'x; `,x => x; `(,@x y) => (append x y) """
if not is_pair(x):
return [_quote, x]
require(x, x[0] is not _unquotesplicing, "can't splice here")
if x[0] is _unquote:
require(x, len(x)==2)
return x[1]
elif is_pair(x[0]) and x[0][0] is _unquotesplicing:
require(x[0], len(x[0])==2)
return [_append, x[0][1], expand_quasiquote(x[1:])]
else:
return [_cons, expand_quasiquote(x[0]), expand_quasiquote(x[1:])]
def let(*args):
args = list(args)
x = cons(_let, args)
require(x, len(args)>1)
bindings, body = args[0], args[1:]
require(x, all(isa(b, list) and len(b)==2 and isa(b[0], Symbol)
for b in bindings), "illegal binding list")
vars, vals = zip(*bindings)
return [[_lambda, list(vars)]+list(map(expand, body))] + list(map(expand, vals))
macro_table = {_let:let} ## More macros can go here
eval(parse("""(begin
(define-macro and (lambda args
(if (null? args) #t
(if (= (length args) 1) (car args)
`(if ,(car args) (and ,@(cdr args)) #f)))))
;; More macros can also go here
)"""))
if __name__ == '__main__':
repl()

View File

@@ -0,0 +1,122 @@
from __future__ import print_function
################ Tests for lis.py and lispy.py
lis_tests = [
("(quote (testing 1 (2.0) -3.14e159))", ['testing', 1, [2.0], -3.14e159]),
("(+ 2 2)", 4),
("(+ (* 2 100) (* 1 10))", 210),
("(if (> 6 5) (+ 1 1) (+ 2 2))", 2),
("(if (< 6 5) (+ 1 1) (+ 2 2))", 4),
("(define x 3)", None), ("x", 3), ("(+ x x)", 6),
("((lambda (x) (+ x x)) 5)", 10),
("(define twice (lambda (x) (* 2 x)))", None), ("(twice 5)", 10),
("(define compose (lambda (f g) (lambda (x) (f (g x)))))", None),
("((compose list twice) 5)", [10]),
("(define repeat (lambda (f) (compose f f)))", None),
("((repeat twice) 5)", 20), ("((repeat (repeat twice)) 5)", 80),
("(define fact (lambda (n) (if (<= n 1) 1 (* n (fact (- n 1))))))", None),
("(fact 3)", 6),
("(fact 50)", 30414093201713378043612608166064768844377641568960512000000000000),
("(define abs (lambda (n) ((if (> n 0) + -) 0 n)))", None),
("(list (abs -3) (abs 0) (abs 3))", [3, 0, 3]),
("""(define combine (lambda (f)
(lambda (x y)
(if (null? x) (quote ())
(f (list (car x) (car y))
((combine f) (cdr x) (cdr y)))))))""", None),
("(define zip (combine cons))", None),
("(zip (list 1 2 3 4) (list 5 6 7 8))", [[1, 5], [2, 6], [3, 7], [4, 8]]),
("""(define riff-shuffle (lambda (deck) (begin
(define take (lambda (n seq) (if (<= n 0) (quote ()) (cons (car seq) (take (- n 1) (cdr seq))))))
(define drop (lambda (n seq) (if (<= n 0) seq (drop (- n 1) (cdr seq)))))
(define mid (lambda (seq) (/ (length seq) 2)))
((combine append) (take (mid deck) deck) (drop (mid deck) deck)))))""", None),
("(riff-shuffle (list 1 2 3 4 5 6 7 8))", [1, 5, 2, 6, 3, 7, 4, 8]),
("((repeat riff-shuffle) (list 1 2 3 4 5 6 7 8))", [1, 3, 5, 7, 2, 4, 6, 8]),
("(riff-shuffle (riff-shuffle (riff-shuffle (list 1 2 3 4 5 6 7 8))))", [1,2,3,4,5,6,7,8]),
]
lispy_tests = [
("()", SyntaxError), ("(set! x)", SyntaxError),
("(define 3 4)", SyntaxError),
("(quote 1 2)", SyntaxError), ("(if 1 2 3 4)", SyntaxError),
("(lambda 3 3)", SyntaxError), ("(lambda (x))", SyntaxError),
("""(if (= 1 2) (define-macro a 'a)
(define-macro a 'b))""", SyntaxError),
("(define (twice x) (* 2 x))", None), ("(twice 2)", 4),
("(twice 2 2)", TypeError),
("(define lyst (lambda items items))", None),
("(lyst 1 2 3 (+ 2 2))", [1,2,3,4]),
("(if 1 2)", 2),
("(if (= 3 4) 2)", None),
("(begin (define x 1) (set! x (+ x 1)) (+ x 1))", 3),
("(define ((account bal) amt) (set! bal (+ bal amt)) bal)", None),
("(define a1 (account 100))", None),
("(a1 0)", 100), ("(a1 10)", 110), ("(a1 10)", 120),
("""(define (newton guess function derivative epsilon)
(define guess2 (- guess (/ (function guess) (derivative guess))))
(if (< (abs (- guess guess2)) epsilon) guess2
(newton guess2 function derivative epsilon)))""", None),
("""(define (square-root a)
(newton 1 (lambda (x) (- (* x x) a)) (lambda (x) (* 2 x)) 1e-8))""", None),
("(> (square-root 200.) 14.14213)", True),
("(< (square-root 200.) 14.14215)", True),
("(= (square-root 200.) (sqrt 200.))", True),
("""(define (sum-squares-range start end)
(define (sumsq-acc start end acc)
(if (> start end) acc (sumsq-acc (+ start 1) end (+ (* start start) acc))))
(sumsq-acc start end 0))""", None),
("(sum-squares-range 1 3000)", 9004500500), ## Tests tail recursion
("(call/cc (lambda (throw) (+ 5 (* 10 (throw 1))))) ;; throw", 1),
("(call/cc (lambda (throw) (+ 5 (* 10 1)))) ;; do not throw", 15),
("""(call/cc (lambda (throw)
(+ 5 (* 10 (call/cc (lambda (escape) (* 100 (escape 3)))))))) ; 1 level""", 35),
("""(call/cc (lambda (throw)
(+ 5 (* 10 (call/cc (lambda (escape) (* 100 (throw 3)))))))) ; 2 levels""", 3),
("""(call/cc (lambda (throw)
(+ 5 (* 10 (call/cc (lambda (escape) (* 100 1))))))) ; 0 levels""", 1005),
("(* 1i 1i)", -1), ("(sqrt -1)", 1j),
("(let ((a 1) (b 2)) (+ a b))", 3),
("(let ((a 1) (b 2 3)) (+ a b))", SyntaxError),
("(and 1 2 3)", 3), ("(and (> 2 1) 2 3)", 3), ("(and)", True),
("(and (> 2 1) (> 2 3))", False),
("(define-macro unless (lambda args `(if (not ,(car args)) (begin ,@(cdr args))))) ; test `", None),
("(unless (= 2 (+ 1 1)) (display 2) 3 4)", None),
(r'(unless (= 4 (+ 1 1)) (display 2) (display "\n") 3 4)', 4),
("(quote x)", 'x'),
("(quote (1 2 three))", [1, 2, 'three']),
("'x", 'x'),
("'(one 2 3)", ['one', 2, 3]),
("(define L (list 1 2 3))", None),
("`(testing ,@L testing)", ['testing',1,2,3,'testing']),
("`(testing ,L testing)", ['testing',[1,2,3],'testing']),
("`,@L", SyntaxError),
("""'(1 ;test comments '
;skip this line
2 ; more ; comments ; ) )
3) ; final comment""", [1,2,3]),
]
def test(tests, name=''):
"For each (exp, expected) test case, see if eval(parse(exp)) == expected."
fails = 0
for (x, expected) in tests:
try:
result = eval(parse(x))
print(x, '=>', lispstr(result))
ok = (result == expected)
except Exception as e:
print(x, '=raises=>', type(e).__name__, e)
ok = isinstance(expected, type) and issubclass(expected, Exception) and isinstance(e, expected)
if not ok:
fails += 1
print('FAIL!!! Expected', expected)
print('%s %s: %d out of %d tests fail.' % ('*'*45, name, fails, len(tests)))
if __name__ == '__main__':
from lis import *
test(lis_tests, 'lis.py')
from lispy import *
test(lis_tests+lispy_tests, 'lispy.py')

View File

@@ -0,0 +1,259 @@
"""
Doctests for `parse`
--------------------
# tag::PARSE_ATOM[]
>>> 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('''
... (define double
... (lambda (n)
... (* n 2)))
... ''')
['define', 'double', ['lambda', ['n'], ['*', 'n', 2]]]
# end::PARSE_LIST[]
Doctest for `Environment`
-------------------------
# tag::ENVIRONMENT[]
>>> from lis import Environment
>>> outer_env = {'a': 0, 'b': 1}
>>> inner_env = {'a': 2}
>>> 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)'), {})
'no-such-name'
>>> evaluate(parse('(quote (99 bottles of beer))'), {})
[99, 'bottles', 'of', 'beer']
>>> evaluate(parse('(quote (/ 10 0))'), {})
['/', 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))))
(gcd 18 45)
"""
def test_gcd():
got = run(gcd_src)
assert got == 9
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_newton():
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))
(counter)
(counter)
(counter)
"""
def test_closure_with_change():
got = run(closure_with_change_src)
assert got == 3
# 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,47 +1,82 @@
#!/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
################ Imports and Types
# tag::IMPORTS[]
import math
import operator as op
from collections import ChainMap
from collections.abc import MutableMapping, Iterator
from itertools import chain
from typing import Any, TypeAlias
from typing import Any, TypeAlias, NoReturn
Symbol: TypeAlias = str
Atom: TypeAlias = float | int | Symbol
Expression: TypeAlias = Atom | list
Environment: TypeAlias = MutableMapping[Symbol, object]
# end::IMPORTS[]
class Procedure:
"A user-defined Scheme procedure."
################ Parsing: parse, tokenize, and read_from_tokens
def __init__(self, parms: list[Symbol], body: list[Expression], env: Environment):
self.parms = parms
self.body = body
self.env = env
def parse(program: str) -> Expression:
"Read a Scheme expression from a string."
return read_from_tokens(tokenize(program))
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
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)
################ global environment
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):
"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 = Environment()
env.update(vars(math)) # sin, cos, sqrt, pi, ...
env.update({
'+': op.add,
@@ -61,6 +96,7 @@ def standard_env() -> Environment:
'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)),
@@ -80,70 +116,32 @@ def standard_env() -> Environment:
return env
################ parse, tokenize, and read_from_tokens
################ Interaction: A REPL
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)
################ interaction: a REPL
def repl(prompt: str = 'lis.py> ') -> None:
"A prompt-read-evaluate-print loop."
global_env: Environment = standard_env()
# tag::REPL[]
def repl() -> NoReturn:
"A prompt-read-eval-print loop."
global_env = standard_env()
while True:
val = evaluate(parse(input(prompt)), global_env)
ast = parse(input('lis.py> '))
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[]
################ eval
################ Evaluator
# tag::EVALUATE[]
KEYWORDS = ['quote', 'if', 'lambda', 'define', 'set!']
def evaluate(exp: Expression, env: Environment) -> Any:
"Evaluate an expression in an environment."
match exp:
@@ -153,40 +151,66 @@ def evaluate(exp: Expression, env: Environment) -> Any:
return env[var]
case []:
return []
case ['quote', exp]:
return exp
case ['quote', x]:
return x
case ['if', test, consequence, alternative]:
if evaluate(test, env):
return evaluate(consequence, env)
else:
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), *parms], *body]:
case ['define', [Symbol(name), *parms], *body] if body:
env[name] = Procedure(parms, body, env)
case ['lambda', [*parms], *body]:
return Procedure(parms, body, env)
case [op, *args]:
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)
values = [evaluate(arg, env) for arg in args]
return proc(*values)
case _:
raise SyntaxError(repr(exp))
raise SyntaxError(lispstr(exp))
# end::EVALUATE[]
# tag::PROCEDURE[]
class Procedure:
"A user-defined Scheme procedure."
################ non-interactive execution
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[]
def run_lines(source: str) -> Iterator[Any]:
global_env: Environment = standard_env()
################ command-line interface
def run(source: str) -> Any:
global_env = standard_env()
tokens = tokenize(source)
while tokens:
exp = read_from_tokens(tokens)
yield evaluate(exp, global_env)
def run(source: str) -> Any:
for result in run_lines(source):
pass
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,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,157 @@
################ 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
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
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)
################ Interaction: A REPL
def repl(prompt: str = 'lis.py> ') -> None:
"A prompt-read-eval-print loop."
global_env = standard_env()
while True:
val = evaluate(parse(input(prompt)), 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)
################ eval
def evaluate(x: 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
return exp
elif x[0] == 'if': # (if test conseq alt)
(_, test, consequence, alternative) = x
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
return Procedure(parms, body, env)
else: # (proc arg…)
proc = evaluate(x[0], env)
args = [evaluate(exp, env) for exp in x[1:]]
return proc(*args)

View File

@@ -0,0 +1,168 @@
from typing import Any, Optional
from pytest import mark, fixture
from lis import parse, evaluate, standard_env, Symbol, Environment, Expression
############################################################# 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 each of the cases in evaluate
def test_evaluate_variable() -> None:
env: Environment = dict(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

View File

@@ -11,11 +11,11 @@ While active, the context manager reverses text output to
... print('Alice, Kitty and Snowdrop') # <2>
... print(what)
...
pordwonS dna yttiK ,ecilA # <3>
pordwonS dna yttiK ,ecilA
YKCOWREBBAJ
>>> what # <4>
>>> what # <3>
'JABBERWOCKY'
>>> print('Back to normal.') # <5>
>>> print('Back to normal.') # <4>
Back to normal.
# end::MIRROR_DEMO_1[]
@@ -27,15 +27,15 @@ This exposes the context manager operation::
>>> from mirror import LookingGlass
>>> manager = LookingGlass() # <1>
>>> manager
<mirror.LookingGlass object at 0x2a578ac>
>>> manager # doctest: +ELLIPSIS
<mirror.LookingGlass object at 0x...>
>>> monster = manager.__enter__() # <2>
>>> monster == 'JABBERWOCKY' # <3>
eurT
>>> monster
'YKCOWREBBAJ'
>>> manager
>ca875a2x0 ta tcejbo ssalGgnikooL.rorrim<
>>> manager # doctest: +ELLIPSIS
>... ta tcejbo ssalGgnikooL.rorrim<
>>> manager.__exit__(None, None, None) # <4>
>>> monster
'JABBERWOCKY'
@@ -69,10 +69,11 @@ The context manager can handle and "swallow" exceptions.
# tag::MIRROR_EX[]
import sys
class LookingGlass:
def __enter__(self): # <1>
import sys
self.original_write = sys.stdout.write # <2>
sys.stdout.write = self.reverse_write # <3>
return 'JABBERWOCKY' # <4>
@@ -81,12 +82,9 @@ class LookingGlass:
self.original_write(text[::-1])
def __exit__(self, exc_type, exc_value, traceback): # <6>
import sys # <7>
sys.stdout.write = self.original_write # <8>
if exc_type is ZeroDivisionError: # <9>
sys.stdout.write = self.original_write # <7>
if exc_type is ZeroDivisionError: # <8>
print('Please DO NOT divide by zero!')
return True # <10>
# <11>
return True # <9>
# <10>
# end::MIRROR_EX[]

View File

@@ -35,22 +35,36 @@ This exposes the context manager operation::
>>> manager # doctest: +ELLIPSIS
>...x0 ta tcejbo reganaMtxetnoCrotareneG_.biltxetnoc<
>>> manager.__exit__(None, None, None) # <4>
False
>>> monster
'JABBERWOCKY'
# end::MIRROR_GEN_DEMO_2[]
The decorated generator also works as a decorator:
# tag::MIRROR_GEN_DECO[]
>>> @looking_glass()
... def verse():
... print('The time has come')
...
>>> verse() # <1>
emoc sah emit ehT
>>> print('back to normal') # <2>
back to normal
# end::MIRROR_GEN_DECO[]
"""
# tag::MIRROR_GEN_EX[]
import contextlib
import sys
@contextlib.contextmanager # <1>
def looking_glass():
import sys
original_write = sys.stdout.write # <2>
def reverse_write(text): # <3>
@@ -59,6 +73,4 @@ def looking_glass():
sys.stdout.write = reverse_write # <4>
yield 'JABBERWOCKY' # <5>
sys.stdout.write = original_write # <6>
# end::MIRROR_GEN_EX[]

View File

@@ -35,6 +35,7 @@ This exposes the context manager operation::
>>> manager # doctest: +ELLIPSIS
>...x0 ta tcejbo reganaMtxetnoCrotareneG_.biltxetnoc<
>>> manager.__exit__(None, None, None) # <4>
False
>>> monster
'JABBERWOCKY'
@@ -48,7 +49,7 @@ is handled by the context manager):
# tag::MIRROR_GEN_DEMO_3[]
>>> from mirror_gen import looking_glass
>>> from mirror_gen_exc import looking_glass
>>> with looking_glass():
... print('Humpty Dumpty')
... x = 1/0 # <1>
@@ -74,13 +75,11 @@ is handled by the context manager):
# tag::MIRROR_GEN_EXC[]
import contextlib
import sys
@contextlib.contextmanager
def looking_glass():
import sys
original_write = sys.stdout.write
def reverse_write(text):
@@ -96,6 +95,4 @@ def looking_glass():
sys.stdout.write = original_write # <3>
if msg:
print(msg) # <4>
# end::MIRROR_GEN_EXC[]