
简介这是一份用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.txtget_assembly.py四元式转汇编four.txtassembly.asmmain.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 集和分析表再跑源程序因为九成的“语法错误”其实错在表上不在代码上。希望这份拆解能帮到你。本文还有配套的精品资源点击获取