☰
Python实现C语言编译器:LL1文法与四元式生成实战
2026/10/10 9:07:49 网站建设 项目流程

简介:这是一份用Python语言实现的C语言编译器项目,面向编译原理学习者、计算机专业学生及希望深入理解编译器构造的开发者。它采用LL1文法完成语法分析,并借助C语言空语句巧妙化解左递归问题,完整覆盖词法分析、语法分析、语义分析与代码生成等核心阶段,是理论结合实践的典型范例。压缩包共21个文件,约42KB,以8个py源码文件为主体,辅以5个txt文法与字符串说明、5个pyc缓存、1个asm汇编输出、1个c示例及1份docx文档,结构紧凑、模块划分清晰。目前已有277人学习下载。读者可从中获取完整的文法规则、各组件实现脚本与测试用例,通过阅读与调试代码,直观掌握LL1解析表的构建逻辑、抽象语法树的生成流程以及目标代码的转换思路,适合作为课程设计或自学编译技术的实战参考。

1. 从一份 Python 写的 C 编译器源码包说起:LL1 文法怎么落地

很多人第一次接触编译原理,卡在“看得懂龙书、写不出 parser”这一步。这份c语言编译器(python版)的资源包,恰好是一个能跑通全流程的小型实现:词法分析、LL1 语法分析、四元式生成、汇编输出,一条链路全用 Python 串起来。它不追求支持完整 C 标准,而是把变量声明、函数定义、控制流这些核心结构用一套自定义文法规则跑通,适合两类人:一是正在做编译原理课设、想找一份能对照调试的参考实现;二是已经会写 Python、想借一个真实项目把 LL1、FIRST/FOLLOW 集、左递归消除这些概念从纸面落到代码里的人。包里main.py是入口,get_word.py、get_production.py、get_four.py、get_assembly.py分别对应词法、产生式、四元式、汇编几个阶段,结构清晰,改起来不费劲。

2. 拆开源码包:模块分工与 LL1 分析链路

2.1 各文件职责与调用顺序

拿到压缩包先别急着跑,把文件按职责分个类,后面调试会省很多事。核心文件大致是这么分工的:

文件职责输入输出
get_word.py词法分析,切分标识符/关键字/运算符源程序字符串单词符号串
get_production.py读取并解析文法产生式wenfa.txt产生式集合
first_fair_main.py计算 FIRST 集与 FOLLOW 集产生式集合预测分析表
get_four.py语法分析并生成四元式单词串 + 分析表four.txt
get_assembly.py四元式转汇编four.txtassembly.asm
main.py串起全流程的入口源程序各阶段产物

调用顺序是main.py→ 词法 → 文法加载 → FIRST/FOLLOW → 语法分析 → 四元式 → 汇编。理解这条链路,比逐行读代码更重要,因为出问题时你能快速定位是哪个阶段崩的。

2.2 LL1 分析表是怎么算出来的

LL1 的核心是预测分析表,而分析表依赖 FIRST 集和 FOLLOW 集。first_fair_main.py干的就是这件事。FIRST(A) 是 A 能推导出的所有串的首终结符集合;FOLLOW(A) 是在某个句型中紧跟在 A 后面的终结符集合。对产生式A -> α,填表规则是:对 FIRST(α) 中每个终结符 a,把A -> α填进 M[A, a];如果 α 能推出空串,就对 FOLLOW(A) 中每个 b 填 M[A, b]。

这里有个容易忽略的点:空串的处理直接决定了分析表会不会冲突。这份实现用 C 语言的空语句;来给左递归一个明确的结束标志,本质上是把“递归何时停”这件事交给了一个可见的终结符,而不是靠 ε 产生式隐式收尾。这个设计选择让分析表更干净,代价是文法要配合改写。

2.3 左递归消除与空语句的配合

左递归分直接和间接两种。直接左递归形如A -> Aα | β,标准消除法是改写成:

A -> βA' A' -> αA' | ε

