☰
广工编译原理实验:手写词法与LL(1)语法分析器实战指南
2026/10/12 18:19:46 网站建设 项目流程

简介:本资源是广东工业大学编译原理课程配套实验材料,面向计算机专业本科生及编译技术初学者,聚焦PL/0教学编译器的扩展开发实践。内容围绕词法与语法分析能力提升,重点实现保留字ELSE、FOR、TO等新增、不等号符号从#到<>的规范替换,以及ELSE子句支持和++、--等复合运算符扩展,切实解决教学中对经典编译流程的深度理解与动手改造难题。压缩包共24个文件(644KB),含核心源码文件(pl0.h、PL0.cpp)、可执行程序(exe)、调试信息(pdb、ilk、obj)、工程配置(dsw、dsp、opt)及实验报告(doc),类型分布体现完整VS6.0开发环境下的编译、调试与文档闭环。已有789人学习下载,提供可直接编译运行的修改版PL/0工程,附带测试代码与结构化目录,便于对照实验要求逐项验证语法增强效果,是开展编译原理课程设计与实验拓展的实用参考。

1. 广工编译原理实验:不是抄代码,是亲手造出一个能跑通的词法分析器和语法分析器

你在广工上编译原理课,老师刚布置完实验一:用 C 或 Java 实现一个能识别整数、标识符、运算符、分隔符的词法分析器;实验二紧接着要求你基于 LL(1) 文法手写递归下降分析器,输入a+b*c这类表达式,输出语法树或错误提示。你翻着王生原《编译原理(第3版)》第三章,对着课件里那张“词法分析→语法分析→语义分析→中间代码生成”的流程图发呆——这图很美,但你连第一个 token 都没成功切出来。这不是理论考试,是实打实要交可执行程序、要现场演示、要过老师验收的硬核实验。广工编译原理实验的核心诉求非常明确:用最小可行代码,在本地 Windows 或 Linux 环境下,跑通从源码字符串到 token 序列、再到语法结构的完整链路。它不考你背文法,而考你能不能把书上的 DFA、FIRST/FOLLOW 集、预测分析表,变成一行行能 debug 的代码。适合刚学完前两章、对正则和递归调用有基本手感的大三学生;也适合想补全编译器工程直觉的应届生——因为广工实验十三(语法分析部分)的验收标准,从来不是“代码看起来像”,而是“输入x = (a + b) * c;,它真能吐出正确的 AST 节点”。


2. 用 Java 手写词法分析器:从正则匹配到状态机落地的最小闭环

广工编译原理实验第一关,本质是字符串处理能力的实战检验。很多同学卡在“为什么我的split("\\s+")不能识别a++中的两个+”——因为词法分析不是简单切分,而是按最长匹配原则和优先级顺序逐字符推进。我们跳过 Lex/Yacc 这类工具链(广工实验明确要求手写),直接用 Java 构建一个可调试、可扩展、能应对实验报告中所有测试用例的词法分析器。

2.1 定义 Token 类型与保留字表:先画清边界再动手

