commit 6add03fe4e15918abf7da493922a174ad615890c Author: Kumi Date: Sun Jan 14 19:44:16 2024 +0100 Initial implementation of Deuthon interpreter Introduced the Deuthon language interpreter for parsing and executing code written with German-like syntax in Python. Added infrastructure files, including a .gitignore, LICENSE, and pyproject.toml, defining the project setup and metadata. Implemented core modules to handle the language-specific logic, such as token transformations, custom import hooks, and wrappers for standard library modules with German names. This foundation enables the development and testing of Deuthon scripts, laying the groundwork for future enhancements and community contributions. diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7a9dbd8 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +venv/ +*.pyc +__pycache__/ +.vscode/ diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..beb5645 --- /dev/null +++ b/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2023 Kumi Mitterer + +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. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..e69de29 diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..73bb09c --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,26 @@ +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[project] +name = "deuthon" +version = "0.0.1" +authors = [ + { name="Kumi Mitterer", email="deuthon@kumi.email" }, +] +description = "Deuthon interpreter written in Python" +readme = "README.md" +license = { file="LICENSE" } +requires-python = ">=3.10" +classifiers = [ + "Programming Language :: Python :: 3", + "License :: OSI Approved :: MIT License", + "Operating System :: OS Independent", +] + +[project.scripts] +deuthon = "deuthon.__main__:main" + +[project.urls] +"Homepage" = "https://kumig.it/kumitterer/deuthon" +"Bug Tracker" = "https://kumig.it/kumitterer/deuthon/issues" \ No newline at end of file diff --git a/src/deuthon/__init__.py b/src/deuthon/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/deuthon/__main__.py b/src/deuthon/__main__.py new file mode 100755 index 0000000..96d8aaf --- /dev/null +++ b/src/deuthon/__main__.py @@ -0,0 +1,70 @@ +#!/usr/bin/env python3 + +import ast +import tokenize +import pathlib +import setuptools +import sys +import traceback + +from argparse import ArgumentParser + +from deuthon.transformer import parse_german_code, prepare_builtin_overrides +from deuthon.importer import install_deuthon_importer, import_base +from deuthon.interpreter import interpreter + +import deuthon.base + + +def main(): + args = ArgumentParser() + args.add_argument("file", default="", nargs="?") + args.add_argument("--version", "-V", action="version", version="Deuthon 0.1.0") + args = args.parse_args() + + prepare_builtin_overrides() + install_deuthon_importer(pathlib.Path(__file__).parent / "wrappers") + import_base(pathlib.Path(__file__).parent / "base") + + if args.file: + with open(args.file) as f: + german_code = f.read() + + python_code = parse_german_code(german_code) + + code_object = compile( + python_code, + pathlib.Path(args.file).absolute(), + "exec", + dont_inherit=True, + optimize=0, + ) + + try: + exec(code_object, {"__file__": pathlib.Path(args.file).absolute()}) + except Ausnahme as e: + # Extract traceback information, excluding frames related to the interpreter + tb = e.__traceback__ + while tb is not None: + if __file__ in tb.tb_frame.f_code.co_filename: + # Remove this frame from the traceback + if tb.tb_next is None: + # Reached the end of the traceback linked list; remove reference to this frame + tb = None + else: + # Skip this frame and link the previous frame to the next one + tb = tb.tb_next + else: + break + + # Format and print the modified traceback + formatted_tb = "".join(traceback.format_exception(type(e), e, tb)) + print(formatted_tb, file=sys.stderr, end="") + return 1 + + else: + interpreter() + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/src/deuthon/base/__init__.deu b/src/deuthon/base/__init__.deu new file mode 100644 index 0000000..e69de29 diff --git a/src/deuthon/base/ausnahme.deu b/src/deuthon/base/ausnahme.deu new file mode 100644 index 0000000..1c67569 --- /dev/null +++ b/src/deuthon/base/ausnahme.deu @@ -0,0 +1,2 @@ +GrundAusnahme = BaseException +Ausnahme = Exception \ No newline at end of file diff --git a/src/deuthon/base/bedingungen.deu b/src/deuthon/base/bedingungen.deu new file mode 100644 index 0000000..35c8f50 --- /dev/null +++ b/src/deuthon/base/bedingungen.deu @@ -0,0 +1 @@ +istinstanz = isinstance \ No newline at end of file diff --git a/src/deuthon/dictionary/__init__.py b/src/deuthon/dictionary/__init__.py new file mode 100644 index 0000000..1ee40bc --- /dev/null +++ b/src/deuthon/dictionary/__init__.py @@ -0,0 +1,80 @@ +from pathlib import Path +import importlib + +dictionary = { + # Conditionals + "wenn": "if", + "sonstwenn": "elif", + "falls": "if", + "sonstfalls": "elif", + "sonst": "else", + "anderenfalls": "else", + + # Booleans / Truth Values / Logic + "Wahr": "True", + "Falsch": "False", + "Keines": "None", + "Nichts": "None", + "nicht": "not", + "und": "and", + "oder": "or", + + # Imports / Contexts + "importiere": "import", + "von": "from", + "aus": "from", + "als": "as", + "mit": "with", + "öffne": "open", + + # Loops + "für": "for", + "von": "in", + "während": "while", + "solange": "while", + "bis": "until", + + # Try / Except + "versuche": "try", + "außer": "except", + "fange": "except", + "schließlich": "finally", + + # Base Types + "wahrheitswert": "bool", + "zeichenkette": "str", + "ganzzahl": "int", + "fließkommazahl": "float", + "liste": "list", + "tupel": "tuple", + "wörterbuch": "dict", + "menge": "set", + "gefrorenemenge": "frozenset", + "datei": "file", + + # Base Functions + "drucke": "print", + "Bereich": "range", + + # Function control flow + "definiere": "def", + "klasse": "class", + "weiter": "continue", + "passe": "pass", + "gib": "yield", + "wirf": "raise", + "stellesicher": "assert", + "sicherstelle": "assert", + "lösche": "del", + "zurück": "return", + "gibzurück": "return", + "brichab": "break", + + # Globals, locals, and nonlocals + "__datei__": "__file__", + "__klasse__": "__class__", + + # Other things + "poppen": "pop", + "dekodieren": "decode", +} diff --git a/src/deuthon/importer.py b/src/deuthon/importer.py new file mode 100644 index 0000000..24fe002 --- /dev/null +++ b/src/deuthon/importer.py @@ -0,0 +1,157 @@ +import importlib.abc +import importlib.util +import importlib.machinery +import os +import tokenize +import pathlib +import sys +import builtins + +from .transformer import parse_german_code + + +class DeuthonSourceLoader(importlib.machinery.SourceFileLoader): + def create_module(self, spec): + # Creating the module instance. The import system will then call exec_module. + module = super().create_module(spec) + if module is not None: + # Set __package__ if not already set + if module.__package__ is None: + module.__package__ = ( + spec.name + if spec.submodule_search_locations is None + else spec.parent + ) + return module + + def exec_module(self, module): + # The module's __file__ attribute must be set prior to the code execution to support relative imports. + module.__file__ = self.get_filename(module.__name__) + # Call the parent method to execute the module body + super().exec_module(module) + + def source_to_code(self, data, path, *, _optimize=-1): + reader = tokenize.open(path) + try: + encoding = reader.encoding + source_data = reader.read() + finally: + reader.close() + + transpiled_code = parse_german_code(source_data) + return compile( + transpiled_code, path, "exec", dont_inherit=True, optimize=_optimize + ) + + +class DeuthonModuleFinder(importlib.abc.MetaPathFinder): + def __init__(self, wrappers_directory, extension=".deu"): + self.wrappers_directory = wrappers_directory + self.extension = extension + + def find_spec(self, fullname, path, target=None): + # Check inside the wrappers directory first + deu_wrapper_path = os.path.join( + self.wrappers_directory, fullname + self.extension + ) + if os.path.isfile(deu_wrapper_path): + loader = DeuthonSourceLoader(fullname, deu_wrapper_path) + spec = importlib.util.spec_from_loader( + fullname, loader, origin=deu_wrapper_path + ) + return spec + + # Check for a .py wrapper + wrapper_path = os.path.join(self.wrappers_directory, fullname + ".py") + if os.path.isfile(wrapper_path): + return importlib.util.spec_from_file_location(fullname, wrapper_path) + + # Check for a wrapper package + deu_wrapper_path = self._find_deu_file(fullname, [self.wrappers_directory]) + if deu_wrapper_path: + loader = DeuthonSourceLoader(fullname, deu_wrapper_path) + spec = importlib.util.spec_from_loader( + fullname, loader, origin=deu_wrapper_path + ) + return spec + + # If it's not a wrapper, look for a .deu file + deu_path = self._find_deu_file(fullname, path) + if deu_path: + loader = DeuthonSourceLoader(fullname, deu_path) + spec = importlib.util.spec_from_loader(fullname, loader, origin=deu_path) + if self._is_package(deu_path): + spec.submodule_search_locations = [os.path.dirname(deu_path)] + return spec + + # Neither .deu file nor wrapper found + return None + + def _is_package(self, path): + # Determine if the path is a package by checking for an __init__.deu file + return os.path.isdir(path) and os.path.isfile( + os.path.join(path, "__init__.deu") + ) + + def _find_deu_file(self, fullname, path=None): + # Determine search paths + if not path: + # If 'path' is not provided, create a search path + # Including the current directory and any DEUTHON_PATH directories + path = ["."] + deuthon_path = os.environ.get("DEUTHON_PATH") + if deuthon_path: + path.extend(deuthon_path.split(os.pathsep)) + + # The base name for the file we're trying to find will be the last component of 'fullname' + # e.g., for 'testmodul.submodul.test', we want to end up with 'test.deu' + module_basename = ( + fullname.rpartition(".")[-1] + self.extension + ) # e.g., 'test.deu' + + for entry in path: + module_full_path = os.path.join( + entry, fullname.replace(".", "/") + self.extension + ) + init_full_path = os.path.join( + entry, fullname.replace(".", "/"), "__init__" + self.extension + ) + + if os.path.isfile( + module_full_path + ): # Regular module (or top-level package) + return module_full_path + + if os.path.isdir(os.path.dirname(init_full_path)) and os.path.isfile( + init_full_path + ): # Package + return init_full_path + + return None # No module or package found + + +def install_deuthon_importer(wrappers_directory): + deuthon_finder = DeuthonModuleFinder(wrappers_directory) + sys.meta_path.insert(0, deuthon_finder) + + +def import_base(base_directory): + # Find all .deu files in the given directory + for filename in os.listdir(base_directory): + if filename.endswith(".deu"): + # Construct module name from file name + module_name = filename[:-4] # Remove .deu extension + module_path = os.path.join(base_directory, filename) + + # Load the module using DeuthonSourceLoader + loader = DeuthonSourceLoader(module_name, module_path) + spec = importlib.util.spec_from_loader( + module_name, loader, origin=module_path + ) + module = importlib.util.module_from_spec(spec) + loader.exec_module(module) + + # Add all names that don't start with an underscore to builtins + for name in dir(module): + if not name.startswith("_"): + setattr(builtins, name, getattr(module, name)) diff --git a/src/deuthon/interpreter.py b/src/deuthon/interpreter.py new file mode 100644 index 0000000..4b285ec --- /dev/null +++ b/src/deuthon/interpreter.py @@ -0,0 +1,11 @@ +from .transformer import parse_german_code + +def interpreter(): + while True: + try: + german_code = input('>>> ') + except EOFError: + break + + python_code = parse_german_code(german_code) + exec(python_code) \ No newline at end of file diff --git a/src/deuthon/transformer.py b/src/deuthon/transformer.py new file mode 100644 index 0000000..be94f3f --- /dev/null +++ b/src/deuthon/transformer.py @@ -0,0 +1,37 @@ +import ast +import tokenize + +from io import BytesIO + +from .dictionary import dictionary + +def translate_german_keywords(tokens): + for token in tokens: + # Translate German keywords to English + if token.type == tokenize.NAME and token.string in dictionary: + yield token._replace(string=dictionary[token.string]) + else: + yield token + +def parse_german_code(german_code): + # Convert the German code into bytes for tokenization + bytes_code = bytes(german_code, 'utf-8') + + # Tokenize the German code + tokens = tokenize.tokenize(BytesIO(bytes_code).readline) + + # Translate German tokens to their English (Python) counterparts + english_tokens = translate_german_keywords(tokens) + + # Detokenize back to a code string in English/Python + python_code_str = tokenize.untokenize(english_tokens).decode('utf-8') + + # Return the compiled Python code object + return python_code_str + +def prepare_builtin_overrides(): + import sys + + original_json = sys.modules["json"] + sys.modules["sysjson"] = original_json + del sys.modules["json"] \ No newline at end of file diff --git a/src/deuthon/wrappers/ervbib/__init__.deu b/src/deuthon/wrappers/ervbib/__init__.deu new file mode 100644 index 0000000..e733510 --- /dev/null +++ b/src/deuthon/wrappers/ervbib/__init__.deu @@ -0,0 +1 @@ +importiere urllib \ No newline at end of file diff --git a/src/deuthon/wrappers/ervbib/anfrage.deu b/src/deuthon/wrappers/ervbib/anfrage.deu new file mode 100644 index 0000000..6937aff --- /dev/null +++ b/src/deuthon/wrappers/ervbib/anfrage.deu @@ -0,0 +1,27 @@ +importiere urllib.request +importiere http.client + +importiere htüp.kundin + +definiere Anfrage(erv, *argumente, **swargumente): + kopfzeilen = swargumente.get("kopfzeilen", {}) + + wenn "kopfzeilen" in swargumente: + lösche swargumente["kopfzeilen"] + + wenn "benutzer_agent" in swargumente: + kopfzeilen["User-Agent"] = swargumente["benutzer_agent"] + lösche swargumente["benutzer_agent"] + + wenn kopfzeilen: + swargumente["headers"] = kopfzeilen + + gibzurück urllib.request.Request(erv, *argumente, **swargumente) + +definiere ervöffnen(*argumente, **swargumente): + antwort = urllib.request.urlopen(*argumente, **swargumente) + + wenn istinstanz(antwort, http.client.HTTPResponse): + antwort.__klasse__ = htüp.kundin.HTÜPAntwort + + gibzurück antwort \ No newline at end of file diff --git a/src/deuthon/wrappers/htüp/__init__.deu b/src/deuthon/wrappers/htüp/__init__.deu new file mode 100644 index 0000000..8f88914 --- /dev/null +++ b/src/deuthon/wrappers/htüp/__init__.deu @@ -0,0 +1 @@ +importiere http \ No newline at end of file diff --git a/src/deuthon/wrappers/htüp/kundin.deu b/src/deuthon/wrappers/htüp/kundin.deu new file mode 100644 index 0000000..1979125 --- /dev/null +++ b/src/deuthon/wrappers/htüp/kundin.deu @@ -0,0 +1,9 @@ +importiere http.client + +klasse HTÜPAntwort(http.client.HTTPResponse): + definiere __init__(selbst, *argumente, **swargumente): + super().__init__(*argumente, **swargumente) + + definiere lesen(selbst, *argumente, **swargumente): + return super().read(*argumente, **swargumente) + diff --git a/src/deuthon/wrappers/importbib.deu b/src/deuthon/wrappers/importbib.deu new file mode 100644 index 0000000..bbd2add --- /dev/null +++ b/src/deuthon/wrappers/importbib.deu @@ -0,0 +1,3 @@ +importiere importlib + +importiere_modul = importlib.import_module \ No newline at end of file diff --git a/src/deuthon/wrappers/json.deu b/src/deuthon/wrappers/json.deu new file mode 100644 index 0000000..2948e92 --- /dev/null +++ b/src/deuthon/wrappers/json.deu @@ -0,0 +1,6 @@ +importiere sysjson + +laden = sysjson.load +ladenz = sysjson.loads +abladen = sysjson.dump +abladenz = sysjson.dumps \ No newline at end of file diff --git a/src/deuthon/wrappers/zeichenkette.deu b/src/deuthon/wrappers/zeichenkette.deu new file mode 100644 index 0000000..be0a528 --- /dev/null +++ b/src/deuthon/wrappers/zeichenkette.deu @@ -0,0 +1,24 @@ +importiere string + +klasse Formatierer(string.Formatter): + def __init__(selbst, *argumente, **swargumente): + super().__init__(*argumente, **swargumente) + + def formatiere(selbst, formatstring, *argumente, **swargumente): + gibzurück super().format(formatstring, *argumente, **swargumente) + +klasse Vorlage(string.Template): + def __init__(selbst, *argumente, **swargumente): + super().__init__(*argumente, **swargumente) + +ascii_buchstaben = string.ascii_letters +ascii_kleinbuchstaben = string.ascii_lowercase +ascii_großbuchstaben = string.ascii_uppercase +ziffern = string.digits +hexadezimalziffern = string.hexdigits +oktalziffern = string.octdigits +druckbar = string.printable +leerzeichen = string.whitespace +punktuation = string.punctuation + +kapitalisiere_wörter = string.capwords \ No newline at end of file diff --git a/src/deuthon/wrappers/zeit.deu b/src/deuthon/wrappers/zeit.deu new file mode 100644 index 0000000..9b4fe1e --- /dev/null +++ b/src/deuthon/wrappers/zeit.deu @@ -0,0 +1,4 @@ +importiere time + +schlafe = time.sleep +zeit = time.time \ No newline at end of file diff --git a/src/deuthon/wrappers/zufall.deu b/src/deuthon/wrappers/zufall.deu new file mode 100644 index 0000000..3b2f2fd --- /dev/null +++ b/src/deuthon/wrappers/zufall.deu @@ -0,0 +1,18 @@ +importiere random + +klasse SystemZufall(random.SystemRandom): + def __init__(selbst, *argumente, **swargumente): + super().__init__(*argumente, **swargumente) + + def besamen(selbst, *argumente, **swargumente): + gibzurück selbst.seed(*argumente, **swargumente) + + def zufallsganzzahl(selbst, a, b): + gibzurück selbst.randint(a, b) + + def zufall(selbst): + gibzurück selbst.random() + +zufallsganzzahl = random.randint +zufall = random.random +besamen = random.seed \ No newline at end of file diff --git a/tests/ervbib_json.deu b/tests/ervbib_json.deu new file mode 100644 index 0000000..1423a6b --- /dev/null +++ b/tests/ervbib_json.deu @@ -0,0 +1,9 @@ +importiere ervbib.anfrage +importiere json + +anfrage = ervbib.anfrage.Anfrage( + erv="https://geek-jokes.sameerkumar.website/api?format=json", + benutzer_agent="ERV-Test/1.0", +) + +drucke(json.ladenz(ervbib.anfrage.ervöffnen(anfrage).lesen().dekodieren())["joke"]) diff --git a/tests/test.deu b/tests/test.deu new file mode 100644 index 0000000..c79db78 --- /dev/null +++ b/tests/test.deu @@ -0,0 +1,50 @@ +# Tests für `drucke()` und globale Variablen + +drucke("Starte Tests...") +drucke(f"Aktuelle Datei: " + str(__datei__)) + +# Import-Tests + +aus zufall importiere SystemZufall +aus zeit importiere zeit + +importiere testmodul.test +importiere testimport + +# Variablen-Tests (Typen, Zuweisungen, etc.) + +länge: ganzzahl = 1 + +system_zufall: SystemZufall = SystemZufall() +system_zufall.besamen(ganzzahl(zeit())) + + +# Funktions-Tests (Parameter, Rückgabewerte, etc.) + +definiere hallo_welt(länge: ganzzahl = länge) -> Nichts: + global system_zufall + für i in Bereich(länge): + falls nicht wahrheitswert(system_zufall.zufallsganzzahl(0, 1)) == Wahr: + drucke(i, ": Hallo Welt!") + anderenfalls: + drucke(i, ": Tschüss Welt!") + +drucke(hallo_welt()) + + +# Sicherstellung von Bedingungen + +stellesicher nicht hallo_welt(), "Hallo Welt ist nicht Nichts!" + + +# Werfen von Ausnahmen + +versuche: + wirf Ausnahme("Test") +außer Ausnahme als a: + drucke(a) + + +# ERVBib und JSON + +importiere ervbib_json \ No newline at end of file diff --git a/tests/testimport.deu b/tests/testimport.deu new file mode 100644 index 0000000..040cf5c --- /dev/null +++ b/tests/testimport.deu @@ -0,0 +1 @@ +drucke("Hallo aus dem Testimport!") \ No newline at end of file diff --git a/tests/testmodul/__init__.deu b/tests/testmodul/__init__.deu new file mode 100644 index 0000000..56bef65 --- /dev/null +++ b/tests/testmodul/__init__.deu @@ -0,0 +1,3 @@ +drucke("Hallo aus dem Testmodul!") + +import testmodul.test \ No newline at end of file diff --git a/tests/testmodul/test.deu b/tests/testmodul/test.deu new file mode 100644 index 0000000..e69de29