编译原理课设实战:用Java从零搭建迷你编译器
简介重庆理工大学《编译原理》课程设计报告面向计算机科学与技术专业学生及编译器入门开发者适合用于课程设计选题、整体规划、阶段划分与报告撰写参考。资源以zip压缩包形式提供大小约4.52MB便于下载后查阅或打印目前已有140人浏览学习。内容围绕词法分析、语法分析、语义分析、中间代码生成、代码优化与目标代码生成等完整的编译器构建流程展开详细涵盖简单程序语言规范设计、词法单元识别、语法结构树构建、类型错误与声明前使用等语义检查以及三地址中间代码表示与优化处理等关键环节同时给出了lex/yacc等自动工具与手动实现的两种思路并记录了设计思路、实现过程、遇到的问题和解决方案。整体结构清晰可直接作为课程设计报告写作蓝本帮助读者梳理编译器设计脉络对正在完成同类课程设计或希望系统理解编译原理实际应用的读者具有较好的参考价值。1. 课设不是写报告先让编译器跑起来再谈那一万字一个编译原理课程设计真正拉开差距的从来不是报告页数而是代码能不能当场跑通。凌晨三点词法分析还卡在括号匹配上报告一字未动这种场景每个做过课设的人都不陌生。下面要讲的这套方案是把一个类 C 子集的迷你编译器从零搭起来词法、语法、语义、目标代码四步怎么切每步写多少行、边界参数怎么定以及那些最容易让人翻车的坑。适合两类人一是下周要交报告、现在代码还跑不通的二是想拿优、但不想靠堆代码量凑页数的。全程用 Java 实现能编译、能报错、能生成简易汇编报告跟着代码走而不是代码跟着报告编。2. 定语言与切模块Java 实现的编译原理实验该怎么拆2.1 为什么用 Java面向对象承载 AST 与符号表顺带解决“java编译原理”的课设需求检索“java编译原理”的人多半是在找课设参考。用 Java 做编译器不是因为它性能好而是它的类型约束和面向对象结构刚好贴合编译器的天然模块划分。AST 节点用抽象类加子类token 类型用枚举符号表条目用类封装这些在 Java 里写出来几乎不用额外设计直接就是你想表达的结构。对比 C 语言方案省去了字符串处理、内存管理这些和编译原理无关的体力活对比 Python 方案调试阶段不会出现“类型不对但不知道哪里不对”的黑匣子问题。工程构建也不要引入 Maven、Gradle 这类依赖管理工具直接 javac 编译三个文件答辩环境只要有 JDK 就能跑。很多课设翻车不是代码逻辑错了而是把简单工程搞成了依赖地狱换个机器就跑不起来。用 Java 写编译器就是让评审老师把注意力放在你如何处理文法、如何做错误恢复上而不是去猜 JVM 参数。2.2 目标子集语言先把 BNF 写死再动代码动手写代码前第一件事是定语言范围。很多课设代码写不下去是因为目标语言没边界今天想支持 for明天想加数组最后卡在语法分析里出不来。我一般建议只做一个类 C 子集范围控制在作业验收能讲清楚、代码量在 1500 行以内。下面这份 BNF 是多次带人做课设后留下的版本加了一条print_stmt专门用来做运行时验证。prog : stmt* stmt : var_decl | assign_stmt | if_stmt | while_stmt | block | print_stmt | expr_stmt var_decl : int IDENT ( expr)? ; | float IDENT ( expr)? ; assign_stmt : IDENT expr ; if_stmt : if ( expr ) stmt (else stmt)? while_stmt : while ( expr ) stmt print_stmt : print expr ; block : { stmt* } expr : add_expr add_expr : mul_expr (( | -) mul_expr)* mul_expr : unary_expr ((* | /) unary_expr)* unary_expr : - unary_expr | primary primary : INT_LIT | FLOAT_LIT | IDENT | ( expr )这份文法的设计意图有三点。第一print是关键没有它你编译完不知道程序算出来多少必须靠看中间代码猜有了它print 1 2 * 3;一跑就知道结果是不是 7。第二去掉了函数定义和数组不是不能做而是这两个特性会把符号表设计拖入作用域和参数传递的深水区课设周期内风险大。第三表达式保留完整的优先级层次这是编译原理课程的核心考点评审老师一定会问。报告里可以把函数和数组写成“扩展方向”比硬做出来一团乱麻更体面。2.3 四个模块的接口约定与工程骨架模块划分直接对应编译原理的经典阶段每个阶段一个类类与类之间只通过明确的中间数据结构通信。这样做的好处是答辩时被问“某个阶段做了什么”你可以指着代码说清楚边界。src/ Token.java // token 类型枚举与 token 类 Lexer.java // 词法分析输出 ListToken Ast.java // AST 节点定义 Parser.java // 语法分析输出 Ast.Program SymbolTable.java // 符号表与语义检查 CodeGen.java // 目标代码生成输出字符串列表 Main.java // 主流程与参数解析 tests/ valid/ // 能正确编译运行的用例 invalid/ // 包含词法、语法、语义错误的用例主流程控制在 60 行以内每个阶段结束后把当前产物 dump 出来这是排查问题的基础设施。下面是一个可用的主流程骨架public class Main { public static void main(String[] args) throws Exception { String src Files.readString(Path.of(args[0])); Lexer lexer new Lexer(src); ListToken tokens lexer.tokenize(); if (args.length 1 args[1].equals(--dump-tokens)) { for (Token t : tokens) System.out.println(t); } if (args.length 1 args[1].equals(--stop-after-lex)) return; Parser parser new Parser(tokens); Ast.Program prog parser.parse(); if (args.length 1 args[1].equals(--dump-ast)) { prog.dump(0); } if (args.length 1 args[1].equals(--stop-after-parse)) return; SymbolTable sema new SymbolTable(); sema.check(prog); if (sema.hasErrors()) { sema.printErrors(); return; // 语义错误不进入代码生成 } CodeGen gen new CodeGen(); ListString asm gen.generate(prog); for (String line : asm) System.out.println(line); } }注意两个参数--dump-tokens和--dump-ast。这两个开关是排错时最常敲的命令后面各章会反复用到。构建和运行命令固定成这样避免答辩现场手忙脚乱javac src/*.java -d out java -cp out Main tests/valid/expr_print.cc --dump-ast java -cp out Main tests/valid/expr_print.cc第一次写可以先不接语义检查和代码生成把--stop-after-parse跑通再逐步打开后续阶段。这样做的好处是每一层错误都在它该出现的阶段暴露而不是混在一起无从下手。2.4 符号表作用域压栈的简单设计符号表是链接语法分析和语义检查的桥梁但它最容易写成一团浆糊。我用一个类维护一个作用域栈每层作用域是一个 HashMap键为变量名值为表项对象。进入{}块时压栈出块时弹栈查找时从栈顶向内层遍历。class SymbolTable { static class Entry { String name; String type; // int 或 float boolean assigned; int scopeDepth; } private final DequeMapString, Entry scopes new ArrayDeque(); void enterScope() { scopes.push(new HashMap()); } void exitScope() { scopes.pop(); } void declare(String name, String type) { if (scopes.peek().containsKey(name)) { throw new CompileError(变量 name 重复声明); } Entry e new Entry(); e.name name; e.type type; scopes.peek().put(name, e); } Entry lookup(String name) { for (MapString, Entry scope : scopes) { Entry e scope.get(name); if (e ! null) return e; } return null; } }这个设计里最关键的是enterScope和exitScope必须成对调用。后面避坑章会专门讲块语句解析时忘了弹栈导致的“局部变量读到全局变量余晖”问题是课设里出现频率最高的语义错误。3. 词法分析落地手工状态机的 token 流实现3.1 token 类型与关键字表先定枚举再写状态机词法分析有两种主流做法用 Lex/Flex 自动生成或者手工写状态机。课设我建议手工写理由很实际自动生成工具生成的代码没法在报告里逐行解释答辩时被追问“NFA 怎么转 DFA”很容易露怯。手工状态机 200 行内能搞定而且每一步都能讲清楚。token 类型先用一个枚举固定下来这也直接决定了后面语法分析写起来顺不顺enum TokenType { IDENT, INT_LIT, FLOAT_LIT, KW_INT, KW_FLOAT, KW_IF, KW_ELSE, KW_WHILE, KW_PRINT, OP_ASSIGN, OP_PLUS, OP_MINUS, OP_MUL, OP_DIV, LPAREN, RPAREN, LBRACE, RBRACE, SEMI, EOF, ILLEGAL }关键字表用 HashMap 做“标识符到关键字类型”的映射。这里有一个初学者容易忽略的细节识别标识符时先按普通标识符读入一整串字母数字再查关键字表而不是在读取过程中逐个字符判断是否是关键字。原因是最长匹配原则intx应该被识别为标识符而不是关键字int加标识符x。private static final MapString, TokenType KEYWORDS new HashMap(); static { KEYWORDS.put(int, TokenType.KW_INT); KEYWORDS.put(float, TokenType.KW_FLOAT); KEYWORDS.put(if, TokenType.KW_IF); KEYWORDS.put(else, TokenType.KW_ELSE); KEYWORDS.put(while, TokenType.KW_WHILE); KEYWORDS.put(print, TokenType.KW_PRINT); }3.2 状态机主循环一段能直接跑的数字与标识符识别词法分析的主循环本质上是一个“看当前字符决定进入哪个状态”的开关。下面这段代码覆盖了标识符、整数、浮点数、运算符和括号是多次课设验证过的最小完整实现ListToken tokenize() { ListToken tokens new ArrayList(); while (pos src.length()) { char c src.charAt(pos); if (Character.isWhitespace(c)) { pos; continue; } if (Character.isDigit(c)) { tokens.add(readNumber()); continue; } if (Character.isLetter(c)) { String word readIdentifier(); TokenType type KEYWORDS.getOrDefault(word, TokenType.IDENT); tokens.add(new Token(type, word, line, col)); continue; } switch (c) { case : tokens.add(new Token(TokenType.OP_PLUS, , line, col)); pos; break; case -: tokens.add(new Token(TokenType.OP_MINUS, -, line, col)); pos; break; case *: tokens.add(new Token(TokenType.OP_MUL, *, line, col)); pos; break; case /: tokens.add(new Token(TokenType.OP_DIV, /, line, col)); pos; break; case (: tokens.add(new Token(TokenType.LPAREN, (, line, col)); pos; break; case ): tokens.add(new Token(TokenType.RPAREN, ), line, col)); pos; break; case {: tokens.add(new Token(TokenType.LBRACE, {, line, col)); pos; break; case }: tokens.add(new Token(TokenType.RBRACE, }, line, col)); pos; break; case ;: tokens.add(new Token(TokenType.SEMI, ;, line, col)); pos; break; case : tokens.add(new Token(TokenType.OP_ASSIGN, , line, col)); pos; break; default: errors.add(第 line 行第 col 列: 非法字符 c ); pos; // 跳过非法字符继续分析 } } tokens.add(new Token(TokenType.EOF, eof, line, col)); return tokens; }readNumber是这里最容易出错的地方单独拆开看Token readNumber() { int startLine line, startCol col; StringBuilder sb new StringBuilder(); while (pos src.length() Character.isDigit(src.charAt(pos))) { sb.append(src.charAt(pos)); pos; } boolean isFloat false; if (pos src.length() src.charAt(pos) .) { if (pos 1 src.length() Character.isDigit(src.charAt(pos 1))) { isFloat true; sb.append(.); pos; while (pos src.length() Character.isDigit(src.charAt(pos))) { sb.append(src.charAt(pos)); pos; } } else { errors.add(第 line 行第 col 列: 小数点后缺少数字); } } TokenType type isFloat ? TokenType.FLOAT_LIT : TokenType.INT_LIT; return new Token(type, sb.toString(), startLine, startCol); }这段代码暗含两个边界决策。第一1.5是浮点1.报错而不是回退成整数1加小数点符号因为语言定义里没有单独的小数点 token把这个错误暴露出来比悄悄吞掉更利于调试。第二数字后面紧跟字母如123abc这里会识别成123和标识符abc严格来说应该报错但课设阶段为了错误恢复的健壮性可以接受这种宽松处理在报告里注明即可。3.3 错误恢复与行列号课设演示不崩的关键参数词法分析器的错误处理策略直接决定你在答辩演示时是体面收场还是当场翻车。很多参考代码遇到非法字符直接抛异常退出演示时输入一个中文字符分号程序瞬间崩溃解释半天也救不回来。正确的做法是收集错误进列表跳过非法字符继续分析。最后只要错误列表非空就不进入后续阶段并把所有错误一次性打印出来。行列号的维护也要从一开始就做对。每消费一个\n行号加一、列号归零每消费一个普通字符列号加一。token 记录起始行号和起始列号这样语法分析和语义检查报错时能精确指出“第 3 行第 9 列变量未声明”。没有行列号的编译器排错体验等于瞎猜后面章节的所有排查手段都建立在这个基础上。3.4 用 dump-tokens 验证词法结果写完词法分析先别急着写语法分析用命令行验证一下java -cp out Main tests/valid/simple.cc --dump-tokens我当时的做法是准备一个覆盖所有 token 类型的用例比如int a 1.5; print a;然后人工核对每一行的输出。输出格式固定为行:列 类型 值例如1:1 KW_INT int 1:5 IDENT a 1:7 OP_ASSIGN 1:9 FLOAT_LIT 1.5 1:11 SEMI ; 2:1 KW_PRINT print 2:7 IDENT a 2:8 SEMI ; 3:1 EOF eof看到第 1 行第 9 列FLOAT_LIT就说明浮点数识别路径没问题。这一步验证到位后面语法分析报错时就可以放心地认为是语法层的问题而不是去词法层重新翻账。4. 递归下降语法分析表达式优先级、AST 与错误恢复4.1 递归下降 vs LR为什么课选前者形式上讲LR 分析器更“正统”《编译原理》教材也花大篇幅在讲 LR 自动机。但课设选递归下降的理由很硬代码结构和非终结符一一对应每个函数就是一个语法成分出错时调用栈就是分析路径。相比之下Yacc/Bison 生成的 LR 分析器核心是一张大表表从哪来、冲突怎么解决报告中三言两语说不清调试时看状态栈更是灾难。递归下降也不是没有代价。最直接的限制是它处理不了左递归文法需要先做文法改写。这个改写过程本身就是课程考核点写进报告反而是加分项。另外递归下降对每个非终结符都要写一个函数代码量略大但好处是每段都能在报告中逐行讲明白工作量真实可见。4.2 文法改写与表达式解析左递归怎么消除expr - expr term这种左递归直接写成递归函数会无限递归。教材标准答案是把左递归改写为右递归加循环对应到递归下降就是解析term之后用 while 循环看下一个 token 是不是或-是就继续解析下一个term并构建左结合树。Ast.Expr parseAddExpr() { Ast.Expr left parseMulExpr(); while (peek().type TokenType.OP_PLUS || peek().type TokenType.OP_MINUS) { Token op consume(); // 消费 或 - Ast.Expr right parseMulExpr(); left new Ast.BinaryOp(op.type, left, right); } return left; } Ast.Expr parseMulExpr() { Ast.Expr left parseUnaryExpr(); while (peek().type TokenType.OP_MUL || peek().type TokenType.OP_DIV) { Token op consume(); Ast.Expr right parseUnaryExpr(); left new Ast.BinaryOp(op.type, left, right); } return left; }这里有一个新手容易忽略的细节while循环里构建的树是左结合的。1 - 2 - 3会解析成(1 - 2) - 3而不是1 - (2 - 3)。这正符合 C 语言语义也是考试里常考的“左结合性”考点。如果贪图方便用递归实现右结合四则运算结果全是错的而且这种错误在语义上不会报错只能在运行结果里暴露。4.3 AST 节点设计树形 dump 就是你的调试器AST 是语法分析的输出也是语义检查和代码生成的输入它设计得好不好直接决定后面 debug 效率。我用的方案是一个抽象基类加十个左右的子类abstract class Node { abstract void dump(int indent); abstract String evalType(SymbolTable st); // 语义检查用 }关键方法有两个。dump做树形打印每个子节点缩进两格evalType返回表达式类型供语义检查时判断int和float混合运算规则。BinaryOp 节点是这个设计的核心class BinaryOp extends Expr { TokenType op; Expr left, right; void dump(int indent) { System.out.println( .repeat(indent) op); left.dump(indent 1); right.dump(indent 1); } }跑--dump-ast时一个print 1 2 * 3;会输出PrintStmt BinaryOp(OP_PLUS) IntLit(1) BinaryOp(OP_MUL) IntLit(2) IntLit(3)一眼看出优先级对不对。这不是花架子后面目标代码生成结果不对时第一步就是 dump AST 确认源程序有没有被正确理解第二步才去看代码生成器的问题。4.4 语法错误恢复panic mode 的同步集怎么定语法错误恢复策略里最简单可靠的是 panic mode。原理是某处解析失败后丢弃 token 直到遇到一个“同步点”再从同步点重新开始分析。同步点选什么是这里唯一需要动脑的地方。我用的同步集合是{SEMI, RBRACE, EOF}也就是分号、右大括号和文件结束。理由很直接语句都以分号结尾块都以右大括号结尾跳到这两个位置再开始基本能保证后续语句还能被正确识别。具体实现是统一封装consume方法Token consume(TokenType expected) { Token t peek(); if (t.type expected) { pos; return t; } errors.add(第 t.line 行第 t.col 列: 期望 expected 实际是 t.type); synchronize(); return t; } void synchronize() { while (peek().type ! TokenType.SEMI peek().type ! TokenType.RBRACE peek().type ! TokenType.EOF) { pos; } if (peek().type TokenType.SEMI) pos; // 跳过同步点本身 }不要在每个 parse 函数里各写各的错误处理那是把错误恢复逻辑散落一地。统一走consume之后所有语法错误都记录在错误列表里最后一次性打印。这样设计一个错误也能继续往下分析能收集到尽可能多的错误课设演示时输入一个带多个错误的文件一口气报出四五条错误比“第一个错就停、改一次跑一次”看起来专业得多。4.5 语义检查声明、类型与 return 路径语法分析通过后语义检查负责抓“变量未声明”“重复声明”“类型不匹配”这类错误。遍历 AST 的方式很简单每个节点都实现check(SymbolTable)方法父节点调用子节点符号表在遇到 Block 节点时压栈和弹栈。class Block extends Stmt { ListStmt stmts; void check(SymbolTable st) { st.enterScope(); for (Stmt s : stmts) s.check(st); st.exitScope(); } }类型检查规则要提前定清楚。int和float做四则运算时我用的规则跟 C 语言一致一个操作数是float则结果类型是float赋值时float不能赋给intint可以赋给float。判断条件只接受int或float类型不接受未声明变量。这些规则写成代码不到 50 行但报告里可以展开成一个完整的类型检查章节。5. 课设避坑手册符号表串值、死循环与演示翻车5.1 符号表串值局部变量读到全局变量的余晖现象在同一个程序里声明一个全局a和一个块内局部a块内给a赋值后输出块外再次输出时发现全局a也被改了。代码检查了很多遍赋值语句只写了一处。原因块语句解析或语义检查时enterScope和exitScope少调用了其中一次。最常见的情况是if语句只有then分支没有else分支两个分支各写一次enterScope/exitScope只写了then的else的忘了。结果就是作用域栈越压越深lookup永远先命中当前最内层赋值改的是内层变量但语义检查认为它和全局是同一个名字查重也查不出来——关闭作用域后声明没有真正失效。解决给 Block 的check方法加一个强制try-finally结构void check(SymbolTable st) { st.enterScope(); try { for (Stmt s : stmts) s.check(st); } finally { st.exitScope(); } }这样即使子节点抛异常或提前返回作用域也必然弹栈。另外在语义检查完成后加一道防线遍历所有赋值语句检查每个被赋值的变量是否已声明未声明但能通过lookup找到的多数是作用域栈没弹干净的信号。这道防线能把串值问题从“结果错”变成“报错”大幅减少排查成本。5.2 递归下降死循环match 没有消费 token 的栈溢出现象跑语法分析时程序卡死过一会儿抛出StackOverflowError。看调用栈parseExpr和parseAddExpr互相调用反复出现在栈顶。原因递归函数没有在入口处消耗 token。典型的失误是在while循环里写了if (peek().type OP_PLUS)就递归调用parseAddExpr但循环内部没有pos或consume()。每轮循环看到的 token 都是同一个递归层数无限增加栈就爆了。这是递归下降里最基础的错误但几乎每个初写的人都会踩一次。解决约定三条铁律。第一每个 parse 函数的第一个动作必须是查看当前 token 类型不匹配就直接返回或报错第二循环里调用下一层解析函数之前必须先consume()消费掉当前运算符第三写完后用一个深层嵌套表达式1234...100做压力测试能过说明结合性处理和 token 消费节奏是对的。排查时开-d参数打印 token 位置观察死循环发生时 token 序号是否原地踏步一列输出就能定位问题。5.3 目标代码黑匣子AST 对而汇编错的定位思路现象--dump-ast输出的树完全正确表达式优先级也对但生成的目标代码算出的结果不对。比如print 2 3 * 4;AST 是2 (3 * 4)生成的汇编却先算加法再算乘法输出 20。原因AST 正确只能说明语法分析和语义检查没问题问题出在代码生成器遍历树的顺序上。常见的失误是代码生成用递归函数先处理左子树、再处理右子树但没有为每个中间结果新建临时变量两个子表达式的结果挤在同一个寄存器里后算的覆盖了先算的。这是典型的“每层都对、合起来错”的黑匣子问题。解决代码生成阶段不要省临时变量。每产生一个子表达式结果就分配一个新的临时变量名哪怕下一个指令立刻就要用。2 3 * 4生成的汇编应该是t1 3 * 4 t2 2 t1 print t23 * 4的结果存在t1中不会被2 t1的结果覆盖。虽然多几条指令但课设不要求做寄存器分配优化正确性优先。验证方法是把 AST dump 和汇编输出并排比对人工检查每个 AST 节点是否都对应了一条tN ...指令。这一步做完目标代码生成的“黑匣子”就拆成了透明流水线。5.4 词法吞字符1.与除法的边界坑现象输入a 1.0 / 2;词法分析把/识别成了注释开头如果某版代码里加了//注释支持或者把1.识别成整数1后紧跟一个非法字符.报出一条莫名其妙的非法字符错误。原因两个边界条件没处理好。一是浮点数识别逻辑里只判断了小数点后是否紧跟数字没有处理1.这种“小数点后没有数字但后面是运算符”的情况二是如果加了/开头的注释支持/后面跟/是注释、跟数字是除号这个分支判断容易写反。解决readNumber里小数点后若没有数字把错误消息写成“第 x 行第 y 列: 小数点后缺少数字”同时把整个 token 标记为 ILLEGAL但不要继续吞字符。后面判断运算符时再遇到.就报非法字符——把错误留在它真正发生的位置。除法与注释的分支用两个字符的 peek 判断/后紧跟/才进注释否则一律走除法路径。注释支持不是必须项课设阶段完全可以不实现//注释减少一个分支就少一类 bug。5.5 报告与演示的翻车可复现比堆代码更重要现象报告写了六十页附录贴了三千行代码演示时输入一个测试用例却报错现场改代码越改越乱最后只能用“这个用例没覆盖到”圆场。原因报告把重心放在了“展示工作量”而不是“展示可验证的正确性”。如果你们学校是把编译原理实验和课设合并计分的比如山科大这类按“实验课设”一起算的课程演示环节更看重你能当场改一个用例、再跑出结果的流畅性。贴大量没有运行输出的源码评审老师一眼就知道哪些是凑数的。解决所有测试用例固化成脚本每个用例独立成一个文件脚本统一编译并比对输出。演示时先跑脚本全集再现场改一个变量值重跑整个流程不超过两分钟。报告里每个代码段下面只放一张关键输出截图比如一处 AST dump 加一处汇编输出比贴三十行毫无输出的类定义有说服力得多。教材第二章的习题答案背得再熟也抵不上现场跑通一个while循环求和的完整链路。6. 验收前的最后一步测试用例覆盖与答辩演示技巧6.1 把测试用例固化成脚本正常与错误用例各一桌课设验收前一天的晚上不要再去加新功能把测试用例补齐。我常用的做法是维护两个目录valid/放能正确编译运行的用例每个覆盖一个语法特性invalid/放包含错误的用例每个只引入一类错误。然后用一个脚本批量跑#!/bin/bash for f in tests/valid/*.cc; do echo $f java -cp out Main $f || echo FAIL: $f done for f in tests/invalid/*.cc; do echo $f java -cp out Main $f 21 | grep -q 错误 echo PASS || echo FAIL: $f done正常用例目录里必须包含赋值与打印、四则运算优先级、浮点与整数混合、if-else两条路径、while循环累加、嵌套块作用域六类至少各一个。错误用例目录里包含非法字符、变量未声明、变量重复声明、类型不匹配、缺少分号五类至少各一个。脚本跑完所有用例的输出要在报告里保留一份这就是附录里最有价值的部分。6.2 答辩演示的两个加分动作-d 参数与现场改用例答辩演示时不要求把整个编译器从头到尾讲一遍但要让人看到你理解它内部发生了什么。第一个动作是开--dump-tokens跑一个小程序让评审看到输入的源程序变成了 token 流再开--dump-ast跑同一个程序让评审看到 token 流变成了语法树最后关掉调试参数跑同一个程序让评审看到语法树变成了汇编并算出结果。这一条链路走下来四个阶段的职责瞬间清晰。第二个动作是现场改用例。准备一个while循环求和的程序演示时把循环次数从 10 改成 100重跑输出从 55 变成 5050。改动小、效果直观还能顺手讲一句“变量初始化在循环外、累加在循环内语义检查保证sum已声明”。这两个动作做完基本能达到“这课设是亲手写的”这个效果。做课设这些年我最深的体会是编译器的每一个阶段都要能可视化才称得上真正理解了它。希望帮到你。本文还有配套的精品资源点击获取
上一篇/下一篇内容由系统自动关联
返回资讯列表 →