☰
Python手写编译器前端:词法分析、递归下降与AST符号表
2026/11/4 16:57:56 网站建设 项目流程

简介:围绕简单文法编译器前端设计展开的课程设计报告,面向学习编译原理、需要完成课程设计或动手实现小型编译器的计算机专业学生。内容以词法分析、语法语义分析与中间代码生成为主线,覆盖词法扫描器与Token序列生成、递归下降子程序法、四元式中间代码输出,并延伸到常量定义、数组结构、if-else与while语句的文法扩展及递归子程序栈的跟踪调试,最后简述后端目标代码生成与优化思路。资源包共1个docx文件,约381KB,正文含摘要、任务要求、算法与数据结构、程序设计与实现、实验结果与总结等章节,另附小组分工说明,便于对照复现与课程答辩准备。已有695人学习下载,适合作为编译原理课程设计的参考模板与文法扩展的实践样例。

1. 从一份二十行的最小文法说起:编译器前端到底交付什么

手写编译器前端,第一次尝试的人多半卡在同一个地方:文法写在纸上很漂亮,落到代码里却不知道从哪一行开始写。两页 EBNF 写完之后卡三天,最后抄一份自己都读不懂的 parser——问题出在没人把「文法规则 → token 流 → 语法树 → 符号表」这条链路拆成几个能单独验证的小步骤,跟文法难度无关。这里选一份只有赋值、加减乘除、if、while、花括号的最小文法,用 Python 从零实现词法分析、递归下降解析、AST 和符号表,每一步都能单独运行、单独打印、单独断言。适合学过编译原理但没动手写过前端的人,也适合要做表达式求值、规则引擎、DSL 解释器的工程师——这些场景的内核,就是同一个编译器前端设计。

2. 词法分析:手写 scanner 把字符流切成 token 的三个决策

词法分析是整个前端里最容易低估的一层。很多人觉得无非是「按空格切开」,真写起来才发现要处理1.25和1.的边界、==与=的先后、注释与换行的行号维护、非法字符的定位。这一层写歪了,后面语法分析收到的 token 流就是脏的,报错信息也会一路失真到没法用。

我一般把词法层定位成三件事:把字符流切成有类型的 token、给每个 token 记下位置、遇到无法识别的东西立刻停下并报出行列。下面按三个决策点依次说明。

2.1 生成器还是手写:先看这份文法值不值得依赖工具

遇到「要不要用 lex/flex 这类生成器」的问题,我的判断标准很朴素:文法规模。非终结符在二十个以内、token 种类在三十个以内,手写 scanner 加手写递归下降,总代码量通常比配置生成器再加构建脚本还少,调试时断点能直接打在自己写的循环里,不用去猜生成代码。反过来,如果文法已经膨胀到几百条规则、需要频繁改词法约定、或者团队里有人专门维护文法文件,生成器带来的声明式描述就更划算。

具体到这份最小文法,我倾向手写,理由有三条。第一,token 种类只有十来个,用一张正则表就能覆盖,不需要状态机生成。第二,前端最需要的是精确的错误位置,手写循环里pos、line、line_start三个变量随时可查,报错可以直接给到「第 3 行第 7 列」。第三,生成器产出的错误信息普遍偏抽象,使用者看到的往往是「syntax error near xxx」,而手写层可以自己决定说什么。

提示:手写不等于不用正则。字符级的while加if判断是最容易写错的一类代码,用一条主正则一次匹配一个 token,正确率和可读性都会好很多。

这里有个边界要说清:手写 scanner 适合「词法规则相对稳定、token 数量可控」的场景;如果要做的是完整语言前端,且后续还要改语法,生成器方案在可维护性上会反超。选型并不存在绝对正确的答案,只看哪一边的维护成本更低。

2.2 用一条主正则扫完整个输入:token 规格表与扫描循环

核心思路是把所有 token 规则写成「名字 + 正则」的列表,用命名分组拼成一条主正则,每次从当前位置匹配一次。Python 的re支持在match时指定起始偏移,天然适合这种推进式扫描。

