编译原理课程设计:从词法分析到中间代码生成的完整实践
发布时间:2026/10/2 5:31:35来源:尧图网络
简介《编译原理》课程设计报告是一份面向计算机专业学生的完整课程设计文档围绕编译器构建的核心流程展开系统覆盖词法分析、语法分析、语义分析、中间代码生成、代码优化及目标代码生成等关键阶段适合需要参考课程设计写法或独立完成编译器项目的学习者也可用于毕业设计与项目实践参考。报告基于重庆理工大学课程设计要求从语言规范定义出发依次记录了词法分析器与语法分析器的设计、语法树构建、类型检查、中间代码表示与优化、目标代码生成等实现细节并附有示例程序演示完整编译过程同时整理了设计思路、实现过程、常见问题与解决方案能够帮助读者快速理解编译器各模块的落地方法也可作为课程设计报告的结构蓝本内容还包含课程设计目的、内容要求、成果展示、评估标准与参考资料等完整框架便于对照检查自身设计是否完备。压缩包以zip格式提供大小约4.52MB由于平台未提供文件总数与类型明细此处不逐一罗列目前已有140人浏览/学习尤其适合正在准备编译原理课程设计或希望深入理解编译器工作流程的学生学习参考。1. 编译原理课程设计从理论推导到能跑通的编译器骨架编译原理课最“劝退”的一关不是闭卷考试而是课程设计。平时做题只要会算 FIRST 集、会画 DFA 就够了课设要求把这些零散知识点组装成一个能编译迷你语言的程序。这份《编译原理》课程设计报告就是我按清华大学出版社《编译原理第3版》的知识点路线用 Java 实现的一整套编译器骨架词法分析、语法分析、语义分析、中间代码生成每一层都有可运行的代码和对应的测试用例。适用人群很明确正在被课设折磨的本科生、想动手把教材例题变成代码的从业者以及需要参考实验案例的教师。它能直接给你一个从 0 到 1 的框架你只需要替换文法、扩充关键字就能接住大多数学校的课程设计题目。2. 词法分析器token 种别设计与状态转移驱动的识别程序2.1 token 种别编码与接口约定词法分析是全流程的地基。课设要求的输入通常是一个源代码文本文件期望的输出是 token 序列加错误报告。做这一步之前先把种别编码表定下来下面这份是报告里常用的种别定义直接用能省掉大半设计时间。种别token 类型示例正规式描述1IDENTcount, a1[a-zA-Z_][a-zA-Z0-9_]*2INT_CONST123, 0[0-9]3KW_INTint保留字4KW_IF / KW_ELSE / KW_WHILEif else while保留字5PLUS / MINUS -单字符运算符6ASSIGN / EQ 单字符与双字符操作符7LPAREN / RPAREN( )界符种别码怎么排有讲究保留字紧挨着标识符但编码本身不参与算法只是输出数据的规范。真正参与识别的是后面要讲的最大匹配逻辑。选型上我建议用 Java 的枚举做 TokenType 而不是 int 常量因为课设的代码量不大枚举的可读性对后面写报告更友好。词法这一层的接口我一般定义成Token nextToken()Token 里带 type、lexeme、line 三个字段遇到文件结尾返回 EOF token。很多学校的编译原理实验都要求第一阶段交可打印的 token 流这个接口可以直接对接到主函数里循环输出。2.2 状态转移驱动的词法分析程序我见过不少同学的写法一个 switch 分支套一个 switch 分支每个字符单独判定。这种代码改一个关键字就要动五六处而且很难讲清楚和 DFA 的关系。常见做法是维护一个状态变量按字符类别跳到下一个状态遇到终态就返回 token。下面这段是标识符与关键字的识别代码核心是“先拼完整词、再查关键字表”。public Token nextToken() { skipWhitespace(); if (pos source.length()) { return new Token(TokenType.EOF, , line); } char c source.charAt(pos); if (isLetter(c) || c _) { return readIdOrKeyword(); // 标识符与保留字共用一条路 } if (isDigit(c)) { return readNumber(); // 无符号整数 } // 运算符部分注意 这种双字符 token 要向前多看一位 if (c ) { if (pos 1 source.length() source.charAt(pos 1) ) { pos 2; return new Token(TokenType.EQ, , line); } pos; return new Token(TokenType.ASSIGN, , line); } return new Token(TokenType.ERROR, unexpected char: c, line); } private Token readIdOrKeyword() { int start pos; while (pos source.length() isLetterOrDigit(source.charAt(pos))) { pos; } String word source.substring(start, pos); TokenType type KEYWORDS.get(word); return new Token(type null ? TokenType.IDENT : type, word, line); }为什么要先拼完整词再查表不能读到一半就判断因为保留字是标识符的子集词法规则遵循最大匹配读到whil时并不会知道后面还有字母e。如果边读边判等发现是while时位置已经回不去了。这个顺序问题是词法实验里最常见的翻车原因。数字识别同理readNumber会一直吃到非数字字符为止这样123abc会拆成整数 123 和标识符 abc而不是直接报错——这符合多数 C 语言编译器的词法行为。2.3 关键字表与手写 DFA 的取舍有一种更理论化的做法是把所有 token 的正规式都转换成 DFA然后用一张状态转移表驱动扫描。我在报告里也画了 DFA 图但代码里没有用转移表原因是转移表对课程设计来说是黑匣子一旦状态号写错调试成本高于收益。我一般这样处理标识符、数字这类结构简单的用函数直接写双字符运算符用 peek 一位特判剩下的单字符直接用枚举跳转。这样老师在答辩时问“DFA 怎么体现在代码里”你可以指着状态变量和分支说清楚比甩一张几十行的转移表更有说服力。测试词法模块时我习惯写一个极小的 main把int a 10; if (a 0) a a - 1;喂进去看输出的 token 流是否逐行对应。输出里必须能看到每个 token 的行号这是文末报告截图里最有说服力的素材。2.4 词法错误与行号追踪词法错误最典型的场景是出现非法字符比如中文标点、、$。处理策略是跳过该字符、记录错误、继续扫描行号在跳过换行时递增。输出格式统一为第X行: 错误描述这个格式后面语法分析也会复用。private void skipWhitespace() { while (pos source.length()) { char c source.charAt(pos); if (c \n) { line; pos; } else if (c || c \t || c \r) { pos; } else break; } }这段代码虽小但容易漏换行符必须在这里处理否则词法正确但行号全部错位。我在报告里专门用这个例子说明“词法状态与行号维护是同一件事不是两个循环”。行号一旦错位语法分析的报错信息全部作废越往后调越乱。3. 语法分析LL(1) 预测表与递归下降子程序的落地实现3.1 文法改写与优先级保持语法分析的第一步是把产生式整理成 LL(1) 能处理的形式。课程设计一般用表达式文法做主体比如加减乘除和括号。原始文法天然带左递归直接写递归下降必然爆栈。我用的文法如下这里注意优先级是靠T和E的层级来保证的原文法含左递归改写后LL(1)E → E T | E - T | TE → T ET → T * F | T / F | FT → F TF → (E) | id | numE → T E | - T E | εT → * F T | / F T | ε改写一旦出错最典型的现象是加减法变成右结合8 - 3 - 2会算出7。原因是E被写成了递归下降而不是循环每层递归都优先吃掉剩余部分。正确实现是用循环来消化同层运算符下面的代码把 while 写在 parseEPrime 里把所有 T和- T看成同一层。private void parseE() { parseT(); parseEPrime(); } private void parseEPrime() { while (curToken TokenType.PLUS || curToken TokenType.MINUS) { Token op curToken; match(curToken); parseT(); // 在这里生成中间代码见第 4 章 } }这段逻辑说明因为E的定义是 T E | - T E从文法上看最后的E会收在 ε 上递归写法天然右结合改成 while 循环后运算符左边的操作数已经完成运算结果继续参与下一次循环这就恢复了左结合。语法分析的代码不是唯一答案但语义上左结合和右结合的区别必须能说清楚答辩老师很爱问这个点。3.2 FIRST 集与 FOLLOW 集的程序化计算预测分析表的构造需要 FIRST 和 FOLLOW手工算小文法没问题稍一扩充就出错。我建议把计算写成独立的方法反复扫描产生式直到集合不再变化也就是不动点迭代。下面是一个可运行的 FIRST 集计算方法递归实现时注意加记忆化防止循环文法导致死递归。public SetString first(Symbol s) { if (s.isTerminal()) { return new HashSet(Collections.singletonList(s.name())); } if (memo.containsKey(s)) { return memo.get(s); } SetString result new HashSet(); memo.put(s, result); // 先占位防止间接递归死循环 for (ListSymbol rhs : productionsOf(s)) { if (rhs.isEmpty()) { result.add(eps); continue; } for (Symbol sym : rhs) { SetString fs first(sym); result.addAll(fs); if (!fs.contains(eps)) { break; // 遇到不能推空的符号就停下 } // 如果 sym 可以推空继续看下一个符号 } } return result; }这里两个关键点一是针对形如A → B、B → A的间接递归必须先往 memo 里放一个占位集合否则会抛栈溢出二是break的条件写错会把不该进 FIRST 的终结符混进来。FOLLOW 集同理只是规则里多一条“若 β 能推出 ε就把 FOLLOW(A) 并入 FOLLOW(B)”并且初始要把#结束符加入开始符号的 FOLLOW。程序化计算时把所有非终结符的集合打印出来和手工推导比对一次最多五分钟就能定位哪一步算错。3.3 预测分析表与递归下降的映射LL(1) 分析表在代码里怎么体现一种做法是构造二维表代码里查询另一种是分析表“隐含”在递归下降的子程序里。课程设计我推荐后者每个非终结符一个方法产生式的选择就是 if/while 的分支表结构仅作为报告验证材料。下面给出这个课设的映射关系表写报告时可以直接引用。非终结符进入条件预测表依据对应方法E当前 token 是 id、num、(parseE()E、- 进入循环)、# 直接返回parseEPrime()Tid、num、(parseT()Fid / num 匹配字面量( 进入括号子程序parseF()这里的规则解释当E遇到id时选择E → T E接着T遇到id选T → F TF直接匹配id。整个链路就是一次方法调用栈出错时按栈往回退天然自带分析树的痕迹打印起来也方便第 6 章会讲到怎么利用这点。4. 语义分析与中间代码符号表、类型检查与四元式生成4.1 符号表的作用域设计很多课设只做到语法分析就收工但作为完整报告语义分析才是工作量所在。符号表要解决三件事声明去重、使用查找、作用域退出清理。我用的结构是链式栈每个作用域一个 Map进入复合语句时压栈退出时弹栈查找从栈顶往下逐层找这正好对应 C 语言的作用域规则。DequeMapString, Symbol scopeStack new ArrayDeque(); public void declare(String name, DataType type, int line) { MapString, Symbol top scopeStack.peek(); if (top.containsKey(name)) { errors.add(line line : redefinition of name); return; } top.put(name, new Symbol(name, type, line)); } public Symbol lookup(String name) { for (MapString, Symbol scope : scopeStack) { Symbol s scope.get(name); if (s ! null) return s; } return null; }链式栈比单表加销毁标记更直观声明时只在栈顶检查重名使用时从内向外找这不光是代码组织问题也是报告里“作用域管理”一节的论述依据。数据类型的检查在声明阶段就能做一部分比如int a b c如果 b 或 c 没声明lookup 返回 null就报undeclared variable。这里有个细节要提前决定变量是否允许在使用后声明课程设计我统一按“先声明后使用”处理规则简单报告也好写。4.2 表达式求值与四元式生成中间代码生成采用四元式(op, arg1, arg2, result)存储在一个全局的 ArrayList 里。表达式的翻译用一遍扫描直接生成不用显式建 AST因为递归下降的调用顺序本身就是语法树的后续遍历。下面这段是二元运算的翻译代码每个子表达式都会返回一个操作数可能是常量名、变量名或临时变量名。public String translateExpr() { if (curToken TokenType.INT_CONST) { String v String.valueOf(curToken.value); match(curToken); return v; } if (curToken TokenType.IDENT) { Symbol s lookup(curToken.lexeme); if (s null) { error(undeclared variable: curToken.lexeme); } String v curToken.lexeme; match(curToken); return v; } // 处理二元运算左右操作数先翻译再合并成一个临时变量 String left translateExpr(); Token op curToken; match(curToken); String right translateExpr(); String temp newTemp(); quads.add(new Quad(op.lexeme, left, right, temp)); return temp; }临时变量命名从 t0 开始递增属于当前编译单元的全局计数。注意这里的一个小坑操作数顺序不能反四元式(-, a, b, t)和(-, b, a, t)的语义完全不同而递归下降翻译减法时左操作数是在读取减号之前拿到的顺序天然正确只要你不在代码里做任何栈内元素的交换。以a 3 4 * 5;为例生成的四元式如下(*, 4, 5, t0) (, 3, t0, t1) (, t1, , a)第一行先算乘法第二行再算加法第三行把结果赋给 a。可以看出四元式列表就是运算顺序的直接投影拿它和课本上的 DAG 图对照一眼就能看出翻译是否正确。4.3 控制流语句的回填if 和 while 的翻译绕不开回填技术。所谓回填是先把跳转四元式的目标地址留空等真正的目标位置明确后再填充。我在报告里用一段带注释的代码演示 if 语句的处理这是课设答辩的高频考点。public void translateIf() { match(TokenType.KW_IF); match(TokenType.LPAREN); String cond translateExpr(); // 条件表达式的值 match(TokenType.RPAREN); int jumpLFalse quads.size(); // 记录“假跳”位置 quads.add(new Quad(jf, cond, , )); // 目标未定先占位 translateBlock(); // then 部分 int jumpLEnd quads.size(); // 记录“跳结尾”位置 quads.add(new Quad(j, , , )); // then 结束后跳过 else quads.get(jumpLFalse).setResult(String.valueOf(quads.size())); // 回填假跳目标 if (curToken TokenType.KW_ELSE) { match(TokenType.KW_ELSE); translateBlock(); } quads.get(jumpLEnd).setResult(String.valueOf(quads.size())); }回填要特别注意跳转目标填的是四元式的下标也就是quads.size()而不是某个符号名。很多人在这里填了 label 字符串最后生成的中间代码在解释器里根本跑不动。四元式列表下标从 0 开始回填的值是“下一条将要生成的四元式的位置”这个位置就是 then 分支执行完、else 分支开始的地方。while 语句走同一套逻辑只是多了一个循环开始标记生成j回跳到循环头。回填这块代码错位了调试起来一半是技术一半是玄学因为打印出来的四元式看起来全都像是对的。5. 课程设计避坑实录六个高频翻车点与排查方法5.1 词法层的两个翻车点踩坑一关键字被识别成标识符语法分析一启动就报错。现象输入int a;词法输出(标识符, int)和(标识符, a)语法分析直接说int不符合文法。 原因关键字表没有在类加载时初始化或者关键字表里存的是Int而代码扫描出的是int大小写对不上。 解决把KEYWORDS定义为static final并在静态块里全部填好同时写一个单元测试断言KEYWORDS.get(int)不为空词法主循环里务必先拼完整个字母序列再查表不要边读边查。踩坑二整个 token 流的行号全部错位排错无从下手。现象报错信息的行号比实际位置大一行或者小一行查了很久发现是注释里的换行没计数。 原因行号自增逻辑写在了主循环末尾而skipWhitespace()跳过换行后主循环已经看不到\n了。 解决把line移到skipWhitespace()内部只要遇到\n就递增主循环里不要再单独处理换行。这里最容易漏的是块注释内部的换行处理块注释时也要带着行号计数。5.2 文法与集合计算的翻车点踩坑三左递归没消除递归下降运行到第二个表达式就 StackOverflow。现象程序一执行就抛栈溢出栈顶是parseE重复出现。 原因用的是E → E T这样的原文法递归下降每一层都要先调parseE()等于无限递归。 解决把文法改写为E → T E、E → T E | ε并且parseEPrime用 while 循环消化同层运算符。注意转写后加减法仍然是左结合因为循环里每次都是拿左边已算完的结果和新的T运算。不能只改文法不改进循环两者必须配套。踩坑四FOLLOW 集算着算着就不收敛预测表里一个格子塞了多条产生式。现象用程序检查预测分析表时发现E的#列有两个产生式。 原因FOLLOW 计算时把FIRST(β)里的 ε 也并进去了或者没有做不动点扫描只遍历了一遍产生式就以为算完了。 解决给 FOLLOW 加一个while(changed)的外层循环每次集合发生变化就重扫所有产生式另外在合并时先临时取firstBeta副本去掉 ε 再并给 FOLLOW(B)。程序化计算 FIRST 和 FOLLOW 时把所有非终结符的集合打印出来和手工推导比对一次最多五分钟就能定位。5.3 错误恢复与中间代码回填的翻车点踩坑五一次性报几百个语法错误实际只有一处词法错。现象源代码少了一个分号错误输出列表里跟着来了一大串expected token。 原因没有设计同步记号语法分析出错后继续死磕当前 token导致雪崩效应。 解决在递归下降每个方法的开头判断当前 token 是否属于该层的 FOLLOW 集成员出错时先skipTo(分号或右括号)然后返回上级。错误计数只加一次后续的错误定位会明显变准。同步记号不需要多复杂选分号和右括号作为同步点就够了LL(1) 的 FOLLOW 集本身就是现成的同步参考。踩坑六if 语句生成的跳转目标全是空字符串解释器跑不动。现象四元式列表里jf和j的 result 字段是空串。 原因回填时用了未初始化的 label 变量或者跳转位置记录错了四元式下标。 解决跳转目标一律用整数下标表示四元式全都存放在同一个 ArrayList 中先记录占位下标再等目标确定后执行get(idx).setResult(String.valueOf(quads.size()))。每生成一个完整 if/while 块后打印出整个四元式列表检查一遍重点看回填后的下标是否落在正确的四元式上。6. 验证方法与调试技巧测试用例三层设计与三个打印开关6.1 测试用例三层设计课设报告里最容易被老师挑刺的就是测试用例不完整。常规做法是用三类用例覆盖正向验证功能边界验证鲁棒性错误验证报错恢复。下面这张表可以直接挪进文档的测试章节。测试层级典型用例预期输出正向a 3 4 * 5;四元式中先乘后加边界空程序、只含一个分号、连续负号a - - 3;不崩溃有空 token 或语法错误错误int a ;、未声明变量、a b c;错误信息带行号且不再雪崩6.2 三个打印开关与答辩材料组织词法、语法、中间代码三个环节各留一个打印开关这是调试性价比最高的手段。词法层打印 token 流语法层打印递归下降的进出栈中间代码层打印四元式列表。比如递归下降方法里加一个 indent 参数每进入一个方法就打印退出就打退格能和课本上的语法树对应起来。private void parseE(String indent) { System.out.println(indent enter parseE, token curToken); parseT(indent ); parseEPrime(indent ); System.out.println(indent leave parseE); }对着输出观察如果8 - 3 - 2的打印显示出先进入右边的parseT再考虑左结合基本可以断定结合性写错了。答辩时老师问的通常就三件事文法为什么这样改写、FIRST/FOLLOW 怎么算、回填怎么实现这三处在报告里都要有能指到代码的具体段落。每完成一个模块就写对应章节词法那章贴 token 定义表和识别流程语法那章贴改写后的文法和预测表中间代码章节贴四元式示例。测试用例全部截图存档尤其是错误用例的输出能很好证明你的错误处理不是摆设。如果手里有清华社《编译原理第3版》的课后习题和参考答案拿答案里给的中间代码序列和你的四元式逐条比对很快能发现哪一步推错了。从那以后我每次写完一个模块都强制自己先跑一遍最小的正向用例再跑一个错误用例最后才跑完整测试集。这份文档和代码我整理在下载页了需要的同学直接拿去对照省得从空白的 main 函数开始憋。希望帮到你。本文还有配套的精品资源点击获取
网站建设高端定制企业官网