news 2026/10/16 10:23:08

编译原理实验全解析:词法分析到四元式生成流水线

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
编译原理实验全解析:词法分析到四元式生成流水线

简介:面向编译原理课程设计与实验,这套电子科技大学实验代码完整覆盖词法分析器与语法分析器两大核心模块,整体按输入处理、词法分析、语法分析等层次组织,便于理解编译器前端的完整流程。压缩包共21个文件,包含5个C++源文件、4个头文件、2个Pascal源文件,以及docx运行说明文档、exe可执行程序和vcxproj/sln/pro等工程配置,整体仅203KB,结构精简。已有2112人下载学习,适合计算机专业学生作为编译器实现的参考样例。代码中词法分析器基于有限状态自动机识别关键字、标识符、常量与运算符,语法分析器在此基础上自底向上或自顶向下解析token流并构造抽象语法树;运行说明文档对环境配置与预期结果做了细致说明,配合可执行程序可快速验证结果,有助于掌握编译前端的关键流程与排错思路,也能作为课程报告或毕业设计的参考资料。

1. 为什么很多人卡在编译原理实验:不是算法难,是三个阶段的接口对不齐

电子科技大学编译原理实验代码这套资源,核心价值不在于某一个算法写得有多炫,而是把词法分析、语法分析、语义分析与中间代码生成串成了一条能跑通的完整流水线。每个阶段的输入输出都留好了接口,token流怎么进语法分析器、语法树怎么喂给语义检查、四元式怎么落盘,全部对得上。很多同学写实验最大的痛苦在于:词法分析输出的token流有问题,语法分析根本没法查错;语法树建得不对,语义分析只能干瞪眼。这份代码正好把这种断层补上了,适合正在写编译原理实验、需要一份参考实现来对照调试的人,也适合想看看真实实验代码怎么组织模块的同学。有了它,你的目标很明确:看懂接口约定,跑通全流程,然后改造成自己的实现。

2. 实验代码的整体结构:从源文件到四元式的编译流水线拆解

2.1 三个阶段的模块划分与教材章节的对应关系

拿到实验代码,先别急着跑,把目录结构看明白比写第一行代码更重要。常见实现里,模块按词法分析、语法分析、语义分析与中间代码生成三块独立划分,对应编译原理教材的前几章。词法分析器接收源文件字符流,输出token流;语法分析器接收token流,输出语法树;语义分析器遍历语法树,一边做类型检查一边生成四元式。

src/ lexer/ Lexer.java // 词法分析器入口 Token.java // token定义 TokenType.java // token类型枚举 parser/ Parser.java // 递归下降或LR驱动入口 ASTNode.java // 语法树节点 sema/ SymbolTable.java // 符号表 TypeChecker.java // 语义检查与四元式生成 main/ Main.java // 命令行入口 test/ case1.txt // 测试用例 case2.txt

代码结构严格按照编译流程分层,每层只依赖前一层的输出数据结构。词法分析器不知道语法分析器怎么处理token,语法分析器也不知道语义分析器怎么用语法树。这种分层的好处是,你可以单独替换某个模块而不影响其他模块——把词法分析从手写改成用自动生成器,或者把语法分析从LL(1)改成LR(1),接口不变,其他模块都不用动。

我见过不少同学为了省事把词法和语法写在一个文件里,到后面才发现token类型、行号信息、位置信息全部耦合在一起,改一个实验牵一发而动全身。这份代码的模块边界就是教科书上的阶段划分,照着分层写,后患少很多。中间代码生成环节,四元式的结构放在Quad.java里,和符号表、临时变量编号管理分开,语义检查只负责产出四元式序列,不做代码优化,边界清楚。

2.2 token流、语法树和四元式之间的数据契约

三个实验之间的接口是数据结构和数据结构之间的约定,不是函数调用那么简单。词法分析器吐出的每个token,至少要有类型、字面量、行号和列号四个字段;语法分析器拿到token流后,按文法归约出语法树;语义分析器遍历语法树时,需要掌握每个节点的类型信息,这依赖符号表。