广工实验常见测试样例包含:int a = 10;、if (x > 0) { y = x * 2; }、甚至带注释的/* hello */ float pi = 3.14;。这意味着你的 Token 必须覆盖:

  • 关键字(int,if,while等)
  • 标识符(以字母/下划线开头,后接字母数字)
  • 整数常量(123,0xFF不要求,但+123,-45要支持)
  • 运算符(+,-,*,/,==,!=,<=等)
  • 分隔符(;,{,},(,))
  • 注释(单行//和多行/* ... */)
// Token.java public class Token { public enum Type { KEYWORD, IDENTIFIER, INTEGER, OPERATOR, SEPARATOR, COMMENT, ERROR } public final Type type; public final String value; public final int line; public Token(Type type, String value, int line) { this.type = type; this.value = value; this.line = line; } @Override public String toString() { return String.format("Token{type=%s, value='%s', line=%d}", type, value, line); } }

提示:广工实验报告五(词法分析部分)常要求输出 token 表格,所以toString()必须格式统一,方便重定向到文件。不要用System.out.println(new Token(...))直接打印,而要封装成printTokens(List<Token>)方法,后续加颜色或 CSV 导出都留了接口。

2.2 基于状态机的手写 Scanner:拒绝正则黑匣子,每个状态都可控

用String.matches()或Pattern处理复杂 token(如>=vs>)极易出错。广工实验十三验收时,老师会故意输入a+++b,看你是否能正确切分为a,++,+,b(而不是a,+,++,b)。这必须靠显式状态机控制。

核心状态定义(简化版,实际需 8–12 个状态):

状态含义转移条件
START初始态遇空格跳过;遇字母进 IDENTIFIER;遇数字进 NUMBER;遇/先看下一个字符判断是否注释
IDENTIFIER收集标识符继续读字母/数字/下划线;遇非标识符字符停止,查保留字表
NUMBER收集整数继续读数字;遇非数字字符停止(注意123abc应切为123+abc)
SLASH/单字符态下一字符是*→ 进 MULTILINE_COMMENT;是/→ 进 SINGLELINE_COMMENT;否则退回输出/
// Scanner.java(关键扫描逻辑) public List<Token> scan(String source) { List<Token> tokens = new ArrayList<>(); int i = 0, line = 1; while (i < source.length()) { char c = source.charAt(i); switch (c) { case ' ': case '\t': case '\r': i++; break; // 跳过空白 case '\n': line++; i++; break; case '/': if (i + 1 < source.length()) { char next = source.charAt(i + 1); if (next == '*') { // /* ... */ i = skipMultilineComment(source, i, line); break; } else if (next == '/') { // // i = skipSingleLineComment(source, i, line); break; } } tokens.add(new Token(Token.Type.OPERATOR, "/", line)); i++; break; case '+': case '-': case '*': case '%': case '=': case '!': case '<': case '>': String op = String.valueOf(c); if (i + 1 < source.length() && isDoubleCharOp(c, source.charAt(i + 1))) { op += source.charAt(i + 1); i += 2; } else { i++; } tokens.add(new Token(Token.Type.OPERATOR, op, line)); break; case '0': case '1': case '2': case '3': case '4': case '5': case '6': case '7': case '8': case '9': int start = i; while (i < source.length() && Character.isDigit(source.charAt(i))) { i++; } tokens.add(new Token(Token.Type.INTEGER, source.substring(start, i), line)); break; case '_': case 'a': case 'b': ... case 'z': case 'A': ... case 'Z': start = i; while (i < source.length() && (Character.isLetterOrDigit(source.charAt(i)) || source.charAt(i) == '_')) { i++; } String id = source.substring(start, i); if (KEYWORDS.contains(id)) { tokens.add(new Token(Token.Type.KEYWORD, id, line)); } else { tokens.add(new Token(Token.Type.IDENTIFIER, id, line)); } break; case ';': case '{': case '}': case '(': case ')': tokens.add(new Token(Token.Type.SEPARATOR, String.valueOf(c), line)); i++; break; default: tokens.add(new Token(Token.Type.ERROR, "Unknown char: '" + c + "'", line)); i++; } } return tokens; }

参数说明与可调点:

  • KEYWORDS是Set<String>,初始化为Set.of("int", "if", "else", "while", "return")—— 广工实验要求覆盖至少 5 个关键字;
  • isDoubleCharOp()是辅助方法,判断c和下一字符是否构成==,!=,>=,<=,++,--等双字符运算符;
  • skipMultilineComment()和skipSingleLineComment()必须严格处理嵌套和换行,否则/* /* nested */ */会翻车;
  • line计数必须在\n处更新,且注释内换行也要计入——这是广工实验报告五的扣分点。

3. 构建 LL(1) 语法分析器:从文法推导到预测分析表的手动实现

词法分析器产出 token 流后,下一步是验证其是否符合语法规则,并构建语法树。广工编译原理实验第二阶段(常称“实验十三”)明确要求:基于给定 LL(1) 文法,手动构造 FIRST 集、FOLLOW 集、预测分析表,并用递归下降或预测分析器实现。别被“LL(1)”吓住——它本质是“从左到右扫描,最左推导,向前看 1 个符号”。广工常用文法是简化版算术表达式文法(含赋值、括号、优先级),例如:

S → ID = E ; E → T E' E' → + T E' | - T E' | ε T → F T' T' → * F T' | / F T' | ε F → ( E ) | ID | NUM

这个文法能覆盖a = b + c * d;,且满足 LL(1) 条件(无左递归、无公共左因子、FIRST/FOLLOW 无交集)。

3.1 手动计算 FIRST 和 FOLLOW 集:血泪经验告诉你哪三步不能跳

FIRST 和 FOLLOW 是预测分析表的基石。广工实验报告要求手写计算过程,但更重要的是——你写的表必须和代码里的一致,否则 parser 一定崩。

FIRST 集计算三步法(针对上述文法):

  1. 终结符:FIRST(ID) = {ID},FIRST(NUM) = {NUM},FIRST('(') = {'('},FIRST('+') = {'+'}...
  2. 非终结符:FIRST(F)包含FIRST('(') ∪ FIRST(ID) ∪ FIRST(NUM) = {'(', ID, NUM};FIRST(T')包含FIRST('*') ∪ FIRST('/') ∪ {ε}(因T' → ε)
  3. 递归展开:FIRST(E') = FIRST('+') ∪ FIRST('-') ∪ {ε};FIRST(E) = FIRST(T);FIRST(T) = FIRST(F) = {'(', ID, NUM}

FOLLOW 集计算四要点(易错!):

  • FOLLOW(S)始终含$(输入结束符);
  • 若A → αBβ,则FOLLOW(B)包含FIRST(β) \ {ε};若β ⇒* ε,则还要加FOLLOW(A);
  • 若A → αB,则FOLLOW(B)包含FOLLOW(A);
  • 关键陷阱:FOLLOW(E')必须包含FOLLOW(E)(因E → T E'),而FOLLOW(E)又含FOLLOW(S)(因S → ID = E ;,E后是;,故FOLLOW(E) = {';'}),所以FOLLOW(E') = {';', '$'}—— 这个$很多人漏掉,导致E' → ε触发条件错。

注意:广工实验十三验收时,老师会问“为什么E'的 FOLLOW 里有$?”,答不出就扣分。这不是理论题,是验证你是否真懂 FOLLOW 的传播逻辑。

3.2 构造预测分析表:二维数组比 HashMap 更直观、更易 debug

预测分析表M[A][a]表示:当栈顶是非终结符A,当前输入符号是a时,该用哪条产生式。广工实验要求你手填表格,但代码里必须精确复现。

// ParseTable.java(核心数据结构) public class ParseTable { // 行:非终结符索引;列:终结符索引(含 $) private final String[][] table; private final Map<String, Integer> nonTerminals; private final Map<String, Integer> terminals; public ParseTable() { // 初始化映射:nonTerminals = {"S":0, "E":1, "E'":2, "T":3, "T'":4, "F":5} // terminals = {"ID":0, "NUM":1, "(":2, ")":3, "+":4, "-":5, "*":6, "/":7, "=":8, ";":9, "$":10} this.table = new String[6][11]; // 6 行 × 11 列 initTable(); } private void initTable() { // S → ID = E ; table[0][0] = "S → ID = E ;"; // S, ID // E → T E' table[1][0] = "E → T E'"; // E, ID table[1][1] = "E → T E'"; // E, NUM table[1][2] = "E → T E'"; // E, ( // E' → + T E' table[2][4] = "E' → + T E'"; // E', + table[2][5] = "E' → - T E'"; // E', - table[2][3] = "E' → ε"; // E', ) table[2][9] = "E' → ε"; // E', ; table[2][10] = "E' → ε"; // E', $ // T → F T' table[3][0] = "T → F T'"; // T, ID table[3][1] = "T → F T'"; // T, NUM table[3][2] = "T → F T'"; // T, ( // T' → * F T' table[4][6] = "T' → * F T'"; // T', * table[4][7] = "T' → / F T'"; // T', / table[4][3] = "T' → ε"; // T', ) table[4][4] = "T' → ε"; // T', + table[4][5] = "T' → ε"; // T', - table[4][9] = "T' → ε"; // T', ; table[4][10] = "T' → ε"; // T', $ // F → ( E ) table[5][2] = "F → ( E )"; // F, ( // F → ID table[5][0] = "F → ID"; // F, ID // F → NUM table[5][1] = "F → NUM"; // F, NUM } public String getProduction(String nonTerminal, String terminal) { int row = nonTerminals.getOrDefault(nonTerminal, -1); int col = terminals.getOrDefault(terminal, -1); return (row == -1 || col == -1) ? null : table[row][col]; } }

参数说明:

  • table是String[][],不是Map,因为广工实验要求你“能一眼看出某行某列填了什么”,二维数组天然支持table[i][j]查表;
  • 终结符列表必须含$(输入结束符),且getProduction()对未知符号返回null,便于后续报错;
  • initTable()中每条赋值对应一个预测分析表格单元,必须和你手算的 FIRST/FOLLOW 严格一致——比如E'行中)列填ε,是因为FOLLOW(E')含),且FIRST(+ T E')不含),所以此处只能选ε。

4. 预测分析器实现与 AST 构建:让语法树真正可打印、可验证

有了词法分析器(产出 token 流)和预测分析表(指导推导路径),最后一步是驱动分析栈、匹配输入、构建抽象语法树(AST)。广工实验十三的终极目标不是“能跑”,而是“能输出结构化的语法树”,用于后续语义分析或中间代码生成。很多同学止步于accept/reject,但老师验收时会要求你printAST(root)输出缩进树。

4.1 基于栈的预测分析器:模拟教科书算法,每步都可断点

预测分析算法本质是维护一个栈(初始为[$, S])和输入指针,循环执行:

  • 若栈顶是终结符,与当前 token 匹配,弹出栈顶,指针后移;
  • 若栈顶是非终结符A,查表得产生式A → α,弹出A,压入α的逆序(因栈是 LIFO);
  • 若栈顶$且输入结束,则成功;否则报错。
// Parser.java public class Parser { private final ParseTable table; private final List<Token> tokens; private int pos = 0; private final Stack<String> stack = new Stack<>(); public Parser(ParseTable table, List<Token> tokens) { this.table = table; this.tokens = tokens; // 初始化栈:$ 在底,S 在顶 stack.push("$"); stack.push("S"); } public ASTNode parse() throws ParseException { while (!stack.isEmpty()) { String top = stack.pop(); if (top.equals("$")) { if (pos >= tokens.size()) return null; // 成功 else throw new ParseException("Unexpected token: " + tokens.get(pos), pos); } if (isTerminal(top)) { if (pos >= tokens.size()) { throw new ParseException("Expected " + top + ", but input ended", pos); } Token token = tokens.get(pos); if (match(top, token)) { pos++; } else { throw new ParseException("Expected " + top + ", got " + token.type + "(" + token.value + ")", pos); } } else { // 非终结符:查表 String terminal = getTerminalName(tokens.get(pos)); String production = table.getProduction(top, terminal); if (production == null) { throw new ParseException("No production for " + top + " on " + terminal, pos); } // 解析产生式,如 "E → T E'" → ["E'", "T"] List<String> rhs = parseRHS(production); // 压入逆序 Collections.reverse(rhs); stack.addAll(rhs); } } return null; } private boolean match(String expected, Token actual) { if (expected.equals("ID")) return actual.type == Token.Type.IDENTIFIER; if (expected.equals("NUM")) return actual.type == Token.Type.INTEGER; if (expected.equals("(")) return actual.type == Token.Type.SEPARATOR && actual.value.equals("("); if (expected.equals(")")) return actual.type == Token.Type.SEPARATOR && actual.value.equals(")"); if (expected.equals(";")) return actual.type == Token.Type.SEPARATOR && actual.value.equals(";"); if (expected.equals("=")) return actual.type == Token.Type.OPERATOR && actual.value.equals("="); if (expected.equals("+")) return actual.type == Token.Type.OPERATOR && actual.value.equals("+"); // ... 其他运算符 return false; } private String getTerminalName(Token token) { switch (token.type) { case IDENTIFIER: return "ID"; case INTEGER: return "NUM"; case SEPARATOR: return token.value; case OPERATOR: return token.value; default: return ""; } } private List<String> parseRHS(String production) { // "E → T E'" → ["T", "E'"] String[] parts = production.split("→"); String rhs = parts[1].trim(); return Arrays.stream(rhs.split("\\s+")) .filter(s -> !s.isEmpty() && !s.equals("ε")) .collect(Collectors.toList()); } }

关键设计点:

  • match()方法将 token 类型/值映射到文法符号(如Token.Type.IDENTIFIER→"ID"),这是连接词法层和语法层的桥梁;
  • parseRHS()处理ε(空产生式)时直接过滤,不压栈;
  • stack存储字符串而非 AST 节点,先保证分析过程正确,再叠加 AST 构建——这是广工实验的推荐路径,避免过早耦合。

4.2 在分析过程中同步构建 AST:每个产生式对应一个节点类型

广工实验报告要求输出 AST,意味着你不能只做 accept/reject。最佳实践是在parseRHS()执行时,根据产生式创建对应节点。例如:

  • S → ID = E ;→ 创建AssignNode(left: IdentifierNode, right: ExprNode)
  • E → T E'→ 创建BinaryOpNode(op: '+', left: T, right: E')
  • F → ID→ 创建IdentifierNode(name)
// ASTNode.java(基类) abstract class ASTNode {} class AssignNode extends ASTNode { final IdentifierNode left; final ASTNode right; AssignNode(IdentifierNode left, ASTNode right) { this.left = left; this.right = right; } } class BinaryOpNode extends ASTNode { final String op; final ASTNode left, right; BinaryOpNode(String op, ASTNode left, ASTNode right) { this.op = op; this.left = left; this.right = right; } } class IdentifierNode extends ASTNode { final String name; IdentifierNode(String name) { this.name = name; } } // 修改 Parser.parse():在压栈时传入 builder lambda public ASTNode parse() throws ParseException { // ... 同上,但压栈时绑定 builder stack.push("S"); // 初始 return parseWithBuilder("S"); } private ASTNode parseWithBuilder(String nonTerminal) throws ParseException { String terminal = getTerminalName(tokens.get(pos)); String production = table.getProduction(nonTerminal, terminal); if (production == null) throw new ParseException("No production...", pos); switch (production) { case "S → ID = E ;": Token idToken = tokens.get(pos++); IdentifierNode left = new IdentifierNode(idToken.value); expect(Token.Type.OPERATOR, "="); // 消费 = ASTNode right = parseWithBuilder("E"); expect(Token.Type.SEPARATOR, ";"); // 消费 ; return new AssignNode(left, right); case "E → T E'": ASTNode t = parseWithBuilder("T"); ASTNode ePrime = parseWithBuilder("E'"); // E' 返回可能是 BinaryOpNode 或 null(ε),需合并 return mergeEPrime(t, ePrime); // ... 其他 case } return null; }

玄学提示:广工实验十三的“后悔药”是——先用纯字符串分析跑通,再加 AST 构建。很多人一上来就边分析边建树,结果E' → ε时不知道返回null还是EmptyNode,最终树结构错乱。我一般会先确保parse()返回true/false,再在此基础上插入节点创建逻辑。


5. 广工编译原理实验避坑指南:验收翻车的 4 个高频雷区

广工编译原理实验的“验收即答辩”,老师会现场给你一段新代码(如while (x < 10) { x = x + 1; }),让你的程序跑一遍并解释输出。以下是你大概率踩过的坑,按现象→原因→解法列清,全是血泪经验。

5.1 现象:词法分析器把a++切成a,+,+,而不是a,++

原因:运算符匹配逻辑未按“最长匹配”实现。你的代码先看到+就输出+,没继续看下一个字符是否也是+。
解决:在case '+'分支里,必须检查i+1 < length && source.charAt(i+1)=='+',成立则i+=2并输出++;否则i++输出+。同理处理--,==,!=,>=,<=。广工实验报告五明确要求支持++和--。

5.2 现象:语法分析器对a = b + c * d;报错,说*不在FOLLOW(T')里

原因:FOLLOW(T')计算遗漏。T'出现在T → F T'的右侧,且T'后无符号,所以FOLLOW(T')必须包含FOLLOW(T);而T出现在E → T E'中,E'可推出ε,所以FOLLOW(T)包含FOLLOW(E);E出现在S → ID = E ;中,E后是;,故FOLLOW(E) = {';'};但E'自身的FOLLOW还含$,而T'的FOLLOW也必须含$(因E' → ε时T'可能留在栈顶等待$)。
解决:手算FOLLOW时,对每个A → αBβ,若β ⇒* ε,则FOLLOW(B)加FOLLOW(A);对A → αB,FOLLOW(B)加FOLLOW(A);最后检查所有FOLLOW是否含$——只要文法开始符号S的FOLLOW含$,所有能推导到句末的非终结符的FOLLOW都可能含$。

5.3 现象:预测分析表M[E'][*]为空,但*属于FIRST(* F T')

原因:E'的产生式是E' → + T E' | - T E' | ε,FIRST(+ T E') = {'+'},FIRST(- T E') = {'-'},FIRST(ε) = {ε},所以E'的FIRST集不含*;而*属于FOLLOW(E'),因此M[E'][*]应填ε。你误以为只有FIRST集里的符号才能触发ε产生式,忽略了FOLLOW的作用。
解决:预测分析表规则是:对A → α,若a ∈ FIRST(α),则M[A][a] = A → α;若ε ∈ FIRST(α),则对所有b ∈ FOLLOW(A),M[A][b] = A → α。所以E' → ε这条必须填到FOLLOW(E')的所有符号列,包括*,/,),;,$。

5.4 现象:AST 打印时出现null节点,或a = b + c * d的树结构扁平化(没体现*优先级)

原因:AST 构建未反映文法的结合性。你的E → T E'和T → F T'文法本意是让E'和T'处理右结合的+/-和*//,但建树时若简单new BinaryOpNode('+', t, ePrime),而ePrime是+ T E'的结果,就会变成左结合。
解决:E'和T'的递归调用必须返回“右子树”,并在父节点中作为整体参与运算。例如mergeEPrime(t, ePrime)函数应:若ePrime是BinaryOpNode('+', t2, ePrime2),则返回new BinaryOpNode('+', new BinaryOpNode('+', t, t2), ePrime2)—— 但这太复杂。广工实验接受的务实解法是:AST 节点不存运算符优先级,而由遍历顺序体现;打印时用中序遍历,*节点深度大于+节点即可。重点是树结构能区分a+(b*c)和(a+b)*c,不必强求*节点一定在+节点下方。


6. 验证与调优:用广工真实实验样例反向校准你的分析器

写完代码只是起点,广工编译原理实验的“及格线”是:你的程序能通过老师给的 5 个标准测试样例,且每个 token、每个语法树节点都能人工追溯到文法规则。我习惯用三步法闭环验证,比单纯跑测试用例更可靠。

6.1 构建最小可验证样例集:覆盖所有文法分支

不要依赖网上搜的“编译原理实验答案”,广工自己的实验指导书(尤其“实验十三”)附有 5 个必测样例,我把它拆解成验证矩阵:

样例输入期望 token 数关键语法结构验证点
1a = 10;5Assign → ID = NUM ;词法能否识别=和;;语法能否匹配S → ID = E ;
2b = a + c * d;11E'和T'嵌套*是否触发T' → * F T';+是否触发E' → + T E'
3x = (y + z) * 2;13括号改变优先级F → ( E )是否被调用;E内部是否递归分析
4if (a > 0) { a = a - 1; }17关键字、复合语句KEYWORD类型是否正确;{}是否作为SEPARATOR被捕获
5/* test */ int p = q;8(注释不产 token)注释处理/* ... */是否完全跳过,不生成任何 token

提示:广工实验报告五要求你手写“样例 3 的分析过程”,所以你的程序必须能输出每一步的栈状态和输入指针位置。我在Parser类里加了一个debugMode开关,开启时打印stack.toString() + " | " + tokens.subList(pos, tokens.size()),一行一行对照文法推导。

6.2 用 AST 可视化确认结构正确性:不靠眼力,靠缩进

广工老师不看你的System.out.println(root),而是要看树形缩进是否合理。我写了一个极简的printAST(ASTNode node, int indent):

public static void printAST(ASTNode node, int indent) { String prefix = " ".repeat(indent); if (node instanceof AssignNode n) { System.out.println(prefix + "Assign:"); System.out.println(prefix + " left:"); printAST(n.left, indent + 2); System.out.println(prefix + " right:"); printAST(n.right, indent + 2); } else if (node instanceof BinaryOpNode n) { System.out.println(prefix + "BinaryOp(" + n.op + "):"); System.out.println(prefix + " left:"); printAST(n.left, indent + 2); System.out.println(prefix + " right:"); printAST(n.right, indent + 2); } else if (node instanceof IdentifierNode n) { System.out.println(prefix + "Identifier(" + n.name + ")"); } }

运行a = b + c * d;应输出:

Assign: left: Identifier(a) right: BinaryOp(+): left: Identifier(b) right: BinaryOp(*): left: Identifier(c) right: Identifier(d)

如果*节点和+节点在同一层,说明文法或建树逻辑错了——*必须比+深一层,因为T'在E'内部。

6.3 参数调优清单:让代码适配广工实验的“隐形要求”

广工编译原理实验不是纯学术,它有明确的工程约束。以下是我在三次带实验课后总结的调优项,直接抄进你的代码:

参数默认值广工建议值为什么
KEYWORDS集{"int", "if", "else", "while", "return"}必须含{"int", "if", "else", "while", "for", "return"}实验十三样例含for循环
注释处理仅跳过//和/*...*//*...*/必须支持嵌套(/* /* inner */ outer */)广工样例 4 有嵌套注释
整数范围int改用long存储value字段防止 `1

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询