From 1ac693fdc2daf8c2ad4abb2752bdccab63a90e43 Mon Sep 17 00:00:00 2001 From: laniakea Date: Fri, 10 Apr 2026 16:39:57 +0200 Subject: [PATCH] boom --- .gitignore | 141 +++++++++++++++++++++++++++++++++++++++ Calculator.py | 36 ++++++++++ ParserError.py | 6 ++ interpeter.py | 66 +++++++++++++++++++ lexer.py | 86 ++++++++++++++++++++++++ nodes.py | 126 +++++++++++++++++++++++++++++++++++ parser_.py | 174 +++++++++++++++++++++++++++++++++++++++++++++++++ tokens.py | 26 ++++++++ values.py | 9 +++ 9 files changed, 670 insertions(+) create mode 100644 .gitignore create mode 100644 Calculator.py create mode 100644 ParserError.py create mode 100644 interpeter.py create mode 100644 lexer.py create mode 100644 nodes.py create mode 100644 parser_.py create mode 100644 tokens.py create mode 100644 values.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8491113 --- /dev/null +++ b/.gitignore @@ -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/ \ No newline at end of file diff --git a/Calculator.py b/Calculator.py new file mode 100644 index 0000000..79c7537 --- /dev/null +++ b/Calculator.py @@ -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() diff --git a/ParserError.py b/ParserError.py new file mode 100644 index 0000000..94f680c --- /dev/null +++ b/ParserError.py @@ -0,0 +1,6 @@ +import ast + +class ParserError(Exception): + + def __init__(self, *args: object) -> None: + super().__init__(*args) \ No newline at end of file diff --git a/interpeter.py b/interpeter.py new file mode 100644 index 0000000..0aeea02 --- /dev/null +++ b/interpeter.py @@ -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)) + + \ No newline at end of file diff --git a/lexer.py b/lexer.py new file mode 100644 index 0000000..e8727f7 --- /dev/null +++ b/lexer.py @@ -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)) \ No newline at end of file diff --git a/nodes.py b/nodes.py new file mode 100644 index 0000000..3977693 --- /dev/null +++ b/nodes.py @@ -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})" diff --git a/parser_.py b/parser_.py new file mode 100644 index 0000000..4d167d3 --- /dev/null +++ b/parser_.py @@ -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}") + + + + + + + + + + diff --git a/tokens.py b/tokens.py new file mode 100644 index 0000000..b870e25 --- /dev/null +++ b/tokens.py @@ -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 "") \ No newline at end of file diff --git a/values.py b/values.py new file mode 100644 index 0000000..385e993 --- /dev/null +++ b/values.py @@ -0,0 +1,9 @@ +from dataclasses import dataclass + + +@dataclass +class Number: + value: float + + def __repr__(self): + return f"{self.value}" \ No newline at end of file