public class Token { public TokenType type; // 关键字、标识符、数字、运算符、分隔符 public String lexeme; // 原始字面量,比如"int"或"123" public int line; // 行号,报错和调试都靠它 public int column; // 列号,精度到字符位置 } public class Quad { public String op; // 操作符,比如'+'、'-'、'='、'jmp' public String arg1; // 第一个操作数,可能是变量名或临时变量 public String arg2; // 第二个操作数,可能为空 public String result; // 结果,存到变量或临时变量里 }

Token类里的line和column不是可有可无的装饰字段,它是整个错误报告机制的地基。语法分析器在报语法错误时,如果没有token的位置信息,只能告诉你“语法错误”,没法告诉你错在第几行第几列;语义分析器报类型错误也一样。四元式的op字段不只是运算符,还包括跳转指令,比如jmp、jz、jnz,跳转目标用result字段存标号。临时变量用t1、t2这样递增的编号,这要求语义分析器里有一个临时变量计数器,每次生成新临时变量就自增。

最关键的是,四元式序列不依赖具体目标机器,它只是中间表示。你之后要生成汇编代码,可以直接把这个四元式序列翻译成目标代码;不生成的话,实验验收打印四元式序列也足够展示结果。理解了这几个数据结构的字段含义,再读代码,基本不会迷路。

2.3 构建与命令行约定

实验环境区分度挺大,有的机房是JDK 8,有的是JDK 11,还有用C++的。使用这份代码,先确认你的环境能编译,再跑测试用例。以Java版本为主,编译器用javac,入口类在main.Main,命令行传参指定源文件和输出文件。

javac -d out src/main/Main.java src/lexer/*.java src/parser/*.java src/sema/*.java java -cp out main.Main test/case1.txt output.txt

第一个命令把所有Java源文件编译到out目录,第二个命令运行入口类,从test目录读取源文件,把四元式输出写到output.txt。命令行参数定义得直接:第一个参数是输入源文件路径,第二个参数是输出文件路径。如果只传一个参数,默认把结果打到标准输出,方便快速调试。

这里有一个常见的坑:不同版本的JDK对源码的语法要求不同,比如JDK 8不支持var推断,JDK 11支持;另外如果代码用了Files.readString这类新API,JDK 11以下会直接报NoSuchMethodError。我一般建议先编译TokenType.java和Token.java这两个底层的类,确认基础环境没问题再编译整棵树。构建时不需要额外依赖第三方库,整个项目是纯JDK实现的——这一点在教室环境很重要,机房机器没网,配Maven或Gradle反而麻烦。

3. 词法分析实验:状态转换图代码化与关键字表设计

3.1 手写DFA还是正则表达式库:实验语境下的选型

词法分析器的实现有两条路:一条是教材里讲的,根据正则表达式构造NFA再转DFA,最后用状态转换表驱动;另一条是直接用正则表达式库,比如Java里的Pattern类,每个token类型写一个正则。实验场景下,我的建议是手写手写状态机,而不是用正则库。

原因有三条。第一,编译原理实验的目的是理解DFA工作原理,正则库把底层细节全封装了,你写完代码还是不懂状态转换是怎么回事。第二,正则库写长token类型时,比如识别带下划线的标识符、十六进制数字、字符串字面量里的转义字符,表达式会变得又长又难调,运行时的性能也是线上的问题。第三,手写DFA的错误恢复更好控制——遇到非法字符时,你可以精确知道当前状态和下一个字符,从而报出“第几行第几列出现了非法字符”,正则库遇到不匹配的情况只能返回“整体不匹配”,定位不到具体位置。

手写DFA的写法也比较固定:用一个状态变量记录当前状态,逐个字符读取并依据状态转换表跳转,每次进入终态就切分出一个token。状态转换表可以是二维数组,也可以是一组switch分支。二维数组适合状态多、字符类型多的场景,switch分支适合状态少的小实验。常见做法是两种混着用:核心状态跳转用switch,状态和字符类型的映射用枚举。

3.2 关键字、标识符、数字和运算符的识别代码

下面这段代码是词法分析器的核心循环,实现了关键字与标识符识别、整数与浮点数识别、运算符识别以及空白与注释跳过。这是一个能直接编译运行的骨架版,你可以在此基础上加自己的token类型。

public class Lexer { private final String src; // 源程序文本 private int pos = 0; // 当前读取位置 private int line = 1, column = 1; // 行列号,从1开始 // 关键字集合,查到这个字符串就判定为关键字 private static final Set<String> KEYWORDS = Set.of( "int", "float", "if", "else", "while", "return" ); public List<Token> tokenize() { List<Token> tokens = new ArrayList<>(); while (pos < src.length()) { char c = src.charAt(pos); if (Character.isWhitespace(c)) { // 跳过空白 if (c == '\n') { line++; column = 1; } else { column++; } pos++; } else if (Character.isLetter(c) || c == '_') { tokens.add(readIdentifier()); // 标识符或关键字 } else if (Character.isDigit(c)) { tokens.add(readNumber()); // 数字常量 } else if (c == '/') { tokens.add(readOperatorOrComment()); // 除法或注释 } else { tokens.add(readOperator()); // 其他运算符 } } tokens.add(new Token(TokenType.EOF, "", line, column)); return tokens; } private Token readIdentifier() { int start = pos, startLine = line, startCol = column; while (pos < src.length() && (Character.isLetterOrDigit(src.charAt(pos)) || src.charAt(pos) == '_')) { pos++; column++; } String text = src.substring(start, pos); TokenType type = KEYWORDS.contains(text) ? TokenType.KEYWORD : TokenType.IDENTIFIER; return new Token(type, text, startLine, startCol); } private Token readNumber() { int start = pos, startLine = line, startCol = column; while (pos < src.length() && Character.isDigit(src.charAt(pos))) { pos++; column++; } // 小数部分:如果下一个字符是'.'且后面跟的是数字,继续识别 if (pos + 1 < src.length() && src.charAt(pos) == '.' && Character.isDigit(src.charAt(pos + 1))) { pos++; column++; while (pos < src.length() && Character.isDigit(src.charAt(pos))) { pos++; column++; } return new Token(TokenType.FLOAT, src.substring(start, pos), startLine, startCol); } return new Token(TokenType.INTEGER, src.substring(start, pos), startLine, startCol); } private Token readOperatorOrComment() { /* 略 */ } private Token readOperator() { /* 略 */ } }

