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.
This commit is contained in:
Kumi 2024-01-14 19:44:16 +01:00
commit 6add03fe4e
Signed by: kumi
GPG key ID: ECBCC9082395383F
27 changed files with 563 additions and 0 deletions

4
.gitignore vendored Normal file
View file

@ -0,0 +1,4 @@
venv/
*.pyc
__pycache__/
.vscode/

19
LICENSE Normal file
View file

@ -0,0 +1,19 @@
Copyright (c) 2023 Kumi Mitterer <deuthon@kumi.email>
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.

0
README.md Normal file
View file

26
pyproject.toml Normal file
View file

@ -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"

0
src/deuthon/__init__.py Normal file
View file

70
src/deuthon/__main__.py Executable file
View file

@ -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())

View file

View file

@ -0,0 +1,2 @@
GrundAusnahme = BaseException
Ausnahme = Exception

View file

@ -0,0 +1 @@
istinstanz = isinstance

View file

@ -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",
}

157
src/deuthon/importer.py Normal file
View file

@ -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))

View file

@ -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)

View file

@ -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"]

View file

@ -0,0 +1 @@
importiere urllib

View file

@ -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

View file

@ -0,0 +1 @@
importiere http

View file

@ -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)

View file

@ -0,0 +1,3 @@
importiere importlib
importiere_modul = importlib.import_module

View file

@ -0,0 +1,6 @@
importiere sysjson
laden = sysjson.load
ladenz = sysjson.loads
abladen = sysjson.dump
abladenz = sysjson.dumps

View file

@ -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

View file

@ -0,0 +1,4 @@
importiere time
schlafe = time.sleep
zeit = time.time

View file

@ -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

9
tests/ervbib_json.deu Normal file
View file

@ -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"])

50
tests/test.deu Normal file
View file

@ -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

1
tests/testimport.deu Normal file
View file

@ -0,0 +1 @@
drucke("Hallo aus dem Testimport!")

View file

@ -0,0 +1,3 @@
drucke("Hallo aus dem Testmodul!")
import testmodul.test

0
tests/testmodul/test.deu Normal file
View file