From 4ec737154c3470e0252080ab3be4951815fe86a4 Mon Sep 17 00:00:00 2001 From: Pavle Portic Date: Thu, 18 Apr 2019 15:07:48 +0200 Subject: [PATCH] Initial commit --- .gitignore | 124 +++++++++++++++++++++++++++++++++++++++++++++++++ constants.py | 13 ++++++ interpreter.py | 65 ++++++++++++++++++++++++++ lexer.py | 90 +++++++++++++++++++++++++++++++++++ main.py | 40 ++++++++++++++++ token.py | 8 ++++ 6 files changed, 340 insertions(+) create mode 100644 .gitignore create mode 100644 constants.py create mode 100644 interpreter.py create mode 100644 lexer.py create mode 100644 main.py create mode 100644 token.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..dedd352 --- /dev/null +++ b/.gitignore @@ -0,0 +1,124 @@ +# 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/ +pip-wheel-metadata/ +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 +.hypothesis/ +.pytest_cache/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +.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 + +# celery beat schedule file +celerybeat-schedule + +# 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/ + diff --git a/constants.py b/constants.py new file mode 100644 index 0000000..e3d2585 --- /dev/null +++ b/constants.py @@ -0,0 +1,13 @@ +NUMBER = 'NUMBER' +EOF = 'EOF' + +PLUS = 'PLUS' +MINUS = 'MINUS' +MUL = 'MUL' +DIV = 'DIV' +POW = 'POW' + +HEX = 'HEX' +DEC = 'DEC' +BIN = 'BIN' + diff --git a/interpreter.py b/interpreter.py new file mode 100644 index 0000000..ddfa155 --- /dev/null +++ b/interpreter.py @@ -0,0 +1,65 @@ +from constants import ( + NUMBER, EOF, + PLUS, MINUS, MUL, DIV, POW, + HEX, DEC, BIN, +) + + +class Interpreter: + def __init__(self): + self.stack = [] + self.mode = DEC + self.mode_func = float + + def init_lexer(self, lexer): + self.lexer = lexer + self.current_token = self.lexer.get_next_token() + + def eat(self, type): + if self.current_token.type == type: + self.current_token = self.lexer.get_next_token() + else: + raise Exception('Wrong token eaten') + + def get_from_stack(self, pop=True): + if pop: + value = self.stack.pop() + else: + value = self.stack[-1] + + if type(value) is str: + return self.ariables[value] + + return value + + def evaluate_operator(self, token): + if len(self.stack) < 2: + raise Exception('Needs at least 2 values on the stack') + + right = self.get_from_stack() + left = self.get_from_stack() + self.stack.append(token.value(left, right)) + self.eat(token.type) + + def set_mode(self, token): + self.mode = token.type + self.mode_func = token.value + + def parse(self, lexer): + self.init_lexer(lexer) + token = self.current_token + + while token.type is not EOF: + if token.type in (PLUS, MINUS, MUL, DIV, POW): + self.evaluate_operator(token) + elif token.type == NUMBER: + self.stack.append(token.value) + self.eat(NUMBER) + elif token.type in (HEX, DEC, BIN): + self.set_mode(token) + self.eat(token.type) + + token = self.current_token + + return self.stack + diff --git a/lexer.py b/lexer.py new file mode 100644 index 0000000..811129f --- /dev/null +++ b/lexer.py @@ -0,0 +1,90 @@ +import operator + +from token import Token +from constants import ( + NUMBER, EOF, + PLUS, MINUS, MUL, DIV, POW, + HEX, DEC, BIN, +) + + +class Lexer(): + def __init__(self, text): + self.text = text + self.pos = 0 + self.current_char = self.text[self.pos] + + def error(self, value=None): + if value is None: + raise Exception(f'Unexpected character {self.current_char}') + else: + raise Exception(f'Unexpected token {value}') + + def advance(self): + self.pos += 1 + + if self.pos > len(self.text) - 1: + self.current_char = None + else: + self.current_char = self.text[self.pos] + + def number(self): + number = '' + while self.current_char is not None and (self.current_char.isdigit() or self.current_char == '.'): + number += self.current_char + self.advance() + + return float(number) + + def skip_whitespace(self): + while(self.current_char is not None and self.current_char.isspace()): + self.advance() + + def get_next_token(self): + while self.current_char is not None: + if self.current_char.isspace(): + self.skip_whitespace() + continue + + elif self.current_char.isdigit(): + return Token(NUMBER, self.number()) + + elif self.current_char == '+': + self.advance() + return Token(PLUS, operator.add) + + elif self.current_char == '-': + self.advance() + return Token(MINUS, operator.sub) + + elif self.current_char == '*': + self.advance() + return Token(MUL, operator.mul) + + elif self.current_char == '/': + self.advance() + return Token(DIV, operator.truediv) + + elif self.current_char == '^': + self.advance() + return Token(POW, operator.pow) + + elif self.current_char and self.current_char.isalpha(): + text = '' + while self.current_char and (self.current_char.isalpha() or self.current_char.isdigit() or self.current_char == '_'): + text += self.current_char + self.advance() + + if text.lower() == 'hex': + return Token(HEX, hex) + elif text.lower() == 'dec': + return Token(DEC, float) + elif text.lower() == 'bin': + return Token(BIN, bin) + else: + self.error(text) + + self.error() + + return Token(EOF, None) + diff --git a/main.py b/main.py new file mode 100644 index 0000000..b753538 --- /dev/null +++ b/main.py @@ -0,0 +1,40 @@ +from lexer import Lexer +from interpreter import Interpreter +from constants import DEC + + +def print_stack(interpreter, stack): + for item in stack: + if interpreter.mode == DEC and item.is_integer(): + print(int(item)) + else: + print(interpreter.mode_func(item)) + + +def main(): + interpreter = Interpreter() + + while True: + try: + text = input('> ') + except (EOFError, KeyboardInterrupt): + break + + if not text: + continue + + if text == 'exit': + break + + lexer = Lexer(text) + try: + stack = interpreter.parse(lexer) + except Exception as exception: + print(exception) + + print_stack(interpreter, stack) + + +if __name__ == "__main__": + main() + diff --git a/token.py b/token.py new file mode 100644 index 0000000..44f068f --- /dev/null +++ b/token.py @@ -0,0 +1,8 @@ +class Token(): + def __init__(self, type, value): + self.type = type + self.value = value + + def __repr__(self): + return "<{} {}>".format(self.type, self.value) +