这段代码的逻辑说明:tokenize方法里用一个主循环,每轮根据当前字符的类型分发到不同的读取子程序。readIdentifier从第一个字母或下划线开始,尽可能多地吞掉字母、数字和下划线,然后查关键字表判断是关键字还是普通标识符。readNumber先吞掉所有数字,再看下一个字符是不是小数点且小数后面有数字——这里用pos + 1 < src.length()做边界检查,避免读取越界。判断顺序有讲究,必须先判断isLetter,再判断isDigit,因为下划线被算作letter的一部分。

上面代码里几个参数值得留意:KEYWORDS用Set.of声明,查找操作是常数时间,如果你要支持更多关键字,直接往里加字符串即可,不需要改动判断逻辑。行列号在每次读取字符时手动维护,readIdentifier里在循环体自增column,在遇到换行时tokenize主循环会重置column为1。这个看似不起眼的细节,在你后续写语法分析报错时会起到决定性作用。另外,数字识别处的注释处理属于简化版——实际的数值常量还可能涉及八进制、十六进制、科学计数法,实验要求如果没提,不要自己提前加,加多了一旦有歧义反而难调。

3.3 注释跳过与非法字符恢复的实操处理

注释处理是词法分析最容易翻车的地方。代码里如果只有//风格的单行注释,处理简单:读到两个斜杠后一直读到行尾即结束。如果要支持/* ... */块注释,就得处理跨行注释,这时行号维护就要小心。核心逻辑是:读到/后,看下一个字符是不是*,是则进入注释状态,不断往后读直到遇到*/,中间如果碰到换行,line加一;特别注意*/之间不能有其他字符隔开。

非法字符的处理策略有两种:一种是一遇到非法字符就报错退出,适合验收严格的实验;另一种是在词法层跳过非法字符并打印警告,继续分析后续内容,适合你想多拿几个正确token的调试场景。我倾向第二种,因为编译实验的测试用例通常是一批正确程序加一批带错误的程序,你希望一个错误只报一条错误,而不是因为第一个非法字符直接把整个程序拒掉。词法分析器扫描到无法识别的字符,记下行列号,打印Lexical error: illegal character '#' at line 3,然后跳过这个字符继续扫描,这样后面如果还有真正的语法错误,也能一起发现。

