Zpět na domů

Vytvoření DSL v Pythonu: ruskojazyčný interpretátor

Podrobný rozbor vytvoření DSL LawScript v Pythonu: architektura bez samostatného lexera, preprocesor s metadaty řádků, kompilace výrazů do RPN a serializace AST pro zrychlení spuštění. Kód a principy pro technickou publiku.

Jak jsem vytvořil ruskojazyčný DSL v Pythonu pro diplom
Advertisement 728x90

# Vytvoření DSL v Pythonu: jak jsem napsal ruskojazyčný interpretátor pro diplomovou práci

Interpretátor LawScript – není to jen akademický experiment, ale praktický DSL s imperativní podsystémou, vytvořený v Pythonu pro řešení právních úkolů. Autor spojil deklarativní popis smluv a procedurální logiku kontroly pravidel, čímž získal hybridní jazyk, který byl katedrou přijat jako diplomová práce. V jádru – parser bez samostatného lexera, preprocesor s metadaty řádků a kompilace do AST, serializovatelné jako „byte-kód“.

Architektura bez lexera: když vynalézání kola je výhoda

Běžný řetězec „lexer → parser → kompiler → interpretátor“ je zde záměrně porušen: lexikální analýza je vestavěná do parseru. Každý řádek zdrojového kódu je zabalen do třídy Line, která uchovává číslo řádku, cestu k souboru a původní text. To umožňuje zachovat kontext chyb a usnadňuje ladění. Příklad 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 zpracovává direktivy VKLYuChIT, rekurzivně načítá moduly z aktuální adresáře nebo standardní knihovny. Podporovány jsou tři typy souborů: .raw (zdrojáky), .law (serializované AST), .pyl (rozšíření v Pythonu). Importy se cachují, aby se zabránilo opakovanému zpracování.

Google AdInline article slot

Parsování prostřednictvím kompozice: gramatika jako sada mikroslužeb

Parser je realizován jako abstraktní základní třída, od níž dědí specializované parsery pro každou gramatickou konstrukci. To umožňuje vkládat parsery do sebe navzájem: při setkání s známou konstrukcí aktuální parser deleguje zpracování dceřině. Výsledek – strom MetaObject, který se poté validuje a převádí na spustitelné objekty.

Lexikální analýza se provádí metodou separate_line_to_token, která rozkládá řádek na tokeny, ignoruje komentáře a kontroluje vyváženost závorek. Při chybách se vypíše poziční nápověda – kurzor ukazuje na problematický symbol. Například přebytečná čárka ve výrazu:

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

Reverzní polská notace a složité výrazy

Výrazy libovolné složitosti včetně argumentů s výchozí hodnotou se kompilují do RPN zásobníku. To umožňuje správné zpracování priorit operací a vnořených volání funkcí. Například procedura seřazení pole:

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

Přeměňuje se na AST následujícího druhu:

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

Kompilátor a provádění: od stromu k akcím

Kompilátor prochází AST a převádí každý uzel na spustitelný objekt. Například AssignField se stává přiřazením proměnné, Loop – cyklem s hranicemi, které se vypočítají v době provádění. Interpretátor tyto objekty provádí postupně, s podporou zásobníku volání a lokálních proměnných. Pro zrychlení opakovaných spuštění se AST serializuje do souboru .law – „byte-kód“, který se načte bez opětovného parsování.

Co je důležité

  • DSL s dvojí povahou: deklarativní smlouvy + imperativní logika = unikátní hybrid pro právní úkoly.
  • Metadaty v každém řádku: třída Line zachovává kontext pro přesnou diagnostiku chyb.
  • Kompozice parserů: každá gramatická konstrukce – samostatná třída, což usnadňuje rozšiřování jazyka.
  • RPN pro výrazy: podpora složité aritmetiky a argumentů s výchozí hodnotou bez externích závislostí.
  • Serializace AST: soubor .law funguje jako cache, zrychluje spuštění po první kompilaci.

— Editorial Team

Google AdInline article slot
Advertisement 728x90

Číst dál