sync with O'Reilly Atlas
This commit is contained in:
parent
e986e3bdc0
commit
f0f160844d
109
02-array-seq/lispy/examples_test.py
Normal file
109
02-array-seq/lispy/examples_test.py
Normal file
@ -0,0 +1,109 @@
|
||||
"""
|
||||
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
|
@ -1,15 +1,16 @@
|
||||
################ Lispy: Scheme Interpreter in Python 3.9
|
||||
################ 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
|
||||
|
||||
import math
|
||||
import operator as op
|
||||
from collections import ChainMap
|
||||
from collections.abc import MutableMapping
|
||||
from collections.abc import MutableMapping, Iterator
|
||||
from itertools import chain
|
||||
from typing import Any, TypeAlias
|
||||
|
||||
Symbol: TypeAlias = str
|
||||
@ -23,7 +24,9 @@ class Procedure:
|
||||
"A user-defined Scheme procedure."
|
||||
|
||||
def __init__(self, parms: list[Symbol], body: Expression, env: Environment):
|
||||
self.parms, self.body, self.env = parms, body, env
|
||||
self.parms = parms
|
||||
self.body = body
|
||||
self.env = env
|
||||
|
||||
def __call__(self, *args: Expression) -> Any:
|
||||
local_env = dict(zip(self.parms, args))
|
||||
@ -31,26 +34,26 @@ class Procedure:
|
||||
return evaluate(self.body, env)
|
||||
|
||||
|
||||
################ Global Environment
|
||||
################ 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(
|
||||
{
|
||||
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': op.add,
|
||||
'append': lambda *args: list(chain(*args)),
|
||||
'apply': lambda proc, args: proc(*args),
|
||||
'begin': lambda *x: x[-1],
|
||||
'car': lambda x: x[0],
|
||||
@ -58,6 +61,7 @@ def standard_env() -> Environment:
|
||||
'cons': lambda x, y: [x] + y,
|
||||
'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),
|
||||
@ -70,12 +74,11 @@ def standard_env() -> Environment:
|
||||
'procedure?': callable,
|
||||
'round': round,
|
||||
'symbol?': lambda x: isinstance(x, Symbol),
|
||||
}
|
||||
)
|
||||
})
|
||||
return env
|
||||
|
||||
|
||||
################ Parsing: parse, tokenize, and read_from_tokens
|
||||
################ parse, tokenize, and read_from_tokens
|
||||
|
||||
|
||||
def parse(program: str) -> Expression:
|
||||
@ -94,11 +97,11 @@ def read_from_tokens(tokens: list[str]) -> Expression:
|
||||
raise SyntaxError('unexpected EOF while reading')
|
||||
token = tokens.pop(0)
|
||||
if '(' == token:
|
||||
L = []
|
||||
exp = []
|
||||
while tokens[0] != ')':
|
||||
L.append(read_from_tokens(tokens))
|
||||
tokens.pop(0) # pop off ')'
|
||||
return L
|
||||
exp.append(read_from_tokens(tokens))
|
||||
tokens.pop(0) # discard ')'
|
||||
return exp
|
||||
elif ')' == token:
|
||||
raise SyntaxError('unexpected )')
|
||||
else:
|
||||
@ -116,7 +119,7 @@ def parse_atom(token: str) -> Atom:
|
||||
return Symbol(token)
|
||||
|
||||
|
||||
################ Interaction: A REPL
|
||||
################ interaction: a REPL
|
||||
|
||||
|
||||
def repl(prompt: str = 'lis.py> ') -> None:
|
||||
@ -138,29 +141,50 @@ def lispstr(exp: object) -> str:
|
||||
|
||||
################ eval
|
||||
|
||||
|
||||
def evaluate(x: Expression, env: Environment) -> Any:
|
||||
# tag::EVALUATE[]
|
||||
def evaluate(exp: Expression, env: Environment) -> Any:
|
||||
"Evaluate an expression in an environment."
|
||||
match x:
|
||||
case Symbol(var): # variable reference
|
||||
match exp:
|
||||
case int(x) | float(x):
|
||||
return x
|
||||
case Symbol(var):
|
||||
return env[var]
|
||||
case literal if not isinstance(x, list): # constant literal
|
||||
return literal
|
||||
case ['quote', exp]: # (quote exp)
|
||||
case []:
|
||||
return []
|
||||
case ['quote', exp]:
|
||||
return exp
|
||||
case ['if', test, conseq, alt]: # (if test conseq alt)
|
||||
case ['if', test, consequence, alternative]:
|
||||
if evaluate(test, env):
|
||||
exp = conseq
|
||||
return evaluate(consequence, env)
|
||||
else:
|
||||
exp = alt
|
||||
return evaluate(exp, env)
|
||||
case ['lambda', parms, body]: # (lambda (parm...) body)
|
||||
return Procedure(parms, body, env)
|
||||
case ['define', Symbol(var), exp]: # (define var exp)
|
||||
env[var] = evaluate(exp, env)
|
||||
case ['define', [name, *parms], body]: # (define (fun parm...) body)
|
||||
return evaluate(alternative, env)
|
||||
case ['define', Symbol(var), value_exp]:
|
||||
env[var] = evaluate(value_exp, env)
|
||||
case ['define', [Symbol(name), *parms], body]:
|
||||
env[name] = Procedure(parms, body, env)
|
||||
case [op, *args]: # (proc arg...)
|
||||
case ['lambda', [*parms], body]:
|
||||
return Procedure(parms, body, env)
|
||||
case [op, *args]:
|
||||
proc = evaluate(op, env)
|
||||
values = (evaluate(arg, env) for arg in args)
|
||||
values = [evaluate(arg, env) for arg in args]
|
||||
return proc(*values)
|
||||
case _:
|
||||
raise SyntaxError(repr(exp))
|
||||
# end::EVALUATE[]
|
||||
|
||||
|
||||
################ non-interactive execution
|
||||
|
||||
|
||||
def run_lines(source: str) -> Iterator[Any]:
|
||||
global_env: Environment = 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
|
||||
return result
|
@ -4,6 +4,23 @@ 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()
|
64
02-array-seq/lispy/meta_test.py
Normal file
64
02-array-seq/lispy/meta_test.py
Normal file
@ -0,0 +1,64 @@
|
||||
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 == []
|
||||
|
32
02-array-seq/match_lat_lon.py
Normal file
32
02-array-seq/match_lat_lon.py
Normal file
@ -0,0 +1,32 @@
|
||||
"""
|
||||
metro_lat_long.py
|
||||
|
||||
Demonstration of nested tuple unpacking::
|
||||
|
||||
>>> main()
|
||||
| latitude | longitude
|
||||
Mexico City | 19.4333 | -99.1333
|
||||
New York-Newark | 40.8086 | -74.0204
|
||||
Sao Paulo | -23.5478 | -46.6358
|
||||
|
||||
"""
|
||||
|
||||
# tag::MAIN[]
|
||||
metro_areas = [
|
||||
('Tokyo', 'JP', 36.933, (35.689722, 139.691667)),
|
||||
('Delhi NCR', 'IN', 21.935, (28.613889, 77.208889)),
|
||||
('Mexico City', 'MX', 20.142, (19.433333, -99.133333)),
|
||||
('New York-Newark', 'US', 20.104, (40.808611, -74.020386)),
|
||||
('Sao Paulo', 'BR', 19.649, (-23.547778, -46.635833)),
|
||||
]
|
||||
|
||||
def main():
|
||||
print(f'{"":15} | {"latitude":>9} | {"longitude":>9}')
|
||||
for record in metro_areas:
|
||||
match record:
|
||||
case [name, _, _, (lat, lon)] if lon <= 0:
|
||||
print(f'{name:15} | {lat:9.4f} | {lon:9.4f}')
|
||||
# end::MAIN[]
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
@ -4,7 +4,7 @@ metro_lat_long.py
|
||||
Demonstration of nested tuple unpacking::
|
||||
|
||||
>>> main()
|
||||
| lat. | long.
|
||||
| latitude | longitude
|
||||
Mexico City | 19.4333 | -99.1333
|
||||
New York-Newark | 40.8086 | -74.0204
|
||||
Sao Paulo | -23.5478 | -46.6358
|
||||
@ -12,7 +12,7 @@ Demonstration of nested tuple unpacking::
|
||||
"""
|
||||
|
||||
metro_areas = [
|
||||
('Tokyo', 'JP', 36.933, (35.689722, 139.691667)), # <1>
|
||||
('Tokyo', 'JP', 36.933, (35.689722, 139.691667)), # <1>
|
||||
('Delhi NCR', 'IN', 21.935, (28.613889, 77.208889)),
|
||||
('Mexico City', 'MX', 20.142, (19.433333, -99.133333)),
|
||||
('New York-Newark', 'US', 20.104, (40.808611, -74.020386)),
|
||||
@ -20,10 +20,10 @@ metro_areas = [
|
||||
]
|
||||
|
||||
def main():
|
||||
print(f'{"":15} | {"lat.":^9} | {"long.":^9}')
|
||||
for name, cc, pop, (latitude, longitude) in metro_areas: # <2>
|
||||
if longitude <= 0: # <3>
|
||||
print(f'{name:15} | {latitude:9.4f} | {longitude:9.4f}')
|
||||
print(f'{"":15} | {"latitude":>9} | {"longitude":>9}')
|
||||
for name, _, _, (lat, lon) in metro_areas: # <2>
|
||||
if lon <= 0: # <3>
|
||||
print(f'{name:15} | {lat:9.4f} | {lon:9.4f}')
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
@ -51,7 +51,7 @@ def shave_marks_latin(txt):
|
||||
if unicodedata.combining(c) and latin_base: # <2>
|
||||
continue # ignore diacritic on Latin base char
|
||||
preserve.append(c) # <3>
|
||||
# if it isn't combining char, it's a new base char
|
||||
# if it isn't a combining char, it's a new base char
|
||||
if not unicodedata.combining(c): # <4>
|
||||
latin_base = c in string.ascii_letters
|
||||
shaved = ''.join(preserve)
|
||||
|
@ -18,8 +18,7 @@ for line in zwg_sample.strip().split('\n'):
|
||||
code, descr, version = (s.strip() for s in line.split('|'))
|
||||
chars = [chr(int(c, 16)) for c in code.split()]
|
||||
print(''.join(chars), version, descr, sep='\t', end='')
|
||||
while chars:
|
||||
char = chars.pop(0)
|
||||
for char in chars:
|
||||
if char in markers:
|
||||
print(' + ' + markers[char], end='')
|
||||
else:
|
||||
|
@ -9,8 +9,8 @@
|
||||
# tag::COORDINATE[]
|
||||
class Coordinate:
|
||||
|
||||
def __init__(self, lat, long):
|
||||
def __init__(self, lat, lon):
|
||||
self.lat = lat
|
||||
self.long = long
|
||||
self.lon = lon
|
||||
|
||||
# end::COORDINATE[]
|
||||
# end::COORDINATE[]
|
||||
|
@ -14,10 +14,10 @@ from dataclasses import dataclass
|
||||
@dataclass(frozen=True)
|
||||
class Coordinate:
|
||||
lat: float
|
||||
long: float
|
||||
lon: float
|
||||
|
||||
def __str__(self):
|
||||
ns = 'N' if self.lat >= 0 else 'S'
|
||||
we = 'E' if self.long >= 0 else 'W'
|
||||
return f'{abs(self.lat):.1f}°{ns}, {abs(self.long):.1f}°{we}'
|
||||
we = 'E' if self.lon >= 0 else 'W'
|
||||
return f'{abs(self.lat):.1f}°{ns}, {abs(self.lon):.1f}°{we}'
|
||||
# end::COORDINATE[]
|
||||
|
@ -13,10 +13,10 @@ from typing import NamedTuple
|
||||
class Coordinate(NamedTuple):
|
||||
|
||||
lat: float
|
||||
long: float
|
||||
lon: float
|
||||
|
||||
def __str__(self):
|
||||
ns = 'N' if self.lat >= 0 else 'S'
|
||||
we = 'E' if self.long >= 0 else 'W'
|
||||
return f'{abs(self.lat):.1f}°{ns}, {abs(self.long):.1f}°{we}'
|
||||
# end::COORDINATE[]
|
||||
we = 'E' if self.lon >= 0 else 'W'
|
||||
return f'{abs(self.lat):.1f}°{ns}, {abs(self.lon):.1f}°{we}'
|
||||
# end::COORDINATE[]
|
||||
|
@ -5,7 +5,7 @@ This version has a field with a default value::
|
||||
|
||||
>>> moscow = Coordinate(55.756, 37.617)
|
||||
>>> moscow
|
||||
Coordinate(lat=55.756, long=37.617, reference='WGS84')
|
||||
Coordinate(lat=55.756, lon=37.617, reference='WGS84')
|
||||
|
||||
"""
|
||||
|
||||
@ -15,6 +15,6 @@ from typing import NamedTuple
|
||||
class Coordinate(NamedTuple):
|
||||
|
||||
lat: float # <1>
|
||||
long: float
|
||||
lon: float
|
||||
reference: str = 'WGS84' # <2>
|
||||
# end::COORDINATE[]
|
||||
# end::COORDINATE[]
|
||||
|
@ -3,7 +3,7 @@ import typing
|
||||
class Coordinate(typing.NamedTuple):
|
||||
|
||||
lat: float
|
||||
long: float
|
||||
lon: float
|
||||
|
||||
trash = Coordinate('foo', None) # <1>
|
||||
print(trash)
|
||||
|
@ -9,12 +9,8 @@ def clock(func):
|
||||
result = func(*args, **kwargs)
|
||||
elapsed = time.perf_counter() - t0
|
||||
name = func.__name__
|
||||
arg_lst = []
|
||||
if args:
|
||||
arg_lst.append(', '.join(repr(arg) for arg in args))
|
||||
if kwargs:
|
||||
pairs = [f'{k}={v!r}' for k, v in kwargs.items()]
|
||||
arg_lst.append(', '.join(pairs))
|
||||
arg_lst = [repr(arg) for arg in args]
|
||||
arg_lst.extend(f'{k}={v!r}' for k, v in kwargs.items())
|
||||
arg_str = ', '.join(arg_lst)
|
||||
print(f'[{elapsed:0.8f}s] {name}({arg_str}) -> {result!r}')
|
||||
return result
|
||||
|
@ -8,7 +8,7 @@ from clockdeco import clock
|
||||
def fibonacci(n):
|
||||
if n < 2:
|
||||
return n
|
||||
return fibonacci(n-2) + fibonacci(n-1)
|
||||
return fibonacci(n - 2) + fibonacci(n - 1)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
@ -8,7 +8,7 @@ htmlize(): generic function example
|
||||
>>> htmlize(abs)
|
||||
'<pre><built-in function abs></pre>'
|
||||
>>> htmlize('Heimlich & Co.\n- a game') # <2>
|
||||
'<p>Heimlich & Co.<br>\n- a game</p>'
|
||||
'<p>Heimlich & Co.<br/>\n- a game</p>'
|
||||
>>> htmlize(42) # <3>
|
||||
'<pre>42 (0x2a)</pre>'
|
||||
>>> print(htmlize(['alpha', 66, {3, 2, 1}])) # <4>
|
||||
@ -45,7 +45,7 @@ def htmlize(obj: object) -> str:
|
||||
|
||||
@htmlize.register # <2>
|
||||
def _(text: str) -> str: # <3>
|
||||
content = html.escape(text).replace('\n', '<br>\n')
|
||||
content = html.escape(text).replace('\n', '<br/>\n')
|
||||
return f'<p>{content}</p>'
|
||||
|
||||
@htmlize.register # <4>
|
||||
|
@ -3,6 +3,6 @@ public class Confidential {
|
||||
private String secret = "";
|
||||
|
||||
public Confidential(String text) {
|
||||
secret = text.toUpperCase();
|
||||
this.secret = text.toUpperCase();
|
||||
}
|
||||
}
|
||||
|
@ -1,21 +0,0 @@
|
||||
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.
|
@ -1,28 +0,0 @@
|
||||
# lis.py
|
||||
|
||||
This directory contains 3 versions of
|
||||
[Peter Norvig's `lis.py` interpreter](https://norvig.com/lispy.html)
|
||||
for a subset of [Scheme](https://en.wikipedia.org/wiki/Scheme_(programming_language)).
|
||||
|
||||
* `original/`: Norvig's `lis.py` unchanged, `lispy.py` with
|
||||
[minor changes](https://github.com/norvig/pytudes/pull/106) to run on Python 3,
|
||||
and the `lispytest.py` custom test script;
|
||||
* `py3.9/`: `lis.py` with type hints and a few minor edits—requires Python 3.9;
|
||||
* `py3.10/`: `lis.py` with type hints, pattern matching, and minor edits—requires Python 3.10.
|
||||
|
||||
The `py3.9/` and `py3.10/` directories also have identical `lis_test.py` to run with
|
||||
[pytest](https://docs.pytest.org).
|
||||
These files include all the
|
||||
[`lis_tests` suite](https://github.com/norvig/pytudes/blob/60168bce8cdfacf57c92a5b2979f0b2e95367753/py/lispytest.py#L5)
|
||||
from `original/lispytest.py`,
|
||||
and additional separate tests for each expression and special form handled by `evaluate`.
|
||||
|
||||
## Provenance, Copyright and License
|
||||
|
||||
`lis.py` is published 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).
|
||||
|
||||
I wrote the changes and additions described above.
|
||||
|
||||
— Luciano Ramalho ([@ramalho](https://github.com/ramalho/)), 2021-06-09.
|
@ -1,132 +0,0 @@
|
||||
################ 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)
|
@ -1,316 +0,0 @@
|
||||
################ 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()
|
@ -1,122 +0,0 @@
|
||||
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')
|
||||
|
@ -1,2 +0,0 @@
|
||||
[mypy]
|
||||
python_version = 3.10
|
@ -1,163 +0,0 @@
|
||||
################ 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, mostly adding type hints.
|
||||
|
||||
################ Imports and Types
|
||||
|
||||
import math
|
||||
import operator as op
|
||||
from collections import ChainMap
|
||||
from collections.abc import MutableMapping
|
||||
from typing import Union, Any
|
||||
|
||||
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: Expression, env: Environment):
|
||||
self.parms, self.body, self.env = parms, body, env
|
||||
|
||||
def __call__(self, *args: Expression) -> Any:
|
||||
local_env = dict(zip(self.parms, args))
|
||||
env: Environment = ChainMap(local_env, self.env)
|
||||
return evaluate(self.body, env)
|
||||
|
||||
|
||||
################ 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:
|
||||
L = []
|
||||
while tokens[0] != ')':
|
||||
L.append(read_from_tokens(tokens))
|
||||
tokens.pop(0) # pop off ')'
|
||||
return L
|
||||
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()
|
||||
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, str): # 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 evaluate(test, env) else alt
|
||||
return evaluate(exp, env)
|
||||
elif x[0] == 'define': # (define var exp)
|
||||
(_, var, exp) = x
|
||||
env[var] = evaluate(exp, env)
|
||||
elif x[0] == 'lambda': # (lambda (var...) 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)
|
@ -1,151 +0,0 @@
|
||||
from typing import Optional
|
||||
|
||||
from pytest import mark, fixture
|
||||
|
||||
from lis import parse, evaluate, Expression, Environment, standard_env
|
||||
|
||||
# Norvig's tests are not isolated: they assume the
|
||||
# same environment from first to last test.
|
||||
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), 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
|
@ -1,2 +0,0 @@
|
||||
[mypy]
|
||||
python_version = 3.9
|
Loading…
x
Reference in New Issue
Block a user