Skip to content

Commit 4df38f2

Browse files
committed
arithmetic evaluator
1 parent 312c3f7 commit 4df38f2

File tree

5 files changed

+148
-17
lines changed

5 files changed

+148
-17
lines changed

mylis/mylis_3/environ.py

+37
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import math
2+
import operator as op
3+
4+
from .mytypes import Environment, Symbol
5+
6+
7+
def core_env() -> Environment:
8+
"An environment for an s-expression calculator."
9+
env: Environment = {}
10+
env.update(vars(math)) # sin, cos, sqrt, pi, ...
11+
env.update(
12+
{
13+
'+': op.add,
14+
'-': op.sub,
15+
'*': op.mul,
16+
'/': op.truediv,
17+
'quotient': op.floordiv,
18+
'>': op.gt,
19+
'<': op.lt,
20+
'>=': op.ge,
21+
'<=': op.le,
22+
'=': op.eq,
23+
'abs': abs,
24+
'begin': lambda *x: x[-1],
25+
'eq?': op.is_,
26+
'equal?': op.eq,
27+
'max': max,
28+
'min': min,
29+
'not': op.not_,
30+
'number?': lambda x: isinstance(x, (int, float)),
31+
'procedure?': callable,
32+
'modulo': op.mod,
33+
'round': round,
34+
'symbol?': lambda x: isinstance(x, Symbol),
35+
}
36+
)
37+
return env

mylis/mylis_3/evaluator.py

+18
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
from typing import Any
2+
from .mytypes import Environment, Expression, Symbol
3+
4+
5+
def evaluate(x: Expression, env: Environment) -> Any:
6+
"Evaluate an expression in an environment."
7+
if isinstance(x, Symbol): # variable reference
8+
return env[x]
9+
elif not isinstance(x, list): # constant literal
10+
return x
11+
elif x[0] == 'define': # (define var exp)
12+
_, var, exp = x
13+
env[var] = evaluate(exp, env)
14+
else: # (proc arg...)
15+
proc_exp, *args = x
16+
proc = evaluate(proc_exp, env)
17+
arg_values = [evaluate(exp, env) for exp in args]
18+
return proc(*arg_values)

mylis/mylis_3/evaluator_test.py

+49
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
from typing import Any
2+
3+
from .environ import core_env
4+
from .evaluator import evaluate
5+
from .parser import parse
6+
from .mytypes import Environment
7+
8+
from pytest import fixture, mark
9+
10+
@fixture
11+
def std_env() -> Environment:
12+
return core_env()
13+
14+
15+
def test_evaluate_variable() -> None:
16+
env: Environment = dict(x=10)
17+
source = 'x'
18+
expected = 10
19+
got = evaluate(parse(source), env)
20+
assert got == expected
21+
22+
23+
def test_evaluate_literal(std_env: Environment) -> None:
24+
source = '3.3'
25+
expected = 3.3
26+
got = evaluate(parse(source), std_env)
27+
assert got == expected
28+
29+
30+
@mark.parametrize( 'source, expected', [
31+
('(* 11111 11111)', 123454321),
32+
('(* (+ 4 3) 6)', 42),
33+
('(sin (/ pi 2))', 1)
34+
])
35+
def test_evaluate_call(
36+
std_env: Environment,
37+
source: str,
38+
expected: Any,
39+
) -> None:
40+
got = evaluate(parse(source), std_env)
41+
assert got == expected
42+
43+
# Special forms
44+
45+
def test_define(std_env: Environment) -> None:
46+
source = '(define answer (* 6 7))'
47+
got = evaluate(parse(source), std_env)
48+
assert got is None
49+
assert std_env['answer'] == 42

mylis/mylis_3/parser.py

+19-9
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,11 @@
55
## See http://norvig.com/lispy.html
66
## Refactorting and additions by Luciano Ramalho (2022)
77

8-
from .lis_types import (
8+
from .mytypes import (
99
Atom,
10+
ParserException,
1011
Symbol,
1112
Expression,
12-
BlankExpression,
1313
BraceNeverClosed,
1414
UnexpectedCloseBrace,
1515
)
@@ -40,7 +40,7 @@ def read_from_tokens(tokens: list[str]) -> Expression:
4040
try:
4141
token = tokens.pop(0)
4242
except IndexError:
43-
raise BlankExpression()
43+
raise ParserException('Empty list of tokens')
4444
if token in BRACES:
4545
exp = []
4646
while tokens and tokens[0] != BRACES[token]:
@@ -66,9 +66,19 @@ def parse_atom(token: str) -> Atom:
6666
return Symbol(token)
6767

6868

69-
def lispstr(exp: object) -> str:
70-
"""Convert a Python object back into a Lisp-readable string."""
71-
if isinstance(exp, list):
72-
return '(' + ' '.join(map(lispstr, exp)) + ')'
73-
else:
74-
return str(exp)
69+
# s_expr is the inverse function of parse, but some formatting
70+
# is lost, and all braces are rendered as ()
71+
def s_expr(obj: object) -> str:
72+
""" Convert Python object back to s-expression code. """
73+
match obj:
74+
case True:
75+
return '#t'
76+
case False:
77+
return '#f'
78+
case list(obj):
79+
items = ' '.join(s_expr(x) for x in obj)
80+
return f'({items})'
81+
case Symbol(x):
82+
return x
83+
case _:
84+
return repr(obj)

mylis/mylis_3/parser_test.py

+25-8
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
1-
from .lis_types import (
2-
BlankExpression,
1+
from email.parser import Parser
2+
from .mytypes import (
33
Expression,
44
ParserException,
55
UnexpectedCloseBrace,
66
BraceNeverClosed,
77
)
8-
from .parser import parse
8+
from .parser import parse, s_expr
99

1010
from pytest import mark, raises
1111

@@ -65,19 +65,36 @@ def test_parse_mixed_braces(source: str, expected: Expression) -> None:
6565

6666

6767
@mark.parametrize(
68-
'source, expected, brace',
68+
'source, expected, match',
6969
[
70-
('', BlankExpression, ''),
70+
('', ParserException, 'Empty'),
7171
('{', BraceNeverClosed, '{'),
7272
('([]', BraceNeverClosed, '('),
7373
('(])', UnexpectedCloseBrace, ']'),
7474
('([)', UnexpectedCloseBrace, ')'),
7575
],
7676
)
7777
def test_parse_malformed(
78-
source: str, expected: ParserException, brace: str
78+
source: str, expected: ParserException, match: str
7979
) -> None:
8080
with raises(expected) as excinfo: # type: ignore
8181
parse(source)
82-
if brace:
83-
assert repr(brace) in str(excinfo.value)
82+
assert match in str(excinfo.value)
83+
84+
85+
@mark.parametrize('obj, expected', [
86+
(0, '0'),
87+
(1, '1'),
88+
(False, '#f'),
89+
(True, '#t'),
90+
(1.5, '1.5'),
91+
('sin', 'sin'),
92+
(['+', 1, 2], '(+ 1 2)'),
93+
(['if', ['<', 'a', 'b'], True, False], '(if (< a b) #t #f)'),
94+
([], '()'),
95+
(None, 'None'),
96+
(..., 'Ellipsis'),
97+
])
98+
def test_s_expr(obj: object, expected: str) -> None:
99+
got = s_expr(obj)
100+
assert got == expected

0 commit comments

Comments
 (0)