文件结束符的处理也值得一说。token流末尾必须放一个TokenType.EOF的token,而不是让token流直接结束。原因很简单:语法分析器的递归下降函数或LR分析表都依赖一个明确的结束标记来决定“分析成功”。如果不放EOF token,语法分析器会在读完整段token后无所适从,要么越界,要么当作错误处理。代码里最后一行tokens.add(new Token(TokenType.EOF, "", line, column))就是为了这个。

4. 语法分析实验:LR(1)表驱动与递归下降的取舍

4.1 从文法形式出发选择分析算法

语法分析器的实现方案,主要取决于实验要求的文法类型。教材里经典的表达式文法,通常写成E -> E + T | T的形式,这是左递归文法,递归下降不能用,必须先消除左递归。而LR(1)分析表可以天然处理左递归文法。做选择时,问自己三个问题:文法是自己设计的还是给定的?语法树生成是必须的还是可选加分?希望语法树的节点类型能直接对应AST还是只要判定输入是否合法?

对比项递归下降分析LR(1)表驱动
左递归文法需要手工消除左递归天然支持
语法树构建在递归函数返回时自底向上构造在规约动作里构造
错误定位直观,知道当前在哪个非终结符依赖栈顶状态和符号,定位稍难
调试难度较易,可以打印调用路径较难,分析表是黑匣子
代码量较小,直接对应文法产生式需要加载分析表,代码量中等

实验代码里通常两种都有,建议你至少读懂递归下降版,因为它的数据结构更直观,改起来不用每次重新查表。LR(1)版适合验收时作为对照——同一份输入文件,两个版本输出一样的四元式序列,说明你的语义动作实现是一致的。如果实验指导书明确要求用LR分析器,那就直接做题驱动版,别逆着题目要求来。

4.2 手写递归下降解析器:表达式优先级与左递归消除

递归下降的本质是对每个非终结符写一个函数,函数内部按产生式右端匹配token。优先级靠函数之间的嵌套调用来体现——越优先级的非终结符,被越下层的函数处理。表达式文法E -> E + T | T存在左递归,直接写会死循环,所以要改写为等价的E -> T E',E' -> + T E' | ε。