import re from dataclasses import dataclass # 顺序即优先级:写在前面的分支先尝试匹配 TOKEN_SPEC = [ ("NUM", r"\d+(?:\.\d+)?"), # 12 / 3.14 ("ID", r"[A-Za-z_][A-Za-z0-9_]*"), # 标识符,关键字在后面单独提升 ("EQ", r"=="), # 必须排在 ASSIGN 之前 ("ASSIGN", r"="), ("OP", r"[+\-*/]"), ("LPAREN", r"\("), ("RPAREN", r"\)"), ("LBRACE", r"\{"), ("RBRACE", r"\}"), ("SEMI", r";"), ("SKIP", r"[ \t\r\n]+"), # 空白不产生 token ("MISMATCH", r"."), # 兜底分支,命中即报错 ] _MASTER = re.compile("|".join(f"(?P<{name}>{pat})" for name, pat in TOKEN_SPEC)) KEYWORDS = {"if", "else", "while"} @dataclass class Token: kind: str text: str line: int col: int def tokenize(src: str) -> list: tokens = [] line, line_start, pos = 1, 0, 0 while pos < len(src): m = _MASTER.match(src, pos) if m is None: # 理论上 MISMATCH 会兜住,留一层保险 raise SyntaxError(f"{line}:{pos - line_start + 1} 无法切分的字符 {src[pos]!r}") kind, text = m.lastgroup, m.group() if kind == "SKIP": if "\n" in text: # 只有跳过空白时行号才可能变化 line += text.count("\n") line_start = pos + text.rindex("\n") + 1 elif kind == "MISMATCH": raise SyntaxError(f"{line}:{pos - line_start + 1} 非法字符 {text!r}") else: col = pos - line_start + 1 if kind == "ID" and text in KEYWORDS: kind = text.upper() # if -> IF,解析层就不必再比对字符串 tokens.append(Token(kind, text, line, col)) pos = m.end() tokens.append(Token("EOF", "", line, pos - line_start + 1)) return tokens

逻辑说明:_MASTER用命名分组把整张表拼成一条正则,m.lastgroup直接给出命中的规则名,省掉一长串if re.match的重复判断。SKIP分支要单独处理,因为行号只会在跳过含换行符的空白时前进,其他分支里line恒定。MISMATCH用单个.兜底,任何没被前面规则覆盖的字符都会落到这里——这是整个 scanner 的安全网,绝对不能静默丢弃。

参数说明:TOKEN_SPEC的顺序就是匹配优先级,==必须排在=前面,否则==会被切成两个ASSIGN;NUM里用(?:\.\d+)?而非(\.\d+)?,避免多出一个无用捕获组影响lastgroup;col是 1-based 列号,方便直接给人看,line_start记录当前行首在源串中的偏移,用于换算列号。

几处容易改错的地方值得点出来。ID的正则写在关键字判断之前,意味着if会先被识别成ID再提升为IF,这个两步走比在正则里写if|else|while更好维护,加关键字只改集合。EOF这个哨兵 token 是必须的,解析层的循环条件依赖它,缺了它每个while都要额外判越界。float转换放到解析层而非词法层,词法层只负责切分和归类,保持职责单一。

2.3 token 里必须带的三个字段:种类、原文、行列号

三个字段里最容易被省掉的是位置。省掉之后,语法错误只能报「意外的 token」,使用者拿着几百行输入无从下手;语义错误更惨,连是哪个变量出问题都要靠猜。位置信息的成本很低,收益却贯穿整个前端,我一般从第一版就带上。

kind和text的分工也要明确:kind供解析层做分支判断,text供报错和后续取值。NUM的text是"3.14",kind是"NUM";IF的text是"if",kind是"IF"。别用text去做分支判断,那样改一个关键字大小写就要改一堆代码。

写一个几行的 dump 函数,把 token 流打出来看,是排查词法问题时最省时间的手段:

def dump(src): for t in tokenize(src): print(f"{t.kind:<8}{t.text!r:<12}{t.line}:{t.col}") dump("a = 1 + 2 * -3;") # ID 'a' 1:1 # ASSIGN '=' 1:3 # NUM '1' 1:5 # OP '+' 1:7 # NUM '2' 1:9 # OP '*' 1:11 # OP '-' 1:13 # NUM '3' 1:14 # SEMI ';' 1:15 # EOF '' 1 <p> <a href="https://download.csdn.net/download/qq_33311869/9340675" style="color:#ec7500;font-size:14px;"> 本文还有配套的精品资源,点击获取 </a> <img alt="menu-r.4af5f7ec.gif" src="https://csdnimg.cn/release/wenkucmsfe/public/img/menu-r.4af5f7ec.gif" style="width:16px;margin-left:4px;vertical-align:text-bottom;cursor:text;"> </p>

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询