用 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 扩展)。导入会被缓存,避免重复处理。
通过组合方式解析:语法即微服务集合
解析器实现为抽象基类,每个语法结构都有专属的子解析器继承自它。这允许嵌套解析:遇到熟悉结构时,当前解析器委托给子解析器。结果是一个 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 栈。这能正确处理运算符优先级和嵌套函数调用。例如,一个数组排序过程:
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
暂无评论。