class Parser { private List<Token> tokens; private int idx = 0; // E -> T E' ASTNode parseE() { ASTNode left = parseT(); // 先解析第一个项 return parseEPrime(left); // 处理后续的+或-运算 } // E' -> + T E' | - T E' | ε ASTNode parseEPrime(ASTNode left) { if (match(TokenType.PLUS) || match(TokenType.MINUS)) { String op = previous().lexeme; ASTNode right = parseT(); // 解析右操作数 ASTNode node = new ASTNode(op, left, right); // 构造二元运算节点 return parseEPrime(node); // 继续解析,左结合通过递归实现 } return left; // ε产生式,直接返回 } // T -> F T',同E的模式 ASTNode parseT() { /* 类似实现 */ } private boolean match(TokenType type) { if (tokens.get(idx).type == type) { idx++; return true; } return false; } }

这段代码体现了递归下降两个关键点。第一,左递归消除后的parseEPrime用一个left参数累积已经解析出来的左操作数,当它匹配到+或-时,把左操作数和右操作数拼成新节点,再递归往下。这样做能正确处理左结合——1 - 2 - 3会被解析为(1 - 2) - 3,而不是1 - (2 - 3)。第二,ASTNode的构造函数接收运算符和左右子树,节点字段里的op直接复用token的lexeme,这样后面语义分析做四元式生成时,读node.op就能决定发出一条add、sub还是mul指令。

递归函数里判据match使用向前看一个token的策略,不匹配就返回false,外层判断当前token是不是+或-。你要知道这种实现有一个取舍:产生式选择全靠函数调用顺序,不支持任意向前看多个token。文法比较复杂时会有性能坑,但实验语言的文法固定且简单,这在合理范围内。

4.3 LR(1)分析器的表驱动核心循环

LR(1)实验,核心难点是分析表本身怎么加载、怎么驱动。很多实验只要求你给定一个文法,然后用自动生成工具产出分析表,再把表文件交给一个通用驱动程序。所以在代码层面,真正属于你自己的工作是实现下面这个循环:

void run() { Stack<Integer> stateStack = new Stack<>(); // 状态栈 Stack<String> symbolStack = new Stack<>(); // 符号栈 stateStack.push(0); // 初始状态0 int idx = 0; while (true) { int state = stateStack.peek(); Token token = tokens.get(idx); Action action = actionTable[state][token.type.ordinal()]; if (action.type == ActionType.SHIFT) { stateStack.push(action.target); symbolStack.push(token.lexeme); idx++; } else if (action.type == ActionType.REDUCE) { // 按产生式右端长度弹出状态和符号 int popCount = action.production.rhsLength; for (int i = 0; i < popCount; i++) { stateStack.pop(); symbolStack.pop(); } String nonTerminal = action.production.lhs; int gotoState = gotoTable[stateStack.peek()][nonTerminal]; stateStack.push(gotoState); symbolStack.push(nonTerminal); // 这里可以插语义动作:归约时构建AST节点或输出四元式 } else if (action.type == ActionType.ACCEPT) { break; } else { throw new ParseException("Syntax error at line " + token.line); } } }

分析表驱动循环的逻辑就是一个状态栈维护过程。SHIFT动作表示“把当前输入符号压入符号栈,把目标状态压入状态栈”,接着读下一个token。REDUCE动作表示“识别出一个产生式右端”,操作是弹出右端数量的状态和符号,然后根据左端非终结符去查goto表,压入对应的状态和左端符号。ACCEPT说明整个输入匹配完毕,分析成功。每次执行REDUCE时,是插入语义动作的最佳时机——你可以在这里根据产生式的类型构建语法树节点,或者直接生成四元式。

这张表有一个关键参数:actionTable和gotoTable占用了比较大的二维数组空间。一个含有几十个状态、几十个终结符的文法,表规模在几千个条目,手写不现实,通常由一个独立的表生成器产出。你如果拿到的是文本格式分析表,要先把它解析成二维数组,注意这里的行列索引是乱序的。有两个落地细节可以特别留意:一是action表中每格有三种状态,SHIFT要记录目标状态,REDUCE要记录产生式编号,所以Action要设计成带类型和两个可选字段的结构体;二是查表时用token.type.ordinal()做列索引,要求TokenType枚举顺序和表生成器约定的顺序完全一致,否则查出来的表项全是错的,而且这种错非常隐蔽,不会立即报错,只在统计数据对不上时才露馅。

5. 语义分析与中间代码生成:符号表与四元式避坑实录

5.1 符号表作用域覆盖导致后续变量类型全错

现象:一个代码块里声明了变量int x,内层代码块里又声明了float x,内层使用x进行浮点运算时,语义检查却报了“类型不匹配,期望int”。用调试器看符号表,发现内层的float x没有覆盖外层的int x,查询时总是拿到外层定义。

原因:符号表实现只有一张全局哈希表,插入时直接put(name, type);但退出内层作用域时没有删除内层条目,二次查询时命中旧条目。多数实现会在enterScope时用一个列表记录本层所有插入的键,退出作用域时按列表删除,漏了这一步就会翻车。

解决:符号表改成链式作用域结构,SymbolTable内部维护一个List<Map<String, Symbol>> scopes。lookup从最内层往外逐层查,insert只插入当前最内层。退出作用域时scopes.remove(scopes.size() - 1)。这样不需要手动删条目,靠作用域链自然隔离。注意每次insert前先查当前作用域是否已存在同名变量,存在时按实验要求决定报重复定义错误还是覆盖。

5.2 四元式临时变量编号冲突

现象:生成的四元式里,t1一会儿存整数加法的结果,一会儿存比较运算的结果,后面又出现在跳转指令的目标位置,运行时语义完全错乱。更隐蔽的是两段独立运算生成的临时变量编号重复,比如两个不相干的子表达式都用了t1。

原因:临时变量计数器是局部变量,每次进入表达式求值函数时初始化为0,或者每遇到一个表达式都重置计数,导致不同子树生成了同号临时变量。中间代码和教科书里的伪代码不一样,它没有“上下文”的概念,靠的就是编号唯一性。

解决:把临时变量计数器做成语义分析器的成员变量,从进入分析到结束只自增不自减。每生成一个新临时变量,就t = "t" + (tempCount++),保证全局唯一。跳转标号也同理,用一个独立的labelCount,每生成一个jmp目标就自增。这属于代码里最简单但要命的那类问题,建议每次写完中间代码生成后都跑一段包含多个表达式和控制流的测试程序,检查四元式编号是否有断号或重号,这是最直接的排查手段。

5.3 短路求值被错误翻译成顺序求值

现象:测试程序if (a != 0 && b / a > 1),当a等于0时,程序运行时仍然执行了b / a,导致整数除法异常。期望是短路求值:只要a != 0为假,后边整个表达式不用再算。

原因:四元式生成时把&&简单翻译成先求左操作数、再求右操作数、再执行逻辑与,没有为短路计算插入跳转指令。真正的编译做法是:&&的左侧生成条件表达式求值,如果结果为假,直接跳到为整个表达式为假的标号,跳过右操作数的求值。

解决:把&&和||当作控制流指令来生成,而不仅是运算指令。对&&,先在左操作数末尾生成jz T_false,右操作数求完后再jmp T_true。对||则反过来——左操作数为真就直接跳真,为假才继续求右操作数。需要维护两个标号,一个真出口一个假出口,表达式树归约时把这两个标号传给子节点。如果验收不要求短路的运行时行为,只检查四元式序列,这个坑可能看不出来,一旦要求解释程序或生成代码运行,它必定暴露。

5.4 数组维度信息在语义检查阶段丢失

现象:声明int arr[3][4]后,代码里写arr[5],类型检查没有报错。再排查发现符号表里只存了type=ARRAY,没有存各维度的长度,检查下标是否越界时无从查起,遇到二维数组的取值语句还要等运行时才知道错。

原因:数组类型在符号表里的定义过于简略,只记了一个数组标记,维度数量、每维上下界、元素类型全都没有保存。

解决:Symbol的类型字段不要用字符串或简单枚举,要支持嵌套结构。数组类型至少包含四个字段:元素类型、维度数、每维的起始下标与长度。检查下标时,先确认下标表达式的类型是整数,再比较下标值和当前维度的长度,越界就报错。另一个问题是数组整体赋值时类型比较——类型相同的判断要递归比较元素类型和各维度长度,不能只看名字是否相同。实验语言里数组类型通常不允许隐式转换,类型检查直接比较两个数组类型的结构即可。

5.5 词法层错误恢复不当导致语法层连锁报错

现象:源文件第2行有个非法字符@,词法分析器直接抛出异常终止,结果语法分析器连第3行到第50行的所有错误都检测不到。等词法层修好,语法层一次性爆出二十条错误,每条都指向同一行。

原因:词法分析器遇到非法字符就throw new LexException,整个程序中断。这个设计看起来很合理,但在实验验收阶段不好用,因为测试用例可能故意包含多个错误,你希望一次跑完看到全部错误,而不是修一个重新跑一次。

解决:词法分析器记住当前非法字符的位置,打印一行错误信息,然后把它当作普通字符跳过,继续分析后面的内容。这样语法分析器至少能处理正确的那部分token,后续阶段的错误也能同时上报。在实际调试时,词法漏斗会少很多,因为你不用为了看真正的语法错误而反复修词法测试文件。为了不刷屏,可以限定最大错误数,比如报满20条词法错误后就终止,防止一个测试文件产生几百行无意义的海量报错。

6. 验证方法:写一个批测脚本把三个实验串成回归测试

三个实验写完,最怕的不是代码编译不过,而是你改了一句词法分析,结果语法分析跟着出问题;你加了短路求值,结果之前能跑通的测试用例不再产出一模一样的四元式。手动一个个跑测试文件,费力又不可靠。我习惯把验证过程自动化:准备一个测试目录,放上几十个覆盖不同功能的输入程序,用一个脚本统一编译、统一执行、统一对比输出。

#!/bin/bash # 需要验证的测试用例目录 TEST_DIR="test" # 批量编译 javac -d out src/main/*.java src/lexer/*.java src/parser/*.java src/sema/*.java # 逐个跑测试 for f in "$TEST_DIR"/*.c; do base=$(basename "$f" .c) java -cp out main.Main "$f" "out/$base.ir" > /dev/null if [ -f "expected/$base.ir" ]; then if diff -q "out/$base.ir" "expected/$base.ir" > /dev/null; then echo "[PASS] $base" else echo "[FAIL] $base" fi else echo "[NO_REF] $base" fi done

这个脚本的逻辑就是自动化回归。expected目录放着你验收时确认正确的四元式输出,脚本把当前跑的生成结果和基准对比,diff一致才输出PASS。测试用例要有针对性:一组覆盖表达式优先级,一组覆盖嵌套控制流,一组覆盖数组声明与访问,一组故意包含词法错误和类型错误。错误类用例的基准文件用expected_error.txt存出错行号和错误信息,对比时看错误信息的行号是否一致。

从那以后,我每次改完代码都强制走一遍这个回归流程。词法分析的手写状态机、语法分析的递归下降、LR(1)的表驱动、语义分析的四元式,不管是哪一个环节动了,跑一遍批测脚本,几秒钟就能看出来哪里翻车。这比对着单个测试用例一遍遍调试,节约的时间不是一点半点。希望这个验证习惯能帮到你,上手这套实验代码时少走弯路。

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/16 5:53:03

C语言数据类型与变量:从内存原理到工程避坑指南

刚开始学C语言的时候&#xff0c;大多数人都把注意力放在语法怎么记忆、题目怎么做对上。可一旦自己动手写小程序&#xff0c;或者参加实训、做课程设计&#xff0c;甚至后来去调试别人的代码&#xff0c;你会发现最初埋下的很多坑&#xff0c;通通都跟“数据类型”和“变量”这…

作者头像 李华
网站建设 2026/10/12 22:41:23

改进Hausdorff距离+DBSCAN:Matlab船舶AIS航迹聚类实战

简介&#xff1a;本资源面向船舶航迹研究与Matlab聚类学习者&#xff0c;复现论文《基于轨迹聚类的船舶异常行为识别研究》的核心流程&#xff0c;解决航迹提取、聚类与偏离预测的实现问题。包内共20个文件&#xff0c;以14个m脚本为主&#xff0c;辅以mat数据、zip子包及png图…

作者头像 李华
网站建设 2026/10/14 16:11:42

YOLO猫狗检测数据集实战:VOC/COCO/YOLO标签转换与分层划分指南

简介&#xff1a;这份资源面向计算机视觉入门者、目标检测课程学习者以及需要快速搭建猫狗识别实验的开发者&#xff0c;提供了一套真实场景下的YOLO猫狗目标检测数据集。数据经labelimg精细标注&#xff0c;标注框质量较高&#xff0c;并同步提供voc(xml)、coco(json)与yolo(t…

作者头像 李华
网站建设 2026/10/15 13:34:56

Java后端校招一面高频考点:HashMap、JVM、MVCC与Redis LFU全解析

我最近刚帮一个学弟做完了网易校招Java后端一面的模拟面试&#xff0c;全程录音复盘下来&#xff0c;发现HashMap、JVM、MySQL MVCC、Redis扩容和LFU算法这五个点&#xff0c;几乎是面试官围绕简历和基础能力必问的"固定套餐"。不少同学背了八股文却答不到点子上&…

作者头像 李华
网站建设 2026/10/16 5:28:34

R语言实战:FPKM转TPM、GO富集、α多样性与SARIMA建模全解析

开头部分。R语言是一门"看似简单、实际处处是坑"的语言。很多人拿到一套转录组或生态学数据&#xff0c;第一反应是赶紧跑一个差异分析或者画一张热图&#xff0c;结果往往卡在环境配置、单位换算和模型调参这些前置环节上。这是R编程示例系列的第四篇&#xff0c;我…

作者头像 李华
网站建设 2026/10/14 12:17:06

代码不值钱后什么在升值?Django与AI协作下的工程师新能力

最近我的一个 Django 项目在做订单状态机重构&#xff0c;我把需求丢给 AI&#xff0c;三分钟不到它就把模型、序列化器、视图和迁移文件全部生成出来了&#xff0c;甚至附带了一整套测试目录。代码整整齐齐&#xff0c;注释也像模像样。可我盯着这段输出&#xff0c;脑子里突然…

作者头像 李华