东南大学编译原理实验:Java实现词法分析到目标代码生成全流程
简介这份资源是东南大学软件学院编译原理课程的实验项目压缩包面向正在学习编译原理、需要动手实现完整编译器流程的高校学生与自学者。它围绕词法分析、语法分析、语义分析、中间代码生成、目标代码生成与代码优化等核心环节构建了一个从源代码到可执行代码的模拟系统适合课程实验复现、编译流程梳理与工程能力训练。包内共28个文件以12个java源文件和12个class编译产物为主体另含2个txt说明、1个iml工程配置与1个md文档压缩包约20KB体量轻便便于快速导入IDE运行与阅读。目前已有63人学习下载。通过该平台读者可对照抽象语法树构建、语义检查、中间表示转换、指令选择与寄存器分配等关键知识点理解各阶段模块的职责划分与衔接方式并借助说明文档与工程结构梳理编译器的整体设计思路为课程实验验收和后续深入学习打下基础。1. 从东南大学软件学院编译原理实验项目说起一个完整编译器模拟系统到底要做什么很多同学第一次看到“东南大学软件学院编译原理课程实验项目”这个标题第一反应是“这不就是写个词法分析器交作业吗”。真动手才发现从源代码到可执行代码的完整链路远比想象中长。这个综合性实践平台要做的是把词法分析、语法分析、语义分析、中间代码生成、目标代码优化这几大模块串成一条能跑通的流水线输入一段类 C 或类 Pascal 的源程序输出可执行的汇编或机器码。它解决的核心问题是让你真正理解编译器每个阶段的数据结构怎么设计、模块之间怎么衔接、错误怎么定位。适合正在上编译原理实验课的学生也适合想系统补一遍编译全流程的后端工程师。热搜里“编译原理实验”“java编译原理”反复出现说明大家最关心的还是怎么用 Java 把这条链路落地。2. 词法分析与语法分析从字符流到语法树的落地路径2.1 词法分析器为什么不能只靠正则表达式硬扫词法分析的本质是把字符流切成有意义的 Token 序列。很多人第一版会写一堆 if-else 或者正则去匹配关键字、标识符、数字、运算符跑简单用例没问题一旦遇到ab这种连续运算符或者浮点数1.2e-3后面紧跟正则回溯就会翻车。常见做法是手写确定性有限自动机DFA用状态转移表驱动。状态表用二维数组或 Map 存行是状态列是输入字符类别。这样每个字符只扫一遍复杂度 O(n)而且错误定位精确到列号。下面是一个 Java 版词法分析器核心骨架用状态机处理标识符和数字public class Lexer { private final String src; private int pos 0; private int line 1; // Token 类型枚举ID, NUM, KEYWORD, OP, EOF public Token nextToken() { while (pos src.length()) { char c src.charAt(pos); if (Character.isWhitespace(c)) { if (c \n) line; pos; continue; } // 标识符或关键字字母开头后跟字母数字下划线 if (Character.isLetter(c) || c _) { int start pos; while (pos src.length() (Character.isLetterOrDigit(src.charAt(pos)) || src.charAt(pos) _)) { pos; } String word src.substring(start, pos); // 查关键字表命中则返回 KEYWORD否则 ID return KeywordTable.contains(word) ? new Token(TokenType.KEYWORD, word, line) : new Token(TokenType.ID, word, line); } // 数字支持整数和简单浮点 if (Character.isDigit(c)) { int start pos; while (pos src.length() Character.isDigit(src.charAt(pos))) pos; if (pos src.length() src.charAt(pos) .) { pos; while (pos src.length() Character.isDigit(src.charAt(pos))) pos; } return new Token(TokenType.NUM, src.substring(start, pos), line); } // 运算符单字符和双字符, !, , // 这里省略双字符前瞻逻辑实际需判断下一个字符 pos; return new Token(TokenType.OP, String.valueOf(c), line); } return new Token(TokenType.EOF, , line); } }逻辑说明pos是全局扫描指针line用于报错。标识符识别用贪心策略先吃满字母数字下划线再查关键字表避免把ifdef误判成if。数字部分只处理了整数和简单小数科学计数法需要额外状态。参数调整上关键字表建议用HashSetO(1) 查询如果语言大小写敏感查表前不要转小写。失败时看line和pos能直接定位到源文件哪一行哪一列。2.2 递归下降语法分析手写 LL(1) 的边界与调试技巧语法分析把 Token 流变成抽象语法树AST。实验项目里最常用的是递归下降因为代码直观、报错友好。每个非终结符对应一个函数函数内部按产生式右部依次匹配 Token。比如表达式文法E - T EE - T E | ε写成 Java 就是parseE()调parseT()再调parseEPrime()。递归下降的硬边界是左递归。如果文法写成E - E T | T直接递归会栈溢出。解决办法是改写文法消除左递归变成右递归形式。另一个坑是 FIRST 集和 FOLLOW 集没算对导致parseEPrime()在遇到)时不知道该不该返回。我一般会在每个 parse 函数入口打印当前 Token 和函数名跑一个ab*c的用例看调用栈是否符合预期。// 消除左递归后的表达式解析 private ASTNode parseE() { ASTNode left parseT(); return parseEPrime(left); } private ASTNode parseEPrime(ASTNode left) { Token t peek(); if (t.type TokenType.OP t.value.equals()) { consume(); ASTNode right parseT(); BinaryNode node new BinaryNode(, left, right); return parseEPrime(node); // 右递归继续找下一个 } return left; // ε 产生式直接返回 }逻辑说明peek()看当前 Token 不消费consume()消费并前进。parseEPrime收到就构造左结合的二叉树然后递归处理后续。参数上peek()需要支持向前看一个 Token实验里通常用ListToken加索引实现。如果遇到abc会构造((ab)c)符合左结合语义。失败时检查peek()返回的 Token 是否在预期集合内不在就抛语法错误并带上行号。3. 语义分析与中间代码生成类型检查、符号表和四元式3.1 符号表设计作用域嵌套时怎么查、怎么插、怎么删语义分析的核心是符号表。每个变量、函数、类型都要登记还要处理作用域嵌套。常见做法是用栈式符号表进入一个作用域就压入一个新 HashMap离开就弹出。查找时从栈顶往下找找到第一个匹配就返回。这样内层变量可以遮蔽外层同名变量符合大多数语言语义。public class SymbolTable { private DequeMapString, Symbol scopes new ArrayDeque(); public SymbolTable() { scopes.push(new HashMap()); // 全局作用域 } public void enterScope() { scopes.push(new HashMap()); } public void exitScope() { scopes.pop(); } public void define(String name, Symbol sym) { scopes.peek().put(name, sym); // 只在当前作用域插入 } public Symbol resolve(String name) { for (MapString, Symbol scope : scopes) { if (scope.containsKey(name)) return scope.get(name); } return null; // 未声明 } }逻辑说明Deque当栈用push压入新作用域pop弹出。define只写栈顶保证同名变量在内层重新定义不会覆盖外层。resolve从栈顶往下遍历实现“最近声明优先”。参数上Symbol类至少存类型、种类变量/函数/类型、行号。如果语言支持函数重载define的 key 要改成name 参数类型列表。失败时看resolve返回 null 的位置就是未声明标识符。3.2 四元式生成把 AST 翻译成中间代码的固定套路中间代码生成通常用四元式(op, arg1, arg2, result)。遍历 AST 时遇到二元运算就生成一条四元式把结果存到临时变量。比如a b c * d先生成(*, c, d, t1)再生成(, b, t1, t2)最后生成(, t2, _, a)。临时变量用t1, t2...递增命名。public class QuadGenerator { private int tempCount 0; private ListQuad quads new ArrayList(); public String newTemp() { return t (tempCount); } public String genBinary(String op, String left, String right) { String temp newTemp(); quads.add(new Quad(op, left, right, temp)); return temp; } public void genAssign(String target, String value) { quads.add(new Quad(, value, _, target)); } // 遍历 AST 的入口 public String visit(ASTNode node) { if (node instanceof BinaryNode) { BinaryNode bin (BinaryNode) node; String l visit(bin.left); String r visit(bin.right); return genBinary(bin.op, l, r); } if (node instanceof IdNode) { return ((IdNode) node).name; // 变量直接返回名字 } if (node instanceof NumNode) { return ((NumNode) node).value; // 常量直接返回字面量 } throw new RuntimeException(未知节点类型); } }逻辑说明visit是递归入口二元节点先递归左右子树拿到操作数再生成四元式并返回临时变量名。genAssign处理赋值语句把右值四元式结果写到左值。参数上tempCount全局递增保证临时变量不重名。如果要做常量折叠优化可以在visit里判断左右都是NumNode时直接算出结果不生成四元式。失败时看四元式列表哪条的操作数没定义就是语义错误。4. 目标代码优化与生成从四元式到可执行汇编的最后一公里4.1 基本块划分与局部优化删冗余、合并常量拿到四元式列表后先划分基本块。基本块是顺序执行、无跳转进入、无跳转退出的最大指令序列。划分规则遇到标号、跳转目标、跳转指令后的下一条就切新块。然后在每个块内做局部优化常见的有常量传播、公共子表达式消除、死代码删除。// 常量传播如果四元式两个操作数都是常量直接算出结果 for (int i 0; i quads.size(); i) { Quad q quads.get(i); if (isConstant(q.arg1) isConstant(q.arg2) !q.op.equals()) { String folded eval(q.op, q.arg1, q.arg2); // 把这条替换成赋值后续用到 result 的地方替换成 folded quads.set(i, new Quad(, folded, _, q.result)); replaceAll(q.result, folded); } }逻辑说明isConstant判断操作数是否是数字字面量。eval按运算符算出结果。replaceAll把后续四元式中所有引用q.result的地方替换成折叠后的常量。参数上常量折叠只对算术和逻辑运算有效涉及变量或函数调用不能折。失败时看优化后四元式数量是否减少没减少说明没命中常量对。4.2 目标代码生成寄存器分配与指令选择的最小实现最后一步是把优化后的四元式翻译成汇编。实验项目通常选 MIPS 或 x86 子集。最简单的方法是“一条四元式对应几条汇编”用栈式分配所有临时变量都放内存需要运算时 load 到寄存器算完 store 回去。虽然效率低但正确性容易保证。// 四元式 (, a, b, t1) 生成 MIPS 汇编 // lw $t0, a // lw $t1, b // add $t2, $t0, $t1 // sw $t2, t1 for (Quad q : quads) { if (q.op.equals()) { emit(lw $t0, q.arg1); emit(lw $t1, q.arg2); emit(add $t2, $t0, $t1); emit(sw $t2, q.result); } // 其他运算符类似减法是 sub乘法是 mul }逻辑说明emit把汇编指令追加到输出列表。每个四元式独立翻译不跨指令复用寄存器所以不会冲突。参数上变量名直接当内存标签用临时变量也在数据段分配空间。如果要做寄存器分配优化可以引入活跃变量分析把不冲突的变量分到同一寄存器。失败时看汇编里有没有未定义的标签或者sw的目标地址是否合法。5. 避坑与排查编译原理实验里最容易翻车的 5 个地方5.1 词法分析把关键字识别成标识符现象if、while被当成普通变量名语法分析报“期望表达式但遇到 ID”。原因关键字表没建或者查表时大小写没统一。解决在词法分析器初始化时把所有关键字塞进HashSet识别完标识符后先查表再返回 Token 类型。5.2 语法分析左递归导致栈溢出现象解析abcd...长表达式时抛StackOverflowError。原因文法写成E - E T递归下降直接左递归。解决改写成E - T EE - T E | ε把递归转成循环或右递归。5.3 符号表作用域没弹出导致变量泄漏现象内层块定义的变量在外层也能访问或者同名变量覆盖了外层。原因进入作用域没push离开没pop。解决在语法分析进入{}块时调enterScope()退出时调exitScope()确保成对出现。5.4 四元式临时变量重名现象优化后计算结果错乱两个不同表达式的临时变量都叫t1。原因tempCount是局部变量每次生成四元式都重置。解决把tempCount提成类成员变量全局递增或者用t1_1、t1_2带作用域后缀。5.5 目标代码生成寄存器冲突现象汇编跑出来结果不对某个变量值被覆盖。原因多条四元式复用同一个寄存器$t0但没保存前一个值。解决最简单的办法是每条四元式都重新 load不跨指令复用寄存器进阶做法是做活跃变量分析只在不同时活跃的变量间复用。6. 进阶技巧用测试用例反推编译器正确性写完编译器最怕的是“看起来能跑一换用例就崩”。我一般会准备三组测试第一组是正常程序覆盖赋值、算术、条件、循环、函数调用第二组是边界用例比如空语句、嵌套十层的括号、超长标识符第三组是错误用例故意写未声明变量、类型不匹配、缺少分号看报错行号对不对。验证方法上可以用“差分测试”同一个源程序分别用你的编译器和 GCC 编译跑出来结果对比。如果结果不一致先查中间代码再查目标代码。另一个技巧是给每个阶段加 dump 开关比如-dump-tokens、-dump-ast、-dump-quads出问题时逐层往下看很快能定位到哪个模块翻车。# 假设编译器主类叫 Compiler java Compiler -dump-tokens test.c # 只看词法 java Compiler -dump-ast test.c # 看语法树 java Compiler -dump-quads test.c # 看四元式 java Compiler test.c -o test.s # 生成汇编参数说明-dump-*是调试开关正式编译时不加。-o指定输出文件。如果汇编生成后还要汇编和链接可以用gcc test.s -o test跑最终可执行文件。我自己的习惯是每改一个模块先把对应 dump 打开跑一遍最小用例确认输出符合预期再往下走。编译原理实验最忌讳一口气写完再调分层验证能省下大量后悔药。希望帮到你。本文还有配套的精品资源点击获取

相关新闻

TM4C129LNCZAD + PCA9422:电池供电设备的PMIC电源管理设计实践

TM4C129LNCZAD + PCA9422:电池供电设备的PMIC电源管理设计实践

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/10 4:52:21 阅读更多 →
SegNet图像分割实战:PyTorch源码详解与调优指南

SegNet图像分割实战:PyTorch源码详解与调优指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/10 4:52:21 阅读更多 →
Skywalking 9.4实战:SpringBoot无侵入接入分布式链路追踪

Skywalking 9.4实战:SpringBoot无侵入接入分布式链路追踪

聊分布式系统的可观测性,Skywalking是我落地链路追踪时经常会用到的开源APM项目。它最吸引人的地方在于JavaAgent无侵入接入,SpringBoot应用加一个启动参数就能看到完整的服务调用拓扑,这个优势让很多团队从其他方案迁过来。这篇文章围绕Skyw…

2026/10/10 4:51:21 阅读更多 →

最新新闻

编程智能体插件开发实战:余额监控、任务面板与番茄钟

编程智能体插件开发实战:余额监控、任务面板与番茄钟

1. 为什么我要给编程智能体做插件1.1 一个让我抓狂的日常场景用编程智能体写代码这件事,现在已经成了我日常开发的主线。不管是让它帮我重构一个模块、生成单元测试,还是排查一段诡异的异步逻辑,我基本都靠它。但用得越久,一个很别…

2026/10/10 5:32:35 阅读更多 →
TaskWeaver 核心数据概念解析:Post 消息在角色通信、附件管理与提示词生命周期中的设计

TaskWeaver 核心数据概念解析:Post 消息在角色通信、附件管理与提示词生命周期中的设计

AI Agent数据分析 【免费下载链接】TaskWeaver The first "code-first" agent framework for seamlessly planning and executing data analytics tasks. 项目地址: https://gitcode.com/gh_mirrors/ta/TaskWeaver 点击查看 免费下载 Post(消…

2026/10/10 5:32:35 阅读更多 →
devdocs 过滤器体系完全指南:从 HTML::Pipeline 到自定义 Filter 的编写实践

devdocs 过滤器体系完全指南:从 HTML::Pipeline 到自定义 Filter 的编写实践

文档开发工具后端前端 【免费下载链接】devdocs API Documentation Browser 项目地址: https://gitcode.com/GitHub_Trending/de/devdocs 点击查看 免费下载 导读 本指南围绕 devdocs 项目的 docs/filter-reference.md 展开,系统讲解项目核心的过滤器&…

2026/10/10 5:32:35 阅读更多 →
WeChatBot_WXAUTO_SE最出圈的功能:角色论坛全解析,让你的AI偶像自己发帖

WeChatBot_WXAUTO_SE最出圈的功能:角色论坛全解析,让你的AI偶像自己发帖

WeChatBot_WXAUTO_SE最出圈的功能:角色论坛全解析,让你的AI偶像自己发帖 【免费下载链接】WeChatBot_WXAUTO_SE 【停止维护】LLM API 聚合对话工具。原项目仓库:https://github.com/KouriChat/KouriChat 本项目由iwyxdxl在原项目基础上修改创…

2026/10/10 5:32:35 阅读更多 →
Android点餐App全栈源码:数据库+后台管理+人脸识别支付闭环

Android点餐App全栈源码:数据库+后台管理+人脸识别支付闭环

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/10 5:32:35 阅读更多 →
frp内网穿透实战:原理、配置与常见问题排查

frp内网穿透实战:原理、配置与常见问题排查

这几年我帮朋友和自己折腾过好几套内网穿透方案,从最初图省事用 ngrok,到后来在真实服务器上长期跑 frp,来回对比过不少次。frp 是一个完全开源的轻量级工具,用 Go 编写,服务端和客户端就是两个二进制文件,…

2026/10/10 5:31:35 阅读更多 →

日新闻

卫星轨道分类全解析:从LEO到GEO的选型逻辑与工程实践

卫星轨道分类全解析:从LEO到GEO的选型逻辑与工程实践

1. 从“卫星轨道分类”这个标题说起:为什么值得花时间搞懂第一次接触“卫星轨道分类”这个概念,很多人会觉得它离自己很远——不就是天上的星星怎么转吗?但如果你正在做航天任务规划、遥感数据接收、星座设计,甚至只是准备一场航天…

2026/10/10 0:00:39 阅读更多 →
Spring AOP 核心原理与实战:从概念到日志切面落地

Spring AOP 核心原理与实战:从概念到日志切面落地

1. 从一个真实痛点说起:为什么你的代码里到处都是重复逻辑刚入行那会儿,我写过一个用户管理模块,注册、登录、改密码、注销四个接口。每个接口里都塞了几乎一样的日志打印、参数校验、事务开启和提交。当时觉得没什么,能跑就行。直…

2026/10/10 0:00:40 阅读更多 →
Python招聘数据采集与分析可视化:从采集清洗到薪资技能城市可视化全链路

Python招聘数据采集与分析可视化:从采集清洗到薪资技能城市可视化全链路

简介:这是一套面向计算机相关专业学生与项目实战学习者的Python数据采集与分析可视化完整项目,以Boss直聘岗位数据为对象,适合用作毕业设计、课程设计或期末大作业。资源包共38个文件,约246KB,以13个py源码文件为核心&…

2026/10/10 0:00:40 阅读更多 →

周新闻

KT148A语音芯片外挂8002D功放的工程实践指南

KT148A语音芯片外挂8002D功放的工程实践指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/8 15:26:32 阅读更多 →
LLC谐振变换器增益公式推导:从FHA等效到完整归一化表达式

LLC谐振变换器增益公式推导:从FHA等效到完整归一化表达式

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/10 1:36:08 阅读更多 →
ARM架构深度解析:从RISC设计理念到交叉编译实战

ARM架构深度解析:从RISC设计理念到交叉编译实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/9 10:11:06 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/10 5:23:50 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/9 21:32:20 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/9 6:17:20 阅读更多 →