但 ε 产生式在 LL1 里会让 FOLLOW 集参与填表,稍不注意就冲突。这份代码的思路是用空语句;替代部分 ε 的收尾作用,让A'在遇到;时明确结束,而不是靠“看下一个符号猜”。下面是我按这个思路整理的一段消除逻辑,方便对照理解:

# 消除直接左递归:A -> Aα | β 改写为 A -> βA' A' -> αA' | ; def eliminate_left_recursion(productions): new_prods = [] for head, bodies in productions.items(): recursive = [b for b in bodies if b and b[0] == head] normal = [b for b in bodies if not b or b[0] != head] if not recursive: new_prods.append((head, bodies)) continue new_head = head + "'" # A -> βA' new_prods.append((head, [b + [new_head] for b in normal])) # A' -> αA' | ; 用空语句作为结束标志 new_prods.append((new_head, [b[1:] + [new_head] for b in recursive] + [[';']])) return new_prods

逻辑说明:recursive收集所有以自身开头的产生式体,normal收集其余。改写后新非终结符A'的递归部分去掉首符号再接A',收尾用[';']而不是空列表。参数上要注意,b[1:]是去掉已经匹配的左递归首符号,如果你的文法里左递归符号不是单个 token,这里要按实际 token 数调整切片位置。

3. 跑通全流程:从源程序到汇编的实操步骤

3.1 环境准备与入口运行

这份代码是 Python 3 写的,包里带了__pycache__里 cpython-36 的字节码,说明作者当年用的是 3.6。现在用 3.8 到 3.11 基本都能跑,但要注意get_word.cpython-36.pyc这类缓存文件在新版本下会失效,Python 会自动重新编译,不用手动删。环境上不需要额外装库,标准库够用。

运行入口是main.py,但直接跑之前先确认几个输入文件在位:wenfa.txt(文法规则)、语句字符串.txt(待编译的源程序)、a.txt(可能是辅助数据)。我一般会先单独跑词法,确认切分没问题再往下走:

# 先单独验证词法分析,避免后面报错时定位困难 python get_word.py # 确认输出正常后,再跑完整流程 python main.py

逻辑说明:分阶段验证是调试编译器这类多阶段程序的习惯做法。词法错了,后面语法分析报的错全是假象。参数上,如果你的源程序文件名不是语句字符串.txt,要去main.py里改读取路径,别指望它自动找。

3.2 文法文件与产生式格式

wenfa.txt是整个编译器的规则来源,格式对不对直接决定 FIRST/FOLLOW 能不能算出来。常见格式是每行一条产生式,用->分隔左右部,多个候选式用|隔开:

program -> decl_list decl_list -> decl decl_list | ; decl -> type id ; type -> int | float

注意最后那条decl_list -> decl decl_list | ;,这里的;就是空语句收尾的体现。如果你的文法里用了 ε 符号,要确认get_production.py里有没有对应的解析分支,否则会被当成普通字符处理,导致 FIRST 集算错。我见过有人把 ε 写成epsilon又没改解析代码,结果分析表整片空,排查半天。

3.3 四元式与汇编输出验证

语法分析通过后,get_four.py会生成四元式,落到four.txt。四元式形如(op, arg1, arg2, result),比如(=, a, -, t1)表示把 a 赋给 t1。这一步是语义落地的关键,检查四元式比检查汇编容易得多,因为中间代码更接近源程序结构。

# 四元式生成的核心:遇到赋值语句时产出一条四元式 def gen_quad(op, arg1, arg2, result): quad = (op, arg1, arg2, result) quad_list.append(quad) return result # 示例:处理 a = b + c t1 = gen_quad('+', 'b', 'c', 't1') # 先算加法 gen_quad('=', 't1', '-', 'a') # 再赋值

逻辑说明:gen_quad把操作符、两个操作数和结果打包成元组追加到列表。参数上,arg2为-表示单目或赋值操作,这是四元式的常见约定。生成完four.txt后,get_assembly.py再把它翻译成assembly.asm。验证汇编是否正确,最直接的办法是看临时变量t1、t2有没有被正确分配寄存器或栈位置,以及控制流跳转标签有没有对上。

