返回首页

在 Python 中创建 DSL:俄语解释器

创建 DSL LawScript 的详细分解:在 Python 中无独立 lexer 的架构、带行元数据的预处理器、表达式编译到 RPN 和 AST 序列化以加速启动。针对技术受众的代码和原理。

我如何为论文在 Python 中创建俄语 DSL
Advertisement 728x90

用 Python 创建 DSL:我如何为毕业设计构建俄语解释器

LawScript 解释器远非单纯的学术实验——它是一个实用的 DSL,内置命令式子系统,用 Python 开发,专为应对法律难题而生。作者巧妙融合了声明式合同描述与过程化规则验证逻辑,打造出一种独特的混合语言,并获系里认可作为毕业论文项目。其核心组件包括:无独立词法分析器的解析器、附带行元数据的预处理器,以及编译为“字节码”序列化的 AST。

无词法分析器架构:重造轮子也能成为亮点

传统的“词法分析器 → 解析器 → 编译器 → 解释器”流水线在这里被故意打破:词法分析直接嵌入解析器中。每行源代码都被包装在 Line 类中,该类存储行号、文件路径和原始文本。这样保留了错误上下文,便于调试。示例结构:

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)

预处理器处理 VKLYuChIT 指令,从当前目录或标准库递归加载模块。它支持三种文件类型:.raw(源文件)、.law(序列化 AST)、.pyl(Python 扩展)。导入会被缓存,避免重复处理。

Google AdInline article slot

通过组合方式解析:语法即微服务集合

解析器实现为抽象基类,每个语法结构都有专属的子解析器继承自它。这允许嵌套解析:遇到熟悉结构时,当前解析器委托给子解析器。结果是一个 MetaObject 树,随后进行验证并转换为可执行对象。

词法分析由 separate_line_to_token 方法处理,它将行拆分为令牌,同时忽略注释并检查括号平衡。出错时,提供位置提示——光标指向问题符号。例如,表达式中多余的逗号:

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

逆波兰记法与复杂表达式

任意复杂度的表达式,包括默认参数,都会被编译为 RPN 栈。这能正确处理运算符优先级和嵌套函数调用。例如,一个数组排序过程:

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

这会转换为如下 AST:

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

编译器与执行:从树到动作

编译器遍历 AST,将每个节点转换为可执行对象。例如,AssignField 变为变量赋值,Loop 变为运行时计算边界的循环。解释器按序执行这些对象,管理调用栈和局部变量。为加速重跑,将 AST 序列化为 .law 文件——“字节码”——无需重新解析即可加载。

关键要点

  • 双重性质 DSL:声明式合同 + 命令式逻辑 = 法律任务的独特混合。
  • 每行元数据Line 类保留上下文,实现精准错误诊断。
  • 解析器组合:每个语法结构独立成类,便于语言扩展。
  • 表达式用 RPN:处理复杂算术和默认参数,无需外部依赖。
  • AST 序列化.law 文件充当缓存,初次编译后加速启动。

— Editorial Team

Google AdInline article slot
Advertisement 728x90

继续阅读