This commit is contained in:
itamar 2026-04-10 16:39:57 +02:00
commit 1ac693fdc2
Signed by: itamar
SSH key fingerprint: SHA256:Dv6UzB9hN8q8FUgMR/7X3DTFpE/vSB2m05+KNnxM4B0
9 changed files with 670 additions and 0 deletions

141
.gitignore vendored Normal file
View file

@ -0,0 +1,141 @@
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
cover/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
.pybuilder/
target/
# Jupyter Notebook
.ipynb_checkpoints
# IPython
profile_default/
ipython_config.py
# pyenv
# For a library or package, you might want to ignore these files since the code is
# intended to run in multiple environments; otherwise, check them in:
# .python-version
# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies.
#Pipfile.lock
# PEP 582; used by e.g. github.com/David-OConnor/pyflow
__pypackages__/
# Celery stuff
celerybeat-schedule
celerybeat.pid
# SageMath parsed files
*.sage.py
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# Pyre type checker
.pyre/
# pytype static type analyzer
.pytype/
# Cython debug symbols
cython_debug/
# Extras
.vscode/

36
Calculator.py Normal file
View file

@ -0,0 +1,36 @@
from interpeter import Interpeter
from ParserError import ParserError
from lexer import Lexer
import traceback
from parser_ import Parser
# 1 % [2 + !3 * 8] @ 2 $ -321.123 - ~12 + 221 & 1000 ^ -1
def main():
while True:
try:
expr = input("> ")
expr = Parser.strip_str(expr)
lexer = Lexer(expr)
tokens = lexer.generate_tokens()
parser = Parser(tokens)
tree = parser.parse()
if not tree: continue
interpeter = Interpeter()
value = interpeter.visit(tree)
print(value)
except ParserError as e:
print("ParserError: ", str(e))
except Exception as e:
print(type(e), end ='')
print(" occured\n" + str(e))
traceback.print_exc()
if __name__ == '__main__':
main()

6
ParserError.py Normal file
View file

@ -0,0 +1,6 @@
import ast
class ParserError(Exception):
def __init__(self, *args: object) -> None:
super().__init__(*args)

66
interpeter.py Normal file
View file

@ -0,0 +1,66 @@
from nodes import *
from values import Number
class Interpeter:
def visit(self, node):
method_name = f'visit_{type(node).__name__}'
method = getattr(self, method_name)
return method(node)
def visit_NumberNode(self, node) -> Number:
return Number(node.value)
def visit_AddNode(self, node) -> Number:
return Number(self.visit(node.node_a).value + self.visit(node.node_b).value)
def visit_SubtractNode(self, node) -> Number:
return Number(self.visit(node.node_a).value - self.visit(node.node_b).value)
def visit_MultiplyNode(self, node) -> Number:
return Number(self.visit(node.node_a).value * self.visit(node.node_b).value)
def visit_DivideNode(self, node) -> Number:
try:
return Number(self.visit(node.node_a).value / self.visit(node.node_b).value)
except Exception:
raise Exception("Runtime Math error (division)")
def visit_PlusNode(self, node) -> Number:
return self.visit(node.node)
def visit_MinusNode(self, node) -> Number:
return Number(-self.visit(node.node).value)
def visit_FactorialNode(self, node) -> Number:
fact = lambda x: 1 if x == 0 else x * fact(x-1)
try:
return Number(fact(self.visit(node.node).value))
except Exception:
raise Exception("Runtime Math error (Factorial)")
def visit_PowerNode(self, node) -> Number:
return Number(pow(self.visit(node.node_a).value, self.visit(node.node_b).value))
def visit_ModuleNode(self, node) -> Number:
return Number(self.visit(node.node_a).value % self.visit(node.node_b).value)
def visit_AverageNode(self, node) -> Number:
return Number((self.visit(node.node_a).value + self.visit(node.node_b).value) / 2.0 )
def visit_MaximumNode(self, node) -> Number:
return Number(max(self.visit(node.node_a).value, self.visit(node.node_b).value) )
def visit_MinimumNode(self, node) -> Number:
return Number(min(self.visit(node.node_a).value, self.visit(node.node_b).value) )
def visit_NegateNode(self, node) -> Number:
if not self.visit(node.node).value.is_integer():
raise Exception("Runtime Math error (Negate)")
return Number(~int(self.visit(node.node).value))

