Powrót do strony głównej

Tworzenie DSL w Pythonie: rosyjskojęzyczny interpreter

Szczegółowy rozbiór tworzenia DSL LawScript w Pythonie: architektura bez osobnego lexera, preprocesor z metadanymi linii, kompilacja wyrażeń w RPN i serializacja AST w celu przyspieszenia uruchomienia. Kod i zasady dla publiczności technicznej.

Jak stworzyłem rosyjskojęzyczny DSL w Pythonie na dyplom
Advertisement 728x90

# Tworzenie DSL w Pythonie: jak napisałem rosyjskojęzyczny interpreter na dyplom

Interpreter LawScript — to nie tylko akademicki eksperyment, ale praktyczny DSL z imperatywną podsystemą, stworzony w Pythonie do rozwiązywania zadań prawnych. Autor połączył deklaratywne opisy kontraktów z proceduralną logiką weryfikacji reguł, tworząc język hybrydowy, zaakceptowany przez katedrę jako projekt dyplomowy. W podstawie leży parser bez osobnego leksyka, preprocesor z metadanymi linii oraz kompilacja do AST, serializowanego jako „bajt-kod”.

Architektura bez leksyka: kiedy rower to zaleta

Zwykły łańcuch „leksyk → parser → kompilator → interpreter” został tu celowo przerwany: analiza leksykalna jest wbudowana w parser. Każda linia kodu źródłowego jest opakowana w klasę Line, która przechowuje numer linii, ścieżkę do pliku i oryginalny tekst. Dzięki temu zachowany jest kontekst błędów, a debugowanie staje się prostsze. Przykład struktury:

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)

Preprocesor obsługuje dyrektywy VKLYuChIT, rekurencyjnie ładując moduły z bieżącego katalogu lub standardowej biblioteki. Obsługiwane są trzy typy plików: .raw (źródła), .law (serializowane AST), .pyl (rozszerzenia w Pythonie). Importy są buforowane, by uniknąć ponownego przetwarzania.

Google AdInline article slot

Parsowanie przez kompozycję: gramatyka jako zestaw mikrousług

Parser jest zaimplementowany jako abstrakcyjna klasa bazowa, od której dziedziczą specjalistyczne parsery dla każdej konstrukcji gramatycznej. Pozwala to na zagnieżdżanie parserów: napotkawszy znaną konstrukcję, bieżący parser deleguje obsługę podrzędnemu. Wynikiem jest drzewo MetaObject, które następnie jest walidowane i przekształcane w wykonywalne obiekty.

Analiza leksykalna odbywa się metodą separate_line_to_token, która rozbija linię na tokeny, ignorując komentarze i sprawdzając balans nawiasów. W przypadku błędów wyświetlana jest wskazówka pozycyjna — kursor wskazuje problematyczny symbol. Na przykład, zbędna przecinka w wyrażeniu:

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

Notacja polska odwrotna i złożone wyrażenia

Wyrażenia dowolnej złożoności, w tym argumenty domyślne, są kompilowane do stosu RPN. Umożliwia to poprawne obsługiwanie priorytetów operacji i zagnieżdżonych wywołań funkcji. Na przykład procedura sortowania tablicy:

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

Przekształca się w AST o następującej strukturze:

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

Kompilator i wykonanie: od drzewa do akcji

Kompilator przechodzi po AST, przekształcając każdy węzeł w wykonywalny obiekt. Na przykład AssignField staje się przypisaniem do zmiennej, Loop — pętlą z granicami obliczonymi w czasie wykonywania. Interpreter kolejno wykonuje te obiekty, obsługując stos wywołań i zmienne lokalne. Dla przyspieszenia ponownych uruchomień AST jest serializowane do pliku .law — „bajt-kodu”, który ładuje się bez ponownego parsowania.

Co najważniejsze

  • DSL o podwójnej naturze: deklaratywne kontrakty + imperatywna logika = unikalny hybryd dla zadań prawnych.
  • Metadane w każdej linii: klasa Line zachowuje kontekst dla precyzyjnej diagnostyki błędów.
  • Kompozycja parserów: każda konstrukcja gramatyczna to osobna klasa, co ułatwia rozszerzanie języka.
  • RPN dla wyrażeń: obsługa złożonej arytmetyki i argumentów domyślnych bez zewnętrznych zależności.
  • Serializacja AST: plik .law działa jak bufor, przyspieszając uruchomienie po pierwszej kompilacji.

— Editorial Team

Google AdInline article slot
Advertisement 728x90

Czytaj dalej