多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

OUC编译原理实验四遍手写指南:词法语法分析器从零实现

OUC编译原理实验四遍手写指南:词法语法分析器从零实现 简介本资源为中国海洋大学2020年春季《编译原理》课程全套实验代码与配套文档面向计算机专业本科生及编译器学习者系统覆盖词法分析、语法分析、语义分析、中间代码生成、代码优化、目标代码生成、错误处理与编译器集成等八大核心环节助力读者从零构建完整编译流程并深化对编译器工作机理的理解。压缩包含74个文件774KB以18个C源码、8个Lex.l与8个Yacc.y脚本为主干辅以8个头文件.h、7个说明文本.txt、4个Makefile及可执行文件.exe等清晰体现各实验模块的源码—编译—测试闭环结构。已有4412人学习下载内容真实完整包含实验要求文档.doc、测试用例.p/.i、符号表与AST实现ast.h/c、错误提示模块errormsg.c/h及多份参考输出ans.txt特别适合开展课程实践、课程设计或编译器原理自学复现。1. OUC编译原理全部实验不是抄代码跑通就完事而是用四遍手写词法/语法分析器把“编译”从黑匣子变成可调试的流水线在OUC中国海洋大学计算机学院的课程体系里“编译原理实验”是出了名的“挂科预警区”——不是因为理论难而是因为它要求你亲手把教科书第3章到第6章的抽象流程一锤一钉地砸进可执行、可断点、可改错的C/C/Java程序里。我带过三届助教亲眼见过太多同学第一次实验词法分析用正则硬凑出token流第二次递归下降语法分析靠模板改变量名糊弄过去第三次语义分析中间代码生成直接复制GitHub上某份“OUC-compiler-lab”仓库结果在第四次目标代码生成面对寄存器分配时彻底失联——因为前三遍根本没真正理解“为什么需要FIRST集”“为什么LL(1)要消除左递归”“为什么四元式比三地址码更适合优化”。这组实验真正的价值不在于提交一份AC的代码而在于用四次从零实现把“编译”从一个期末背诵的名词变成你IDE里能单步跟踪、能修改文法、能注入错误并观察传播路径的活体系统。适合谁刚学完《编译原理》前六章、手头有《龙书》或《虎书》、愿意为每个if (lookahead ID)加一行printf(DEBUG: now matching ID at line %d\n, line_no)的人。别怕慢OUC实验设计本意就是逼你慢下来——慢到看清每个字符如何被识别、每条产生式如何被推导、每个符号表项如何被查重插入。2. 从正则到DFA手写词法分析器的最小可行路径与三个必须绕开的玄学陷阱OUC编译原理实验的第一关永远是词法分析。但注意OUC明确要求“不得使用Lex/Flex等自动生成工具”必须手写状态转换逻辑。这意味着你得把教材里的NFA→DFA→最小化DFA三步真刀真枪地编码落地。常见误区是直接照搬教材DFA图硬编码switch-case结果遇到注释、字符串字面量等跨行结构就崩盘。正确路径是分三层实现第一层用正则描述词法规则供人读第二层用子集构造法生成DFA供机器跑第三层用状态机驱动输入流供调试。下面给出可直接运行的C语言核心骨架重点看状态跳转逻辑和错误处理机制。// lexer.c - OUC实验要求的手写词法分析器核心状态机 #include stdio.h #include string.h #include ctype.h #define MAX_TOKEN_LEN 100 typedef enum { TOKEN_ID, TOKEN_NUM, TOKEN_PLUS, TOKEN_STAR, TOKEN_LPAREN, TOKEN_RPAREN, TOKEN_ASSIGN, TOKEN_SEMI, TOKEN_EOF, TOKEN_ERROR } TokenType; typedef struct { TokenType type; char lexeme[MAX_TOKEN_LEN]; int line_no; } Token; // 状态枚举对应DFA节点编号教材图3.14可映射 typedef enum { STATE_START 0, STATE_IN_ID 1, STATE_IN_NUM 2, STATE_COMMENT 3, STATE_STRING 4, STATE_ERROR 5 } State; Token next_token(FILE *src) { static int line_no 1; Token tok {TOKEN_ERROR, , line_no}; int c; State state STATE_START; int pos 0; char buf[MAX_TOKEN_LEN]; while ((c fgetc(src)) ! EOF) { buf[pos] \0; switch (state) { case STATE_START: if (isspace(c)) { if (c \n) line_no; continue; // 跳过空白 } else if (isalpha(c) || c _) { buf[pos] c; state STATE_IN_ID; } else if (isdigit(c)) { buf[pos] c; state STATE_IN_NUM; } else if (c ) { tok.type TOKEN_PLUS; strcpy(tok.lexeme, ); return tok; } else if (c *) { tok.type TOKEN_STAR; strcpy(tok.lexeme, *); return tok; } else if (c () { tok.type TOKEN_LPAREN; strcpy(tok.lexeme, (); return tok; } else if (c )) { tok.type TOKEN_RPAREN; strcpy(tok.lexeme, )); return tok; } else if (c ) { int next fgetc(src); if (next ) { tok.type TOKEN_ASSIGN; strcpy(tok.lexeme, ); return tok; } else { ungetc(next, src); // 回退 tok.type TOKEN_ASSIGN; strcpy(tok.lexeme, ); return tok; } } else if (c ;) { tok.type TOKEN_SEMI; strcpy(tok.lexeme, ;); return tok; } else if (c /) { int next fgetc(src); if (next *) { state STATE_COMMENT; // 进入多行注释状态 continue; } else if (next /) { // 单行注释跳到行尾 while ((c fgetc(src)) ! \n c ! EOF); if (c \n) line_no; state STATE_START; continue; } else { ungetc(next, src); tok.type TOKEN_ERROR; strcpy(tok.lexeme, /); return tok; } } else if (c ) { state STATE_STRING; continue; } else { tok.type TOKEN_ERROR; buf[0] c; buf[1] \0; strcpy(tok.lexeme, buf); return tok; } break; case STATE_IN_ID: if (isalnum(c) || c _) { if (pos MAX_TOKEN_LEN-1) buf[pos] c; } else { ungetc(c, src); buf[pos] \0; strcpy(tok.lexeme, buf); tok.type TOKEN_ID; return tok; } break; case STATE_IN_NUM: if (isdigit(c)) { if (pos MAX_TOKEN_LEN-1) buf[pos] c; } else { ungetc(c, src); buf[pos] \0; strcpy(tok.lexeme, buf); tok.type TOKEN_NUM; return tok; } break; case STATE_COMMENT: if (c *) { int next fgetc(src); if (next /) { state STATE_START; } else { ungetc(next, src); } } else if (c \n) { line_no; } break; case STATE_STRING: if (c ) { buf[pos] \0; strcpy(tok.lexeme, buf); tok.type TOKEN_ID; // 字符串字面量按标识符处理OUC实验简化要求 return tok; } else if (c \n) { tok.type TOKEN_ERROR; strcpy(tok.lexeme, unclosed string); return tok; } else { if (pos MAX_TOKEN_LEN-1) buf[pos] c; } break; case STATE_ERROR: tok.type TOKEN_ERROR; strcpy(tok.lexeme, unknown error); return tok; } } // 文件结束 if (state STATE_IN_ID || state STATE_IN_NUM) { buf[pos] \0; strcpy(tok.lexeme, buf); tok.type (state STATE_IN_ID) ? TOKEN_ID : TOKEN_NUM; return tok; } else if (state STATE_START) { tok.type TOKEN_EOF; return tok; } else { tok.type TOKEN_ERROR; strcpy(tok.lexeme, unexpected EOF); return tok; } }提示这段代码不是“最终答案”而是OUC实验要求的最小可调试基线。关键在STATE_COMMENT和STATE_STRING的处理——很多同学卡在注释嵌套或字符串转义上但OUC实验说明明确指出“暂不支持嵌套注释”“字符串内不处理转义”所以按上述简化逻辑即可。ungetc()的调用位置决定回退字符是否被下次fgetc()读取这是调试时最易出错的点。2.1 正则规则到DFA状态的映射为什么你的ID状态机总漏掉下划线OUC实验指导书附录A给出了标准词法规则其中ID → letter (letter | digit | _)*。但学生常犯的错误是在DFA状态定义中只判断isalpha(c)忽略c _。更隐蔽的坑是下划线作为首字符的合法性——教材例题通常只写letter但OUC测试用例明确包含_count、__init这类合法标识符。解决方案是在STATE_START分支中将isalpha(c) || c _作为进入STATE_IN_ID的唯一条件并在STATE_IN_ID循环中同样检查c _。这个细节在调试时可通过打印buf内容验证当输入_x1;时第一个token应为_x而非_截断。2.2 行号计数的精确性为什么你的错误定位总偏移一行line_no变量必须在每次读到\n时立即自增且必须在ungetc()之后、状态跳转之前完成。反例若在STATE_COMMENT中检测到\n后延迟到循环末尾再line_no则当注释跨行时第二行的错误提示会显示为第一行。正确做法如代码所示在STATE_COMMENT分支内if (c \n) line_no;。此外fgetc()读到\n后该字符已消耗无需额外处理——这是C标准库行为不是玄学。2.3 错误恢复策略遇到非法字符时是报错退出还是跳过继续OUC实验评分标准明确要求“对单个非法字符应报告错误但分析器需尝试恢复并继续扫描后续token”。这意味着TOKEN_ERROR不能终止整个next_token()函数而应返回错误token后让调用方语法分析器决定是否丢弃该token并继续。代码中tok.type TOKEN_ERROR后直接return tok即满足此要求。切记不要在STATE_ERROR中exit(1)——那是编译器前端崩溃不是词法分析器的职责。3. 递归下降 vs LL(1)预测分析OUC语法分析实验的两种实现选择与性能临界点OUC编译原理实验的第二关语法分析核心矛盾在于指导书说“推荐递归下降”但测试用例包含左递归文法而递归下降无法直接处理左递归。这就逼你必须做选择是花两天时间手动消除左递归再写递归下降还是直接上LL(1)预测分析表我的血泪经验是对OUC实验选递归下降但必须先做文法改造且改造过程本身就是考点。因为OUC历年真题中约70%的语法错误源于未正确消除左递归导致的无限递归栈溢出而非预测表构建错误。我们以OUC实验指定的算术表达式文法为例来自《龙书》习题4.2E → E T | E - T | T T → T * F | T / F | F F → ( E ) | id | num这个文法含左递归递归下降会死循环。OUC要求你先改写为E → T E E → T E | - T E | ε T → F T T → * F T | / F T | ε F → ( E ) | id | num改写后你才能安全编写递归下降函数。下面给出parse_E()和parse_Eprime()的C语言实现重点看match()函数如何与词法分析器联动以及ε产生式的空产生式处理逻辑。// parser.c - 递归下降语法分析器核心 #include lexer.h // 假设lexer.h声明了next_token()和Token结构 Token lookahead; // 当前预读token全局变量便于各parse函数共享 void match(TokenType expected) { if (lookahead.type expected) { lookahead next_token(stdin); // 消耗当前token预读下一个 } else { fprintf(stderr, Syntax Error at line %d: expected %d, got %d\n, lookahead.line_no, expected, lookahead.type); exit(1); } } // 解析 E → T E void parse_E() { parse_T(); // 先解析T parse_Eprime(); // 再解析E } // 解析 E → T E | - T E | ε void parse_Eprime() { if (lookahead.type TOKEN_PLUS || lookahead.type TOKEN_MINUS) { match(lookahead.type); // 消耗或- parse_T(); // 解析T parse_Eprime(); // 递归解析剩余E } // 若lookahead不是/-则匹配ε什么也不做隐式返回 } // 解析 T → F T void parse_T() { parse_F(); parse_Tprime(); } // 解析 T → * F T | / F T | ε void parse_Tprime() { if (lookahead.type TOKEN_STAR || lookahead.type TOKEN_SLASH) { match(lookahead.type); parse_F(); parse_Tprime(); } // ε产生式无操作 } // 解析 F → ( E ) | id | num void parse_F() { if (lookahead.type TOKEN_LPAREN) { match(TOKEN_LPAREN); parse_E(); // 递归解析括号内表达式 match(TOKEN_RPAREN); } else if (lookahead.type TOKEN_ID || lookahead.type TOKEN_NUM) { match(lookahead.type); // 消耗id或num } else { fprintf(stderr, Syntax Error at line %d: expected ID, NUM or (, got %d\n, lookahead.line_no, lookahead.type); exit(1); } } // 主解析入口 int main() { lookahead next_token(stdin); // 预读第一个token parse_E(); // 开始解析 if (lookahead.type ! TOKEN_EOF) { fprintf(stderr, Syntax Error: unexpected token after expression\n); exit(1); } printf(Parse successful.\n); return 0; }注意match()函数是递归下降的“心脏”。它不只做类型检查更关键的是推进lookahead到下一个token。若忘记调用next_token()所有parse_*函数将永远卡在同一个token上导致无限循环。OUC实验调试时务必在match()开头加printf(MATCHING %s at line %d\n, token_type_name(lookahead.type), lookahead.line_no);——这是你唯一的“后悔药”。3.1 为什么OUC坚持用递归下降而不是LL(1)表驱动表面看LL(1)更“正规”但OUC实验设计者深谙教学规律递归下降的函数调用栈天然对应语法树的节点层次学生单步调试时能直观看到“现在在解析E还是E”而LL(1)表驱动的stack.push()和table[action]是黑匣子。更重要的是OUC测试用例的错误注入点如缺失右括号、运算符错位在递归下降中表现为清晰的match()失败位置而在LL(1)中可能表现为栈顶符号与输入不匹配的抽象状态。所以尽管LL(1)理论上更通用但OUC实验的育人目标是让学生“看见语法结构”而非“学会查表”。3.2 ε产生式的陷阱空产生式不等于什么都不做初学者常以为E → ε分支可以完全省略导致parse_Eprime()函数为空。这是致命错误因为当输入为id无运算符时parse_Eprime()必须成功返回否则parse_E()后续逻辑无法继续。正确做法是只要当前lookahead不匹配或-就静默返回如代码所示。调试时可在parse_Eprime()开头加printf(Entering Eprime, lookahead%d\n, lookahead.type);观察其在id和id两种输入下的行为差异。3.3 左递归消除的边界什么时候必须改写文法OUC实验文档明确列出“禁止左递归”的文法约束。但学生常忽略一个细节间接左递归同样致命。例如文法A → B a | b,B → A c | d虽无直接左递归但A ⇒ B a ⇒ A c a构成间接左递归。OUC测试用例近年已加入此类陷阱。解决方案是对所有非终结符计算其FIRST和FOLLOW集若存在A ⇒* Aαα可为空则必须消除。工具推荐用Python脚本自动检测见下一章而非肉眼排查。4. 避坑OUC编译原理实验四大高频翻车现场与血泪修复方案OUC编译原理实验的“挂科率”高不是因为题目超纲而是因为几个经典坑被反复踩中。作为连续三年批改实验报告的助教我整理出四个最高频、最隐蔽、最让学生崩溃的问题。每个问题都按“现象→原因→解决”结构给出可立即执行的修复指令拒绝空泛建议。4.1 现象词法分析器在处理123abc时输出TOKEN_NUM值为123但abc消失不见后续语法分析直接报错原因STATE_IN_NUM状态中遇到非数字字符如a时执行ungetc(c, src)后未重置状态机导致a被下一轮next_token()的STATE_START分支当作新token首字符处理——但STATE_START中isalpha(a)为真于是进入STATE_IN_ID而此时buf数组未清空a被追加到旧数字缓冲区末尾造成buf内容错乱。解决在STATE_IN_NUM的else分支中ungetc()后必须显式清空buf并重置pos0。修改代码如下case STATE_IN_NUM: if (isdigit(c)) { if (pos MAX_TOKEN_LEN-1) buf[pos] c; } else { ungetc(c, src); buf[pos] \0; // 关键确保字符串以\0结尾 strcpy(tok.lexeme, buf); tok.type TOKEN_NUM; pos 0; // 重置缓冲区指针 return tok; } break;4.2 现象语法分析器对ab*c能正确解析但对(ab)*c报“unexpected )”原因parse_F()中处理(后调用parse_E()但parse_E()成功返回时lookahead已指向)而parse_F()末尾缺少match(TOKEN_RPAREN)导致)留在lookahead中被外层parse_Tprime()误判为*或/。解决严格遵循文法F → ( E )match(TOKEN_RPAREN)必须在parse_E()之后、parse_F()返回前执行。检查所有含括号的产生式确保左右括号的match()成对出现且顺序严格为match(LPAREN) → parse_X() → match(RPAREN)。4.3 现象语义分析阶段符号表查重失败int a; int a;未报错原因符号表实现采用简单数组lookup()函数仅比较name字段但未考虑作用域。OUC实验要求支持块作用域如{ int a; ... }而学生常忽略scope_level字段在insert()时未记录当前作用域深度在lookup()时未从当前层向上逐层搜索。解决符号表节点结构必须包含int scope_levelinsert()时传入当前作用域层数如全局为0第一个{内为1lookup()时从current_level开始向0遍历。示例伪代码Symbol* lookup(char* name, int current_level) { for (int level current_level; level 0; level--) { for (each symbol in level) { if (strcmp(symbol-name, name) 0) return symbol; } } return NULL; }4.4 现象中间代码生成的四元式序列中t1 a b后t2 t1 * c的t1被错误替换为a b导致重复计算原因临时变量命名未去重new_temp()函数简单返回t count但未检查该临时变量是否已在当前基本块中定义。OUC要求“同一表达式子树复用临时变量”而学生实现的new_temp()每次调用都生成新名。解决为每个表达式子树缓存其计算结果的临时变量名。在gen_expr()函数中对E → E1 T先递归生成E1和T的代码若二者均有返回临时变量则直接生成t t1 t2若E1是id则生成t id t2。关键是要在AST节点中存储result_temp字段避免重复生成。5. 从手写到半自动用Python脚本批量验证文法属性与生成LL(1)分析表OUC编译原理实验的第三关语义分析和第四关中间代码生成对文法质量要求极高——如果文法含左递归、FIRST集相交、或FOLLOW集冲突后续所有步骤都会雪崩式报错。但手工计算FIRST/FOLLOW集极其枯燥且易错。我的做法是用Python写一个轻量级文法分析器输入OUC实验给定的BNF文法文件自动输出FIRST集、FOLLOW集、LL(1)分析表并标出所有冲突。这不是偷懒而是把重复劳动交给机器把脑力留给真正需要设计的语义动作。下面是一个可直接运行的Python脚本grammar_analyzer.py它解析标准BNF格式OUC实验文档所用格式计算并打印关键属性。脚本设计原则零依赖仅用Python标准库、输入格式严格匹配OUC文档、输出带行号便于对照。# grammar_analyzer.py - OUC编译原理实验文法验证工具 import sys import re from collections import defaultdict, deque class GrammarAnalyzer: def __init__(self, filename): self.filename filename self.productions [] # [(lhs, [rhs_symbols])] self.nonterminals set() self.terminals set() self.first defaultdict(set) self.follow defaultdict(set) self.ll1_table defaultdict(dict) # {nonterminal: {terminal: production_index}} def load_grammar(self): 加载OUC实验BNF文件格式E - T E | T with open(self.filename, r) as f: lines [l.strip() for l in f if l.strip() and not l.startswith(#)] for i, line in enumerate(lines): # 匹配 E - T E | T 格式 match re.match(r^(\w)(\s*-\s*)(.)$, line) if not match: print(fWarning: Line {i1} {line} doesnt match BNF format, skipped) continue lhs match.group(1).strip() self.nonterminals.add(lhs) rhs_part match.group(3).strip() # 分割 | 分隔的多个产生式 for rhs_str in [r.strip() for r in rhs_part.split(|)]: if not rhs_str: continue # 分割空格分隔的符号 rhs_symbols [s.strip() for s in rhs_str.split() if s.strip()] self.productions.append((lhs, rhs_symbols)) for sym in rhs_symbols: if sym.isupper() and len(sym) 1: # 单大写字母视为非终结符 self.nonterminals.add(sym) elif sym ! ε: # ε是特殊终结符 self.terminals.add(sym) # 添加起始符号的FOLLOW if self.productions: start self.productions[0][0] self.follow[start].add($) def compute_first(self): 计算FIRST集迭代直到收敛 changed True while changed: changed False for lhs, rhs in self.productions: # 对每个产生式 A - X1 X2 ... Xn first_of_rhs set() for sym in rhs: if sym in self.terminals: first_of_rhs.add(sym) break elif sym in self.nonterminals: first_of_rhs.update(self.first[sym]) if ε not in self.first[sym]: break else: break else: # 所有符号都能推出ε first_of_rhs.add(ε) old_size len(self.first[lhs]) self.first[lhs].update(first_of_rhs) if len(self.first[lhs]) old_size: changed True def compute_follow(self): 计算FOLLOW集 changed True while changed: changed False for lhs, rhs in self.productions: # 对每个产生式 A - αBβ for i, sym in enumerate(rhs): if sym in self.nonterminals: # 计算 FIRST(β) first_beta set() for j in range(i1, len(rhs)): s rhs[j] if s in self.terminals: first_beta.add(s) break elif s in self.nonterminals: first_beta.update(self.first[s]) if ε not in self.first[s]: break else: # β为空或全ε first_beta.update(self.follow[lhs]) # 将FIRST(β)\{ε}加入FOLLOW(B) old_size len(self.follow[sym]) self.follow[sym].update(first_beta - {ε}) if len(self.follow[sym]) old_size: changed True # 如果ε ∈ FIRST(β)则FOLLOW(A) ⊆ FOLLOW(B) if ε in first_beta: old_size len(self.follow[sym]) self.follow[sym].update(self.follow[lhs]) if len(self.follow[sym]) old_size: changed True def build_ll1_table(self): 构建LL(1)分析表 for i, (lhs, rhs) in enumerate(self.productions): # 对每个产生式 A - α if ε in self.first[tuple(rhs) if rhs else ()]: # α * ε则对每个a ∈ FOLLOW(A)M[A,a] A-α for a in self.follow[lhs]: if a in self.ll1_table[lhs] and self.ll1_table[lhs][a] ! i: print(fLL(1) Conflict at [{lhs}, {a}]: production {self.ll1_table[lhs][a]} vs {i}) self.ll1_table[lhs][a] i else: # 对每个a ∈ FIRST(α)M[A,a] A-α for a in self.first[tuple(rhs) if rhs else ()]: if a ε: continue if a in self.ll1_table[lhs] and self.ll1_table[lhs][a] ! i: print(fLL(1) Conflict at [{lhs}, {a}]: production {self.ll1_table[lhs][a]} vs {i}) self.ll1_table[lhs][a] i def print_results(self): print(\n FIRST SETS ) for nt in sorted(self.nonterminals): print(fFIRST({nt}) {{{, .join(sorted(self.first[nt]))}}}) print(\n FOLLOW SETS ) for nt in sorted(self.nonterminals): print(fFOLLOW({nt}) {{{, .join(sorted(self.follow[nt]))}}}) print(\n LL(1) PARSING TABLE ) terminals_sorted sorted(self.terminals | {$}) print(f{NT\\T:10}, end) for t in terminals_sorted: print(f{t:10}, end) print() for nt in sorted(self.nonterminals): print(f{nt:10}, end) for t in terminals_sorted: prod_idx self.ll1_table[nt].get(t, -1) print(f{prod_idx:10}, end) print() if __name__ __main__: if len(sys.argv) ! 2: print(Usage: python grammar_analyzer.py grammar_file.bnf) sys.exit(1) analyzer GrammarAnalyzer(sys.argv[1]) analyzer.load_grammar() analyzer.compute_first() analyzer.compute_follow() analyzer.build_ll1_table() analyzer.print_results()使用方法将OUC实验文档中的BNF文法保存为ouc_grammar.bnf如E - T E | T运行python grammar_analyzer.py ouc_grammar.bnf。脚本会输出完整的FIRST/FOLLOW集和LL(1)表并在控制台标出所有冲突位置。这是你提交前必做的验证步骤——OUC实验评分细则中“文法无LL(1)冲突”是基础分冲突未修复直接扣20%。5.1 输入文件格式规范为什么你的BNF总被脚本跳过脚本严格匹配OUC实验文档的BNF书写习惯-两侧有空格产生式右侧用空格分隔符号|前后有空格。错误示例E-T E|T-无空格、E - T E | T|后无空格会被跳过。正确写法E - T E | T E - T E | - T E | ε T - F T T - * F T | / F T | ε F - ( E ) | id | num注意ε必须小写且单独作为一个符号不能写成epsilon或。5.2 冲突诊断当脚本报“Conflict at [E, ]”时下一步做什么这表示E的两个产生式 T E和ε的FIRST集都含违反LL(1)条件。解决方案只有两个1) 确认文法是否真的需要ε产生式OUC实验中E → ε是必须的用于处理id无运算符的情况2) 检查 T E的FIRST集是否被错误计算——是终结符其FIRST集就是{}不可能与ε冲突。此时问题必在E的FOLLOW集若出现在FOLLOW(E)中而E → ε被激活则会同时属于FIRST( T E)和FOLLOW(E)导致冲突。根因是文法设计缺陷需回溯检查E在哪些产生式中被使用如E → T E确认FOLLOW(E)是否真的应含。OUC标准答案中FOLLOW(E) FOLLOW(E) ∪ {), $}不含所以冲突表明你的FOLLOW计算有误。5.3 为什么不用现成工具如ANTLROUC的底层逻辑是什么OUC严禁使用ANTLR、Yacc等工具根本原因在于这些工具把文法分析封装成黑盒学生无法观察FIRST集如何影响预测表无法理解为什么E → ε必须与FOLLOW(E)联动。而手写脚本验证的模式让你既免于手工计算的枯燥又保有对每个集合含义的掌控。这正是OUC实验的精妙之处——它不反对自动化但自动化必须服务于“理解”而非替代“理解”。6. 符号表与中间代码用哈希表链表实现OUC要求的块作用域与四元式生成技巧OUC编译原理实验的最后两关——语义分析和中间代码生成——是区分“及格”和“优秀”的分水岭。很多同学能跑通语法分析却在符号表设计上栽跟头全局变量和局部变量混在一起int a; { int a; }不报重定义或者四元式生成时a b c * d的运算符优先级错乱生成t1 b c; t2 t1 * d而非t1 c * d; t2 b t1。这些问题的根源不是算法不会而是没有把OUC实验文档中“块作用域”和“三地址码”这两个关键词真正转化成内存布局和代码生成逻辑。下面给出经过OUC真题验证的符号表与四元式生成方案。6.1 块作用域符号表哈希表嵌套链表的物理实现OUC要求符号表支持“块作用域”block scope即{...}本文还有配套的精品资源点击获取
返回列表