86
lexer.py Normal file
View file

@ -0,0 +1,86 @@
from tokens import TokenType, Token
DIGITS = '0123456789'
class Lexer:
def __init__(self, text):
self.text = iter(text)
self.advance()
def advance(self):
try:
self.current_char = next(self.text)
except StopIteration:
self.current_char = None
def generate_tokens(self):
while self.current_char != None:
if self.current_char == '.' or self.current_char in DIGITS:
yield self.generate_number()
elif self.current_char == '+':
self.advance()
yield Token(TokenType.PLUS)
elif self.current_char == '-':
self.advance()
yield Token(TokenType.MINUS)
elif self.current_char == '~':
self.advance()
yield Token(TokenType.NEGATE)
elif self.current_char == '*':
self.advance()
yield Token(TokenType.MULTIPLY)
elif self.current_char == '/':
self.advance()
yield Token(TokenType.DIVIDE)
elif self.current_char == '^':
self.advance()
yield Token(TokenType.POWER)
elif self.current_char == '%':
self.advance()
yield Token(TokenType.MODULE)
elif self.current_char == '!':
self.advance()
yield Token(TokenType.FACTORIAL)
elif self.current_char == '@':
self.advance()
yield Token(TokenType.AVERAGE)
elif self.current_char == '&':
self.advance()
yield Token(TokenType.MINIMUM)
elif self.current_char == '$':
self.advance()
yield Token(TokenType.MAXIMUM)
elif self.current_char == '[':
self.advance()
yield Token(TokenType.LBRACKET)
elif self.current_char == ']':
self.advance()
yield Token(TokenType.RBRACKET)
else:
raise Exception(f"Illegal character '{self.current_char}'")
def generate_number(self):
decimal_point_count = 0
number_str = self.current_char
self.advance()
while self.current_char != None and (self.current_char == '.' or self.current_char in DIGITS):
if self.current_char == '.':
decimal_point_count += 1
if decimal_point_count > 1:
break
number_str += self.current_char
self.advance()
if number_str.startswith('.'):
number_str = '0' + number_str
if number_str.endswith('.'):
number_str += '0'
# Create the token
return Token(TokenType.NUMBER, float(number_str))

126
nodes.py Normal file
View file

@ -0,0 +1,126 @@
from dataclasses import dataclass
@dataclass
class NumberNode:
value: float
def __repr__(self):
return f"{self.value}"
@dataclass
class AddNode:
node_a: any
node_b: any
def __repr__(self):
return f"[{self.node_a}+{self.node_b}]"
@dataclass
class SubtractNode:
node_a: any
node_b: any
def __repr__(self):
return f"[{self.node_a}-{self.node_b}]"
@dataclass
class MultiplyNode:
node_a: any
node_b: any
def __repr__(self):
return f"[{self.node_a}*{self.node_b}]"
@dataclass
class DivideNode:
node_a: any
node_b: any
def __repr__(self):
return f"[{self.node_a}/{self.node_b}]"
@dataclass
class PowerNode:
node_a: any
node_b: any
def __repr__(self):
return f"[{self.node_b}^{self.node_b}]"
@dataclass
class ModuleNode:
node_a: any
node_b: any
def __repr__(self):
return f"[{self.node_b}%{self.node_b}]"
@dataclass
class AverageNode:
node_a: any
node_b: any
def __repr__(self):
return f"[{self.node_b}@{self.node_b}]"
@dataclass
class MaximumNode:
node_a: any
node_b: any
def __repr__(self):
return f"[{self.node_b}${self.node_b}]"
@dataclass
class MinimumNode:
node_a: any
node_b: any
def __repr__(self):
return f"[{self.node_b}&{self.node_b}]"
# Uniary symbols #
@dataclass
class PlusNode:
node: any
def __repr__(self):
return f"(+{self.node})"
@dataclass
class MinusNode:
node: any
def __repr__(self):
return f"(-{self.node})"
@dataclass
class FactorialNode:
node: any
def __repr__(self):
return f"(!{self.node})"
@dataclass
class NegateNode:
node: any
def __repr__(self):
return f"(~{self.node})"

174
parser_.py Normal file
View file

