Retour à l'accueil

Création de DSL en Python : interpréteur en langue russe

Analyse détaillée de la création du DSL LawScript en Python : architecture sans lexer séparé, préprocesseur avec métadonnées de ligne, compilation d'expressions en RPN et sérialisation AST pour un démarrage plus rapide. Code et principes pour un public technique.

Comment j'ai créé un DSL en langue russe en Python pour ma thèse
Advertisement 728x90

Création d'un DSL en Python : Comment j'ai construit un interpréteur en langage russe pour mon projet de diplôme

L'interpréteur LawScript n'est pas seulement une expérience académique — c'est un DSL pratique doté d'un sous-système impératif, construit en Python pour relever des défis juridiques. L'auteur a combiné des descriptions déclaratives de contrats avec une logique procédurale pour la validation des règles, créant un langage hybride approuvé par le département comme projet de thèse. Au cœur : un analyseur syntaxique sans lexer séparé, un préprocesseur avec métadonnées de ligne, et une compilation vers un AST sérialisé sous forme de « bytecode ».

Architecture sans lexer : Quand réinventer la roue est un atout

L'habituel pipeline « lexer → parser → compiler → interpreter » est délibérément rompu ici : l'analyse lexicale est intégrée directement dans l'analyseur syntaxique. Chaque ligne de code source est encapsulée dans une classe Line qui stocke le numéro de ligne, le chemin du fichier et le texte original. Cela préserve le contexte d'erreur et simplifie le débogage. Structure d'exemple :

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)

Le préprocesseur gère les directives VKLYuChIT, chargeant récursivement les modules du répertoire courant ou de la bibliothèque standard. Il supporte trois types de fichiers : .raw (fichiers source), .law (AST sérialisé), .pyl (extensions Python). Les imports sont mis en cache pour éviter le retraitement.

Google AdInline article slot

Analyse syntaxique par composition : La grammaire comme un ensemble de microservices

L'analyseur syntaxique est implémenté comme une classe de base abstraite, avec des analyseurs spécialisés qui en héritent pour chaque construction grammaticale. Cela permet d'imbriquer les analyseurs : en rencontrant une construction familière, l'analyseur courant délègue à son enfant. Le résultat est un arbre MetaObject, qui est ensuite validé et converti en objets exécutables.

L'analyse lexicale est gérée par la méthode separate_line_to_token, qui divise la ligne en tokens tout en ignorant les commentaires et en vérifiant l'équilibre des crochets. En cas d'erreur, elle fournit un indice positionnel — un curseur pointe vers le symbole problématique. Par exemple, une virgule superflue dans une expression :

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

Notation polonaise inversée et expressions complexes

Les expressions de toute complexité, y compris les arguments par défaut, sont compilées en une pile RPN. Cela gère correctement la priorité des opérateurs et les appels de fonctions imbriqués. Par exemple, une procédure de tri de tableau :

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

Cela se transforme en un AST comme ceci :

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

Compilateur et exécution : De l'arbre aux actions

Le compilateur parcourt l'AST, transformant chaque nœud en un objet exécutable. Par exemple, AssignField devient une assignation de variable, Loop une boucle avec des bornes calculées à l'exécution. L'interpréteur exécute ces objets séquentiellement, gérant une pile d'appels et des variables locales. Pour des relances plus rapides, l'AST est sérialisé dans un fichier .law — « bytecode » — chargé sans réanalyse.

Points importants

  • DSL à double nature : contrats déclaratifs + logique impérative = hybride unique pour les tâches juridiques.
  • Métadonnées par ligne : classe Line préserve le contexte pour des diagnostics d'erreur précis.
  • Composition de l'analyseur : chaque construction grammaticale est sa propre classe, facilitant les extensions du langage.
  • RPN pour les expressions : gère l'arithmétique complexe et les arguments par défaut sans dépendances externes.
  • Sérialisation de l'AST : fichier .law agit comme un cache, accélérant les lancements après la compilation initiale.

— Editorial Team

Google AdInline article slot
Advertisement 728x90

Lire ensuite