Zurück zur Startseite

DSL in Python erstellen: Russischsprachiger Interpreter

Detaillierte Aufschlüsselung der Erstellung der DSL LawScript in Python: Architektur ohne separaten Lexer, Preprocessor mit Zeilenmetadaten, Kompilierung von Ausdrücken zu RPN und AST-Serialisierung für schnelleren Start. Code und Prinzipien für technisches Publikum.

Wie ich eine russischsprachige DSL in Python für meine Abschlussarbeit erstellt habe
Advertisement 728x90

## Eine DSL in Python erstellen: So habe ich einen Interpreter für eine russische Sprache für mein Diplomprojekt gebaut

Der LawScript-Interpreter ist nicht nur ein akademisches Experiment – er ist eine praktische DSL mit einem imperativen Subsytem, in Python entwickelt, um rechtliche Herausforderungen zu meistern. Der Autor hat deklarative Vertragsbeschreibungen mit prozeduraler Logik für die Regelüberprüfung kombiniert und dadurch eine Hybridsprache geschaffen, die vom Fachbereich als Thesis-Projekt genehmigt wurde. Im Kern: ein Parser ohne separaten Lexer, ein Präprozessor mit Zeilenmetadaten und Kompilation zu einem AST, der als „Bytecode“ serialisiert wird.

Lexer-freie Architektur: Wann das Rad neu erfinden ein Feature ist

Der übliche Pipeline-Ablauf „Lexer → Parser → Compiler → Interpreter“ wird hier absichtlich unterbrochen: Die lexikalische Analyse ist direkt in den Parser eingebettet. Jede Zeile des Quelltexts wird in eine Line-Klasse gepackt, die Zeilennummer, Dateipfad und Originaltext speichert. So bleibt der Fehlerkontext erhalten und das Debugging wird vereinfacht. Beispielstruktur:

class Info(NamedTuple):
    num: int
    file: str
    raw_line: str

class Line(str):
    def __new__(cls, value: str, num: int = 0, file: str = ""):
        obj = str.__new__(cls, value)
        obj.raw_data = value
        obj.num = num
        obj.file = file
        return obj

    def get_file_info(self) -> Info:
        return Info(num=self.num, file=self.file, raw_line=self.raw_data)

Der Präprozessor verarbeitet VKLYuChIT-Direktiven und lädt Module rekursiv aus dem aktuellen Verzeichnis oder der Standardbibliothek. Er unterstützt drei Dateitypen: .raw (Quelltexte), .law (serialisierter AST), .pyl (Python-Erweiterungen). Imports werden zwischengespeichert, um erneute Verarbeitung zu vermeiden.

Google AdInline article slot

Parsing durch Komposition: Grammatik als Satz von Microservices

Der Parser ist als abstrakte Basisklasse implementiert, von der spezialisierte Parser für jeden grammatikalischen Konstrukt erben. Das erlaubt das Verschachteln von Parsers: Bei einem bekannten Konstrukt delegiert der aktuelle Parser an sein Kind. Das Ergebnis ist ein MetaObject-Baum, der validiert und in ausführbare Objekte umgewandelt wird.

Die lexikalische Analyse übernimmt die Methode separate_line_to_token, die die Zeile in Tokens zerlegt, Kommentare ignoriert und die Klammerbalance prüft. Bei Fehlern gibt sie einen positionsbezogenen Hinweis – ein Cursor zeigt auf das problematische Symbol. Zum Beispiel eine überflüssige Komma in einem Ausdruck:

if token_ == Tokens.right_bracket:
    sub_expr = expr[offset:]
    previous_tok = sub_expr[offset_ - 1]
    if previous_tok == Tokens.comma:
        err_expr = ''.join([str(i) for i in sub_expr][:offset_+1])
        sub_expr = [str(i) for i in sub_expr]
        res_expr = ''.join(str(i) for i in expr)
        target_comma = (
            f"{err_expr}\n"
            f"{' ' * (sum(len(t) for o, t in enumerate(sub_expr) if o < offset_ - 1))}^"
        )
        raise InvalidExpression(
            f"In vyrazhenii: '{res_expr}' worth lishnyaya zapyataya '{Tokens.comma}'\n\n"
            f"{target_comma}\n"
        )

Umgekehrte Polnische Notation und komplexe Ausdrücke

Ausdrücke beliebiger Komplexität, inklusive Default-Argumente, werden in einen RPN-Stack kompiliert. Das behandelt Operatorpriorität und verschachtelte Funktionsaufrufe korrekt. Beispiel: Eine Array-Sortierprozedur:

Google AdInline article slot
OPREDELIT PROTsEDURU sortirovka_array(array_numbers) (
    SET length = length_array(array_numbers);
    SET minimum_index = 0;

    LOOP index FROM 0 TO length-1 (
        minimum_index = index;
        LOOP internal_index FROM index+1 TO length-1 (
            IF fetch_from_array(array_numbers, internal_index) MENShE fetch_from_array(array_numbers, minimum_index) THEN (
                minimum_index = internal_index;
            )
        )
        IF minimum_index NERAVNO index THEN (
            SET temporary_variable = fetch_from_array(array_numbers, index);
            change_in_array(array_numbers, index, fetch_from_array(array_numbers, minimum_index));
            change_in_array(array_numbers, minimum_index, temporary_variable);
        )
    )
    NAPEChATAT array_numbers;
)

Dies wird zu einem AST wie diesem:

[
  ["AssignField", "TARGET", "length", "EXPR", [...]],
  ["AssignField", "TARGET", "minimum_index", "EXPR", [Number(0)]],
  ["Loop",
    "FROM_EXPR", [Number(0)],
    "TO_EXPR",   [Service name: <length>, Number(1), Service name: <->],
    [
      ["AssignOverrideVariable", "TARGET_EXPR", [...], "OVERRIDE_EXPR", [...]],
      ["Loop",
        "FROM_EXPR", [...],
        "TO_EXPR",   [...],
        [
          ["When", "EXPR", [...],
            [["AssignOverrideVariable", ...]]
          ]
        ]
      ],
      ["When", "EXPR", [...]],
      [["AssignField", ...], [...], [...]]
    ]
  ],
  ["Print", "EXPR", [Service name: <array_numbers>]]
]

Compiler und Ausführung: Vom Baum zu Aktionen

Der Compiler durchläuft den AST und wandelt jeden Knoten in ein ausführbares Objekt um. Zum Beispiel wird AssignField zu einer Variablenzuweisung, Loop zu einer Schleife mit laufzeitberechneten Grenzen. Der Interpreter führt diese Objekte sequentiell aus, verwaltet einen Call-Stack und lokale Variablen. Für schnellere Wiederläufe wird der AST in eine .law-Datei serialisiert – „Bytecode“ – und ohne erneutes Parsen geladen.

Was wichtig ist

  • Duale DSL-Natur: deklarative Verträge + imperative Logik = einzigartiges Hybrid für Rechtsaufgaben.
  • Metadaten pro Zeile: Line-Klasse erhält Kontext für präzise Fehlendiagnose.
  • Parser-Komposition: Jeder grammatikalische Konstrukt ist eigene Klasse, erleichtert Spracherweiterungen.
  • RPN für Ausdrücke: Behandelt komplexe Arithmetik und Default-Argumente ohne externe Abhängigkeiten.
  • AST-Serialisierung: .law-Datei als Cache, beschleunigt Starts nach Initialkompilation.

— Editorial Team

Google AdInline article slot
Advertisement 728x90

Weiterlesen