@ -0,0 +1,174 @@
from ParserError import ParserError
from tokens import TokenType
from nodes import *
class Parser:
def __init__(self, tokens):
self.tokens = iter(tokens)
self.advance()
def raise_error(self):
raise ParserError("Invalid syntax")
def advance(self):
try:
self.current_token = next(self.tokens)
except StopIteration:
self.current_token = None
def parse(self):
if self.current_token == None:
return None
result = self.level1_operators()
if self.current_token != None:
self.raise_error()
return result
# Takes care of (+ -)
def level1_operators(self):
result = self.level2_operators()
while self.current_token != None and self.current_token.type in (TokenType.PLUS, TokenType.MINUS):
if self.current_token.type == TokenType.PLUS:
self.advance()
result = AddNode(result, self.level2_operators())
elif self.current_token.type == TokenType.MINUS:
self.advance()
result = SubtractNode(result, self.level2_operators())
return result
def level2_operators(self):
result = self.level3_operators()
while self.current_token != None and self.current_token.type in (TokenType.MULTIPLY, TokenType.DIVIDE):
if self.current_token.type == TokenType.MULTIPLY:
self.advance()
result = MultiplyNode(result, self.level3_operators())
elif self.current_token.type == TokenType.DIVIDE:
self.advance()
result = DivideNode(result, self.level3_operators())
return result
def level3_operators(self):
result = self.level4_operators()
while self.current_token != None and self.current_token.type == TokenType.POWER:
self.advance()
result = PowerNode(result, self.level4_operators())
return result
def level4_operators(self):
result = self.level5_operators()
while self.current_token != None and self.current_token.type in (TokenType.MODULE, TokenType.FACTORIAL):
if self.current_token.type == TokenType.MODULE:
self.advance()
result = ModuleNode(result, self.level5_operators())
elif self.current_token.type == TokenType.FACTORIAL:
self.advance()
result = FactorialNode(self.level5_operators(result))
return result
def level5_operators(self, num=None):
if num != None:
result = num
else:
result = self.level6_operators()
while self.current_token != None and self.current_token.type in (TokenType.AVERAGE, TokenType.MAXIMUM, TokenType.MINIMUM):
if self.current_token.type == TokenType.AVERAGE:
self.advance()
result = AverageNode(result, self.level6_operators())
elif self.current_token.type == TokenType.MAXIMUM:
self.advance()
result = MaximumNode(result, self.level6_operators())
elif self.current_token.type == TokenType.MINIMUM:
self.advance()
result = MinimumNode(result, self.level6_operators())
return result
def level6_operators(self):
token = self.current_token
if token.type == TokenType.LBRACKET:
self.advance()
result = self.level1_operators()
if self.current_token.type != TokenType.RBRACKET:
self.raise_error()
self.advance()
return result
elif token.type == TokenType.NUMBER:
self.advance()
return NumberNode(token.value)
elif token.type == TokenType.PLUS:
self.advance()
return PlusNode(self.level6_operators())
elif token.type == TokenType.MINUS:
self.advance()
return MinusNode(self.level6_operators())
elif token.type == TokenType.NEGATE:
self.advance()
return NegateNode(self.level6_operators())
return None
@staticmethod
def strip_str(level1_operators : str):
"""removes all unnecessery characters from level1_operatorsession
Args:s
level1_operators (str): the level1_operatorsession that will be stripped
Raises:
ParserError: Error if parsing failed
Returns:
str: The level1_operatorsession without the unnecessery characters
"""
try:
temp_list = level1_operators.split()
level1_operators = ''.join(temp_list)
level1_operators = level1_operators.replace('\\n', '')
level1_operators = level1_operators.replace('\\t', '')
return level1_operators
except Exception as e:
raise ParserError("Error during parsing: {e}")

26
tokens.py Normal file
View file

@ -0,0 +1,26 @@
from enum import Enum
from dataclasses import dataclass
class TokenType(Enum):
NUMBER = 0
PLUS = 1
MINUS = 2
NEGATE = 3
MULTIPLY = 4
DIVIDE = 5
POWER = 6
MODULE = 7
FACTORIAL = 8
AVERAGE = 9
MAXIMUM = 10
MINIMUM = 11
LBRACKET = 12
RBRACKET = 13
@dataclass
class Token:
type: TokenType
value: any = None
def __repr__(self):
return self.type.name + (f"{self.value}" if self.value != None else "")

9
values.py Normal file
View file

@ -0,0 +1,9 @@
from dataclasses import dataclass
@dataclass
class Number:
value: float
def __repr__(self):
return f"{self.value}"