Skip to content

Instantly share code, notes, and snippets.

@realchonk
Created January 7, 2022 15:38
Show Gist options
  • Save realchonk/ac454366957c32786d700c88db0c9818 to your computer and use it in GitHub Desktop.
Save realchonk/ac454366957c32786d700c88db0c9818 to your computer and use it in GitHub Desktop.
A basic calculator written in Python3.
#!/usr/bin/env python3
# Copyright (c) 2022 Benjamin Stürz <[email protected]>
#
# 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.
#
#
# This python3 script implements a basic calculator.
# These are the steps in order:
# - Read a line from stdin.
# - Tokenize the line into tokens.
# - Parse these tokens to build a parse tree.
# - Evaluate the parse tree and print the result to stdout.
from dataclasses import dataclass
from enum import Enum
# Define Token Types.
class TokenType(Enum):
INTEGER = 1
OPERATOR = 2
PARENTHESIS = 3
# Define a Token.
# A token is an atom of a language.
@dataclass
class Token:
type: TokenType
value: any
# A Parse Tree is an intermediate representation of the input.
# Threre a 4 destinct types this Parse Tree has:
# - 'leaf' An integer, eg: '10'
# - 'subexpr' A sub-expression, eg: '(...)'
# - 'unary' Unary '+' or '-' expression, eg: '+...'
# - 'binary' Binary expression with one of these operators: (+ - * /), eg: '... * ...'
class ParseTree:
def __init__(self, *args):
if len(args) == 1:
if isinstance(args[0], int):
self.type = 'leaf'
self.value = args[0]
elif isinstance(args[0], ParseTree):
self.type = 'subexpr'
self.expr = args[0]
else:
raise TypeError("Invalid type of args[0]")
elif len(args) == 2:
self.type = 'unary'
self.op = args[0]
self.child = args[1]
elif len(args) == 3:
self.type = 'binary'
self.op = args[0]
self.left = args[1]
self.right = args[2]
else:
raise RuntimeError("Invalid number of args")
# String Reader.
# It just provides peek(), next(), eof() and skip()
class Reader:
def __init__(self, data, default):
self.data = data
self.pos = 0
self.default = default
def peek(self):
if self.eof():
return self.default
else:
return self.data[self.pos]
def next(self):
if self.eof():
return self.default
else:
ch = self.data[self.pos]
self.skip()
return ch
def eof(self):
return self.pos >= len(self.data)
def skip(self):
self.pos = self.pos + 1
# Just provides a few helper functions over the standard Reader class.
class TokenReader(Reader):
def matches(self, tktype):
return self.peek().type == tktype
def matches_op(self, ops: [str]):
tk = self.peek()
return tk != None and tk.type == TokenType.OPERATOR and ops.count(tk.value) == 1
# Tokenize the input into many small tokens.
def lex(reader):
while reader.peek().isspace():
reader.skip()
ch = reader.peek()
if ch.isdigit():
# Parse integers.
val = 0
while reader.peek().isdigit():
val = val * 10 + (int(reader.next()) - int('0'))
return Token(TokenType.INTEGER, val)
elif ['+', '-', '*', '/'].count(ch) == 1:
# Parse operators.
reader.skip()
return Token(TokenType.OPERATOR, ch)
elif ['(', ')'].count(ch) == 1:
# Parse parenthesises.
reader.skip()
return Token(TokenType.PARENTHESIS, ch)
else:
return
# Parse an atom.
# Atoms in this language are:
# - integers
# - sub-expressions
def parse_atom(tksrc):
tk = tksrc.next()
if tk == None:
raise RuntimeError(f"NULL tk")
elif tk.type == TokenType.INTEGER:
return ParseTree(tk.value)
elif tk.type == TokenType.PARENTHESIS:
if tk.value != '(':
raise RuntimeError(f"Expected '(', got {tk}")
expr = parse(tksrc)
tk = tksrc.next()
if tk.type != TokenType.PARENTHESIS or tk.value != ')':
raise RuntimeError(f"Expected ')', got {tk}")
return ParseTree(expr)
else:
raise RuntimeError(f"Expected integer or '(', got {tk}")
# Parse unary expressions.
def parse_unary(tksrc):
if tksrc.matches_op(['+', '-']):
op = tksrc.next()
right = parse_unary(tksrc)
return ParseTree(op, right)
else:
return parse_atom(tksrc)
# Parse multiplicative binary expressions.
def parse_multiplication(tksrc):
left = parse_unary(tksrc)
while tksrc.matches_op(['*', '/']):
op = tksrc.next()
right = parse_unary(tksrc)
left = ParseTree(op, left, right)
return left
# Parse additive binary expressions.
def parse_addition(tksrc):
left = parse_multiplication(tksrc)
while tksrc.matches_op(['+', '-']):
op = tksrc.next()
right = parse_multiplication(tksrc)
left = ParseTree(op, left, right)
return left
# Top-level parse function.
def parse(tksrc: TokenReader):
return parse_addition(tksrc)
# Evaluata a unary expression.
def eval_unary(op, tree):
match op.value:
case '+':
return eval(tree)
case '-':
return eval(tree) * -1
case _:
raise RuntimeError(f"eval_unary(): Invalid operator {op}")
return
# Evaluate a binary expression.
def eval_binary(op, left, right):
match op.value:
case '+':
return eval(left) + eval(right)
case '-':
return eval(left) - eval(right)
case '*':
return eval(left) * eval(right)
case '/':
return eval(left) / eval(right)
case _:
raise RuntimeError(f"eval_binary(): Invalid operator {op}")
return
# Evaluate an expression.
def eval(tree: ParseTree):
match tree.type:
case 'leaf':
return tree.value
case 'subexpr':
return eval(tree.expr)
case 'unary':
return eval_unary(tree.op, tree.child)
case 'binary':
return eval_binary(tree.op, tree.left, tree.right)
case _:
raise RuntimeError(f"eval(): Invalid tree type {tree}")
# Main function.
if __name__ == "__main__":
while True:
try:
line = input("> ")
except EOFError:
break
reader = Reader(line, '')
try:
tokens = []
while not reader.eof():
tk = lex(reader)
if tk == None:
raise RuntimeError("Invalid input!")
break
tokens.append(tk)
except:
print("Invalid Input!")
continue
try:
tree = parse(TokenReader(tokens, None))
except RuntimeError as e:
print(e)
continue
except:
print("Failed to parse expression.")
continue
try:
value = eval(tree)
except RuntimeError as e:
print(e)
continue
except:
print("Failed to evaluate expression")
continue
print(value)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment