# 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í.
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:
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
Linezachová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
.lawfunguje jako cache, zrychluje spuštění po první kompilaci.
— Editorial Team
Zatím žádné komentáře.