Created
January 7, 2022 15:38
-
-
Save realchonk/ac454366957c32786d700c88db0c9818 to your computer and use it in GitHub Desktop.
A basic calculator written in Python3.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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