山东大学编译原理新版实验一~三通关指南:词法、语法与语义分析实战
简介这份资源是山东大学编译原理与技术课程新版实验一至三的配套代码包面向正在学习编译器前端构建的高校学生与自学者帮助解决词法分析与语法分析从理论到实现的落地问题。包内共15个文件以8个C头文件与5个cpp源文件为核心辅以1个build.sh构建脚本和1个README说明文档压缩包约30KB涵盖词法分析器、语法分析器、抽象语法树生成及对象代码生成等模块结构紧凑便于直接编译运行。实验内容从识别关键字、标识符、常量与运算符的词法规则入手逐步过渡到基于上下文无关文法的语法结构构造并涉及递归下降、LL与LR等分析技术的实践。已有57人学习下载适合作为编译器前端实验的参考实现读者可借此理解有限自动机状态转移、词素流处理与语法错误报告等关键环节并对照完成自己的实验任务。1. 编译原理实验的“第一道坎”从山东大学新版实验一~三说起如果你正在搜“山东大学编译原理与技术课程新版实验一~三”大概率是刚拿到实验指导书或者被 Lex/Yacc、递归下降、语法树这些词砸得有点懵。这套实验是山大编译原理课程的核心实践环节实验一通常做词法分析器实验二做语法分析器实验三做语义分析或中间代码生成。它解决的不是“编译原理是什么”这种科普问题而是让你亲手把正则表达式、上下文无关文法、LL(1) 分析表这些纸面知识变成能跑通的代码。适合谁正在上这门课、需要交实验报告、或者想用 C/C/Python 补一遍编译前端实操的从业者。我拆过这套实验的常见版本下面按“能复现”的标准把关键路径和坑点讲透。2. 实验一词法分析器的状态机设计与正则匹配2.1 为什么先做词法分析从字符流到 Token 序列词法分析是编译器的入口任务是把源程序的字符流切分成有意义的 Token 序列比如关键字、标识符、数字、运算符。实验一通常要求你实现一个能识别 C 语言子集的词法分析器。常见做法是手写状态机或者用 Lex/Flex 自动生成。山大新版实验一般建议手写因为能逼你理解 DFA 的构造过程。核心逻辑是读入一个字符根据当前状态和字符类型决定下一个状态遇到接受状态就输出 Token 并回退多余字符。这里的关键参数是“最长匹配”原则——比如必须匹配成大于等于不能拆成和。我一般会先画状态转移图再写代码否则边界条件容易漏。2.2 手写词法分析器的代码骨架与参数说明下面是一个能跑通的 Python 版词法分析器骨架覆盖标识符、整数、运算符和关键字。代码里用pos记录当前扫描位置peek()看下一个字符但不移动advance()移动并返回字符。import re class Lexer: def __init__(self, text): self.text text self.pos 0 self.current_char self.text[self.pos] if self.text else None def advance(self): self.pos 1 self.current_char self.text[self.pos] if self.pos len(self.text) else None def peek(self): peek_pos self.pos 1 return self.text[peek_pos] if peek_pos len(self.text) else None def skip_whitespace(self): while self.current_char is not None and self.current_char.isspace(): self.advance() def number(self): result while self.current_char is not None and self.current_char.isdigit(): result self.current_char self.advance() return (INTEGER, int(result)) def identifier(self): result while self.current_char is not None and (self.current_char.isalnum() or self.current_char _): result self.current_char self.advance() # 关键字表实验里通常要求识别 if/else/while/return 等 keywords {if, else, while, return, int, void} if result in keywords: return (KEYWORD, result) return (IDENTIFIER, result) def get_next_token(self): while self.current_char is not None: if self.current_char.isspace(): self.skip_whitespace() continue if self.current_char.isdigit(): return self.number() if self.current_char.isalpha() or self.current_char _: return self.identifier() # 处理双字符运算符比如 ! if self.current_char and self.peek() : self.advance(); self.advance() return (OPERATOR, ) if self.current_char and self.peek() : self.advance(); self.advance() return (OPERATOR, ) if self.current_char and self.peek() : self.advance(); self.advance() return (OPERATOR, ) if self.current_char ! and self.peek() : self.advance(); self.advance() return (OPERATOR, !) # 单字符运算符 if self.current_char in -*/(){};,: op self.current_char self.advance() return (OPERATOR, op) raise Exception(f非法字符: {self.current_char}) return (EOF, None)逻辑说明get_next_token是主循环每次跳过空白后判断字符类型。数字走number()字母或下划线走identifier()运算符先检查双字符组合再检查单字符。参数上keywords集合决定哪些标识符被提升为关键字实验里通常要求覆盖 C 子集。注意peek()只用于双字符运算符的前瞻不要用它来移动位置。跑通后可以用while True: token lexer.get_next_token(); if token[0] EOF: break; print(token)验证。2.3 用 Flex 快速生成词法分析器的替代方案如果实验允许用工具Flex 是更省事的路径。写一个.l文件定义正则模式和对应动作flex lexer.l生成lex.yy.c再gcc lex.yy.c -lfl -o lexer编译。常见做法是# 安装 flexUbuntu/Debian sudo apt-get install flex # 编写 lexer.l 后生成 C 代码 flex lexer.l gcc lex.yy.c -lfl -o lexer ./lexer test.c参数说明-lfl链接 Flex 库 test.c把测试文件喂给标准输入。Flex 的规则是“最长匹配优先先定义优先”所以把写在前面。坑在于 Flex 默认不处理嵌套注释实验里如果要求支持/* */得自己写状态或正则。3. 实验二语法分析器的递归下降与 LL(1) 分析表3.1 递归下降 vs 预测分析表选型理由与适用边界实验二通常要求实现语法分析器把 Token 序列变成语法树。两条主流路线递归下降和 LL(1) 预测分析表。递归下降写起来直观每个非终结符对应一个函数适合文法没有左递归、提取了左公因子的情况。LL(1) 分析表更“自动化”需要先算 FIRST 集、FOLLOW 集再构造预测分析表适合实验要求“展示分析过程”的场景。山大新版实验一般两种都接受但递归下降更容易调试。我一般会先消除左递归再写递归下降因为左递归会导致无限递归这是血泪经验。3.2 递归下降分析器的实现与语法树构造下面是一个针对简单表达式文法的递归下降分析器文法为expr - term (( | -) term)* term - factor ((* | /) factor)* factor - INTEGER | ( expr )代码用current_token保存当前 Tokeneat()消费并前进。class Parser: def __init__(self, lexer): self.lexer lexer self.current_token self.lexer.get_next_token() def eat(self, token_type): if self.current_token[0] token_type: self.current_token self.lexer.get_next_token() else: raise Exception(f期望 {token_type}实际 {self.current_token[0]}) def factor(self): token self.current_token if token[0] INTEGER: self.eat(INTEGER) return (NUM, token[1]) elif token[0] OPERATOR and token[1] (: self.eat(OPERATOR) node self.expr() self.eat(OPERATOR) # 期望 ) return node else: raise Exception(factor 解析错误) def term(self): node self.factor() while self.current_token[0] OPERATOR and self.current_token[1] in (*, /): op self.current_token[1] self.eat(OPERATOR) right self.factor() node (op, node, right) return node def expr(self): node self.term() while self.current_token[0] OPERATOR and self.current_token[1] in (, -): op self.current_token[1] self.eat(OPERATOR) right self.term() node (op, node, right) return node逻辑说明expr处理加减term处理乘除factor处理数字和括号。每个函数返回一个语法树节点元组形式(op, left, right)或(NUM, value)。参数上eat负责匹配并前进如果 Token 类型不匹配就抛异常。注意括号匹配时eat(OPERATOR)期望的是)但代码里没检查具体字符实验里最好加上token[1] )的判断。跑通后可以用parser.expr()得到树再写个简单的树打印函数验证。3.3 LL(1) 分析表的构造步骤与 FIRST/FOLLOW 集计算如果实验要求 LL(1) 分析表步骤是1消除左递归和左公因子2对每个非终结符算 FIRST 集3算 FOLLOW 集4构造预测分析表表项是产生式。常见做法是用 Python 字典存 FIRST 和 FOLLOW然后遍历产生式填充。参数上FIRST 集看产生式右部第一个符号如果是终结符直接加入非终结符则递归FOLLOW 集看产生式右部非终结符后面的符号如果后面是空或末尾加入左部的 FOLLOW。坑在于空产生式ε的处理容易漏。我一般会写个小测试用id id * id验证分析表能否正确推导。4. 实验三语义分析与中间代码生成的落地细节4.1 语义分析要做什么符号表、类型检查与作用域实验三通常要求做语义分析核心是符号表和类型检查。符号表用来记录变量名、类型、作用域层级。常见做法是用栈式符号表进入作用域时压栈退出时弹栈。类型检查要验证表达式两边类型一致比如int float要报错或隐式转换。参数上作用域层级用整数表示查找变量时从当前层级往上找。坑在于同名变量在不同作用域的处理以及函数参数的作用域。我一般会先定义 AST 节点类型再写 visitor 遍历。4.2 三地址码生成从语法树到四元式中间代码生成常用三地址码形式如t1 a b。四元式是(op, arg1, arg2, result)。下面是一个简单的三地址码生成器遍历语法树用临时变量计数器temp_count。class ThreeAddressCode: def __init__(self): self.temp_count 0 self.code [] def new_temp(self): self.temp_count 1 return ft{self.temp_count} def generate(self, node): if node[0] NUM: return str(node[1]) op, left, right node left_val self.generate(left) right_val self.generate(right) temp self.new_temp() self.code.append((op, left_val, right_val, temp)) return temp逻辑说明generate递归处理左右子树遇到数字返回字面量遇到运算符生成新临时变量并追加四元式。参数上temp_count保证临时变量唯一code列表存四元式。跑通后可以用for quad in code: print(quad)输出。注意实验里可能要求优化比如常量折叠那是进阶内容。4.3 符号表与类型检查的联动实现符号表和类型检查要联动在遍历 AST 时遇到变量声明就插入符号表遇到变量引用就查找并检查类型。常见做法是写一个SemanticAnalyzer类维护scope_stack和symbol_table。参数上每个符号表项存(name, type, scope_level)。坑在于函数调用时的参数类型匹配以及数组下标类型检查。我一般会先实现单作用域再扩展多作用域避免一开始就复杂化。5. 避坑与排查实验一~三最常见的五个翻车点5.1 现象词法分析器把拆成和原因没有做最长匹配前瞻解决在单字符运算符判断前先检查双字符组合用peek()看下一个字符。5.2 现象递归下降分析器无限递归原因文法存在左递归比如expr - expr term解决消除左递归改成expr - term (( | -) term)*。5.3 现象LL(1) 分析表出现多重入口原因文法不是 LL(1)FIRST 集有交集解决提取左公因子或改用 LR 分析。实验里如果要求 LL(1)必须确保文法满足条件。5.4 现象语义分析时变量找不到原因符号表作用域没正确压栈弹栈解决进入{}时压栈退出时弹栈查找时从栈顶往下找。5.5 现象三地址码临时变量重复原因temp_count没有全局唯一解决用类成员变量或全局计数器确保每次new_temp()都递增。6. 进阶技巧用测试用例驱动实验验收与自动化对比实验做完不是终点能验证才算落地。我一般会写一组测试用例覆盖正常和边界情况比如空输入、非法字符、嵌套括号、多行注释。然后写个脚本自动跑对比输出是否符合预期。下面是一个简单的测试驱动脚本import subprocess test_cases [ (1 2 * 3, 7), ((1 2) * 3, 9), (10 / 2 - 3, 2), ] for expr, expected in test_cases: # 假设你的编译器输出计算结果 result subprocess.run([python, compiler.py, expr], capture_outputTrue, textTrue) actual result.stdout.strip() if actual expected: print(fPASS: {expr} {actual}) else: print(fFAIL: {expr} 期望 {expected}实际 {actual})参数说明subprocess.run调用你的编译器脚本capture_outputTrue捕获输出textTrue返回字符串。坑在于路径和编码Windows 下可能需要encodingutf-8。另外实验报告里最好附上测试用例和通过率这是加分项。从那以后我每次做完实验都强制走一遍自动化测试不然手工点容易漏。希望帮到你。本文还有配套的精品资源点击获取