4. 避坑与排查:LL1 实现里最容易翻车的几处

4.1 分析表冲突却报“语法错误”

现象:源程序明明符合文法,语法分析却在中途报错退出。原因多半是 FIRST/FOLLOW 算错导致分析表某个格子为空或填了错的产生式。常见触发点是文法里有隐藏的左递归没消干净,或者某个非终结符的 FOLLOW 集漏了符号。解决:先把first_fair_main.py算出的 FIRST/FOLLOW 集打印出来,和手算结果对一遍,重点看能推出空串的非终结符,它们的 FOLLOW 集最容易漏。

4.2 空语句;被当成普通符号

现象:用;收尾的地方解析不通过,或者;被塞进了四元式。原因:词法分析阶段没把;识别为独立 token,或者文法里;的优先级没处理好。解决:在get_word.py里确认;在分隔符表里,且不会被并入前一个标识符。我一般会在词法输出里搜一遍;,确认它单独成项。

4.3 四元式临时变量命名冲突

现象:four.txt里出现两个t1,后面的赋值覆盖了前面的值。原因:临时变量计数器没有全局递增,或者在递归下降时被重置了。解决:把临时变量编号做成全局状态,每次生成新临时变量就自增,别在函数内部用局部变量计数。

4.4 汇编输出标签重复

现象:assembly.asm里两个跳转标签同名,汇编器报重复定义。原因:控制流语句(if/while)生成标签时用了固定名字,嵌套时撞车。解决:给标签加全局唯一编号,比如L1、L2递增,别用if_label这种固定串。

4.5 Python 版本导致的字节码不兼容

现象:删了__pycache__后运行报bad magic number。原因:残留的.pyc是 3.6 编译的,当前解释器版本不匹配。解决:直接删掉整个__pycache__目录,让 Python 重新生成。这不是代码问题,是环境问题,别去改源码。

5. 进阶玩法:把这份编译器改成你自己的实验平台

跑通只是第一步,这份代码真正的价值在于它足够小,小到你可以随便改。我一般会拿它做三件事。

第一件是加一条新文法规则,比如支持for循环。做法是在wenfa.txt里加产生式,然后在get_four.py里加对应的四元式生成分支。加之前先用 FIRST/FOLLOW 手算一遍,确认不会和分析表里已有的规则冲突。冲突了就得调整文法结构,这一步最能练对 LL1 边界的理解。

第二件是把四元式输出接到一个简单的解释器上,直接执行而不是转汇编。这样你就能绕开汇编器,快速验证语义是否正确。解释器核心就是一个循环,按四元式逐个执行,遇到跳转就改指令指针:

# 极简四元式解释器:按顺序执行,支持条件跳转 def run_quads(quads, env): pc = 0 while pc < len(quads): op, a1, a2, res = quads[pc] if op == '=': env[res] = env.get(a1, a1) elif op == '+': env[res] = env.get(a1, a1) + env.get(a2, a2) elif op == 'j': pc = int(res) # 无条件跳转 continue elif op == 'j<': if env.get(a1, a1) < env.get(a2, a2): pc = int(res) continue pc += 1 return env

逻辑说明:env是变量环境字典,pc是指令指针。跳转类四元式直接改pc并continue,避免末尾又自增。参数上,res存跳转目标行号,这要求你在生成四元式时就把标签解析成行号,而不是留到解释阶段。

第三件是对比不同文法写法的分析表大小。同一套语言,用;收尾和用 ε 收尾,算出来的 FIRST/FOLLOW 集和分析表规模不一样。把两种写法都跑一遍,打印分析表,你能直观看到空语句方案在哪些格子省了条目、在哪些格子引入了额外终结符。这种对比比看十页教材都管用。

从那以后我每次拿到一个 LL1 实现,都强制先打印 FIRST/FOLLOW 集和分析表再跑源程序,因为九成的“语法错误”其实错在表上,不在代码上。希望这份拆解能帮到你。

本文还有配套的精品资源,点击获取

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

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

立即咨询