Volver al inicio

Creando DSL en Python: intérprete en ruso

Desglose detallado de la creación de DSL LawScript en Python: arquitectura sin lexer separado, preprocesador con metadatos de línea, compilación de expresiones a RPN y serialización de AST para un inicio más rápido. Código y principios para audiencia técnica.

Cómo creé un DSL en idioma ruso en Python para mi tesis
Advertisement 728x90

Creando un DSL en Python: Cómo construí un intérprete de lenguaje ruso para mi proyecto de diploma

El intérprete de LawScript no es solo un experimento académico, es un DSL práctico con un subsistema imperativo, construido en Python para abordar retos legales. El autor combinó descripciones declarativas de contratos con lógica procedural para la validación de reglas, creando un lenguaje híbrido aprobado por el departamento como proyecto de tesis. En su núcleo: un parser sin lexer separado, un preprocesador con metadatos de línea y compilación a un AST serializado como "bytecode".

Arquitectura sin lexer: Cuando reinventar la rueda es una ventaja

El pipeline habitual de "lexer → parser → compiler → interpreter" se rompe deliberadamente aquí: el análisis léxico está incrustado directamente en el parser. Cada línea de código fuente se envuelve en una clase Line que almacena el número de línea, la ruta del archivo y el texto original. Esto preserva el contexto de errores y simplifica la depuración. Estructura de ejemplo:

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)

El preprocesador maneja directivas VKLYuChIT, cargando recursivamente módulos del directorio actual o la biblioteca estándar. Soporta tres tipos de archivos: .raw (archivos fuente), .law (AST serializado), .pyl (extensiones de Python). Las importaciones se almacenan en caché para evitar reprocesarlas.

Google AdInline article slot

Análisis sintáctico por composición: Gramática como un conjunto de microservicios

El parser se implementa como una clase base abstracta, con parsers especializados que heredan de ella para cada constructo gramatical. Esto permite anidar parsers: al encontrar un constructo conocido, el parser actual delega en su hijo. El resultado es un árbol MetaObject, que luego se valida y convierte en objetos ejecutables.

El análisis léxico lo maneja el método separate_line_to_token, que divide la línea en tokens ignorando comentarios y verificando el balance de corchetes. En caso de errores, proporciona una pista posicional: un cursor apunta al símbolo problemático. Por ejemplo, una coma extra en una expresión:

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

Notación polaca inversa y expresiones complejas

Las expresiones de cualquier complejidad, incluidos argumentos por defecto, se compilan en una pila RPN. Esto maneja correctamente la precedencia de operadores y las llamadas a funciones anidadas. Por ejemplo, un procedimiento de ordenación de arrays:

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

Esto se transforma en un AST como este:

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

Compilador y ejecución: Del árbol a las acciones

El compilador recorre el AST, transformando cada nodo en un objeto ejecutable. Por ejemplo, AssignField se convierte en una asignación de variable, Loop en un bucle con límites calculados en tiempo de ejecución. El intérprete ejecuta estos objetos de forma secuencial, gestionando una pila de llamadas y variables locales. Para reruns más rápidos, el AST se serializa en un archivo .law: el "bytecode", que se carga sin volver a analizar.

Lo más importante

  • DSL de doble naturaleza: contratos declarativos + lógica imperativa = híbrido único para tareas legales.
  • Metadatos por línea: la clase Line preserva el contexto para diagnósticos de errores precisos.
  • Composición del parser: cada constructo gramatical es su propia clase, facilitando las extensiones del lenguaje.
  • RPN para expresiones: maneja aritmética compleja y argumentos por defecto sin dependencias externas.
  • Serialización de AST: el archivo .law actúa como caché, acelerando los lanzamientos tras la compilación inicial.

— Editorial Team

Google AdInline article slot
Advertisement 728x90

Leer después