-
Notifications
You must be signed in to change notification settings - Fork 11
/
llcalc.py
158 lines (133 loc) · 4.39 KB
/
llcalc.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
"""
An LL parser for the CIS 211 calculator.
Michal Young, spring 2018, revised winter 2019
LL / recursive descent parsing is one of approach to
parsing syntax of programming languages, including
arithmetic expressions. I'll describe this more
in a separate document
"""
from lex import TokenStream, TokenCat
import expr
import io
from typing import TextIO
import logging
logging.basicConfig()
log = logging.getLogger(__name__)
log.setLevel(logging.INFO)
class InputError(Exception):
"""Raised when we can't parse the input"""
pass
def parse(srcfile: TextIO) -> expr.Expr:
"""Interface function to LL parser"""
stream = TokenStream(srcfile)
return _program(stream)
#
# The grammar comes here. We will have a similar parser
# with a richer grammar for a programming langauge we will
# compile in our last project. This grammar just parses
# one expression or assignment.
#
# program ::= stmt
# stmt ::= exp ['=' exp]
# exp ::= term { ('+'|'-') term }
# term ::= primary { ('*'|'/') primary }
# primary ::= IDENT | CONST | '(' exp ')'
#
def require(stream: TokenStream, category: TokenCat, desc: str = "", consume=False):
"""Requires the next token in the stream to match a specified category.
Consumes and discards it if consume==True.
"""
if stream.peek().kind != category:
raise InputError(f"Expecting {desc or category}, but saw {stream.peek()} instead")
if consume:
stream.take()
return
def _program(stream: TokenStream) -> expr.Expr:
"""
program ::= stmt
We are parsing just a single statement for the
calculator. Later we will parse a sequence of
statements in programs.
"""
log.debug(f"Parsing program from token {stream.peek()}")
return _stmt(stream)
def _stmt(stream: TokenStream) -> expr.Expr:
"""
stmt ::= exp ['=' exp]
"""
left = _expr(stream)
if stream.peek().kind is TokenCat.ASSIGN:
if not isinstance(left, expr.Var):
raise InputError("Can only assign to a variable")
stream.take()
right = _expr(stream)
return expr.Assign(left, right)
else:
return left
def _expr(stream: TokenStream) -> expr.Expr:
"""
expr ::= term { ('+'|'-') term }
"""
log.debug(f"parsing sum starting from token {stream.peek()}")
left = _term(stream)
log.debug(f"sum begins with {left}")
while stream.peek().value in ["+", "-"]:
op = stream.take()
log.debug(f"expr addition op {op}")
right = _term(stream)
if op.value == "+":
left = expr.Plus(left, right)
elif op.value == "-":
left = expr.Minus(left, right)
else:
raise InputError(f"What's that op? {op}")
return left
def _term(stream: TokenStream) -> expr.Expr:
"""term ::= primary { ('*'|'/') primary }"""
left = _primary(stream)
log.debug(f"term starts with {left}")
while stream.peek().value in ["*", "/"]:
op = stream.take()
right = _primary(stream)
if op.value == "*":
left = expr.Times(left, right)
elif op.value == "/":
left = expr.Div(left, right)
else:
raise InputError(f"Expecting multiplicative op, got {op}")
return left
def _primary(stream: TokenStream) -> expr.Expr:
"""Constants, Variables, and parenthesized expressions"""
log.debug(f"Parsing primary with starting token {stream.peek()}")
token = stream.take()
if token.kind is TokenCat.INT:
log.debug(f"Returning Const node from token {token}")
return expr.IntConst(int(token.value))
elif token.kind is TokenCat.VAR:
log.debug(f"Variable {token.value}")
return expr.Var(token.value)
elif token.kind is TokenCat.LPAREN:
nested = _expr(stream)
require(stream, TokenCat.RPAREN, consume=True)
return nested
else:
raise InputError(f"Confused about {token} in expression")
###
# Calculator
###
def calc(text: str):
"""Parse and execute a single line"""
try:
exp = parse(io.StringIO(text))
print(f"{exp} => {exp.eval()}")
except Exception as e:
print(f"Oops! {e}")
def llcalc():
"""Interactive calculator interface."""
txt = input("Expression (return to quit):")
while len(txt.strip()) > 0:
calc(txt)
txt = input("Expression (return to quit):")
print("Bye! Thanks for the math!")
if __name__ == "__main__":
llcalc()