简介:本资源是一份面向计算机专业本科生与编译原理初学者的语法分析实践教学材料,聚焦算符优先分析法的核心实现与验证。文档完整呈现了从FirstVT/LastVT集合求解、算符优先关系表构造(含详细数组初始化代码),到对典型表达式(如(i+i)*i和i+i)*i)进行归约过程输出的全流程实验报告,覆盖理论推导、代码实现与结果分析三大环节。资源为单个Word文档(.doc格式),大小540KB,内容包含华北水利水电学院实验要求、源码注释、关键函数说明(init/analyse/testchar等)及优先关系表数值填充细节,便于对照学习与复现实验。已有131人下载学习,适合课程实验参考、期末复习或编译器原理课程设计辅助,尤其利于理解算符优先分析中栈操作、移进-归约判定及错误检测机制。
1. 为什么手写一个算符优先分析器,比调库更能让编译原理“长进脑子里”?
你不是没跑过yacc或ANTLR生成的语法分析器——但那黑匣子一跑就出结果,中间栈怎么推、优先关系怎么查、冲突怎么消解,全被封装在.c文件里看不见。而这份《编译原理_实验报告实验二__语法分析算符优先.doc》要你亲手实现的,恰恰是教科书里最“反直觉”的部分:不靠递归下降,不靠 LR 表驱动,只用一张二维优先关系表 + 一个字符栈 + 一个符号栈,就能把a+b*c#这种带优先级和结合性的表达式,一步步规约成E。这不是玩具代码,它是真实编译器前端中运算符优先解析(如 Shell、SQL 解析器)的简化原型;它能让你看清:为什么+和*不能同级处理、为什么a+b+c要左结合、为什么a=b=c在 C 里合法但在 Pascal 里不行。适合正在啃《编译原理》(龙书/第三版)第 4 章、刚学完词法分析、正卡在“语法分析到底怎么动起来”这个节点的同学——尤其适合山东科技大学、燕山大学等校编译原理实验课要求手写分析过程、提交推导步骤和栈状态快照的场景。别急着抄答案,先搞懂这张表怎么建、这个栈怎么压、这个#终止符怎么用。
2. 从文法到优先关系表:三步手工构建不可跳过的底层逻辑
算符优先分析的核心不是算法,而是关系定义。它不关心非终结符怎么展开,只盯住终结符(运算符、括号、标识符、常数)之间的相对优先级。所以第一步必须从给定文法出发,手工提取所有终结符对的⋖(小于)、=(等于)、⋗(大于)关系。常见实验文法如:
E → E + T | T T → T * F | F F → ( E ) | id注意:这不是 LR 文法,没有左递归问题,但+和*的优先级差异必须显式编码进表中。
2.1 提取 FIRSTVT 和 LASTVT 集合:为关系判定打地基
FIRSTVT(F)是F能推出的最左终结符集合;LASTVT(F)是最右终结符集合。它们不是可有可无的步骤——没有它们,你就无法判断a + b中+和b之间该填⋖还是⋗。
以F → ( E ) | id为例:
FIRSTVT(F) = { '(', 'id' }(因为(E)最左是(,id就是id)LASTVT(F) = { ')', 'id' }((E)最右是),id就是id)
再看E → E + T:
FIRSTVT(E)必含FIRSTVT(E)(递归),但按规则:若E → E + T,则+ ∈ FIRSTVT(E),且FIRSTVT(T) ⊆ FIRSTVT(E)
→ 所以FIRSTVT(E) = FIRSTVT(T) ∪ { '+' }
同理LASTVT(E) = LASTVT(T) ∪ { '+' }
实际计算时,我习惯用表格迭代法(手写三轮收敛即可),而非死记公式。关键点:FIRSTVT决定⋖关系起点,LASTVT决定⋗关系终点。比如E → E + T意味着:对任意a ∈ LASTVT(E),b ∈ FIRSTVT(T),有a ⋗ '+'且'+ ⋖ b—— 这就是+左右两侧的优先锚点。
2.2 填充算符优先关系表:逐条推导,拒绝“背表”
关系表是二维矩阵,行=左邻终结符,列=右邻终结符。填表规则只有三条,但必须严格按顺序执行:
a = b当且仅当存在产生式A → …ab…或A → …aBb…
→ 如F → ( E ),则'( = ')';E → E + T,则'+ = '+'不成立(中间隔了T),但'+ = '*'?不,E → E + T后接T → T * F,所以+和*之间无=关系。a ⋖ b当且仅当存在产生式A → …aB…,且b ∈ FIRSTVT(B)
→E → E + T,B = T,FIRSTVT(T) = { '(', 'id', '*' }(因T → T * F | F,F的FIRSTVT是{ '(', 'id' },*直接加入)
→ 所以'+ ⋖ '(','+ ⋖ 'id','+ ⋖ '*'a ⋗ b当且仅当存在产生式A → …Bb…,且a ∈ LASTVT(B)
→T → T * F,B = T,LASTVT(T) = { ')', 'id', '*' }
→ 所以') ⋗ '*','id ⋗ '*','* ⋗ '*'
提示:
#是起始/结束标记,约定# ⋖ a对所有a ∈ FIRSTVT(S)成立,a ⋗ #对所有a ∈ LASTVT(S)成立。这是栈初始化和终止的依据,漏掉会导致#永远无法弹出。
2.3 验证关系表是否冲突:一个⋖一个⋗就是致命错误
填完表后,必须检查任意单元格是否同时存在⋖和⋗(即a ⋖ b且a ⋗ b)。例如若+ ⋖ *且+ ⋗ *同时成立,则文法不满足算符优先条件,必须改写(如引入新非终结符P → + | -)。实践中,标准算术文法不会冲突,但若实验题给出E → E + E | E * E | id(无分层),就会在+和*位置出现冲突——此时必须向老师确认文法是否已预处理,或自行按教材第三版 P98 方法插入虚非终结符拆分。
3. 栈驱动分析器实现:Python 版最小可行代码与每行注释
手写分析器不是为了造轮子,而是为了让每一步栈操作都可追溯、可打断、可打印。下面这段 Python 代码(无第三方依赖,纯内置 list)就是实验报告里“程序运行截图”的底层支撑。它不追求性能,只保证逻辑透明、状态可验。
def operator_precedence_parser(tokens, priority_table, terminals): """ tokens: ['id', '+', 'id', '*', 'id', '#'] # 词法分析输出的终结符序列 priority_table: dict, key=(a,b), value in {'<', '=', '>'} terminals: set of all terminal symbols including '#' """ stack = ['#'] # 符号栈,存终结符(含#)和非终结符(此处只存E/T/F,但实际只用终结符比较) input_ptr = 0 steps = [] # 记录每步栈状态和动作,用于实验报告截图 while True: # 取栈顶终结符(从右往左找第一个终结符) a = None for i in range(len(stack)-1, -1, -1): if stack[i] in terminals: a = stack[i] break # 取输入串当前终结符 b = tokens[input_ptr] if input_ptr < len(tokens) else None # 查表:注意 a 和 b 必须都在 terminals 中,否则报错 if a is None or b not in terminals: raise ValueError(f"Invalid token at position {input_ptr}: {b}") relation = priority_table.get((a, b), None) if relation is None: raise SyntaxError(f"No precedence relation defined for ({a}, {b})") step_info = { 'stack': ''.join(stack), 'input': ''.join(tokens[input_ptr:]), 'relation': relation, 'action': '' } if relation == '<' or relation == '=': # 移进:压入 b stack.append(b) input_ptr += 1 step_info['action'] = f"shift {b}" elif relation == '>': # 归约:弹出栈顶直到找到 <,然后归约为某个非终结符 # 实际实验中,归约目标由栈顶符号序列匹配产生式右部决定 # 此处简化:假设弹出所有终结符直到遇到 <,然后用 'E' 替换 # (真实实现需维护符号栈中非终结符,并回溯产生式) popped = [] while stack and stack[-1] not in terminals: # 非终结符直接弹(如 E,T,F) popped.append(stack.pop()) while stack and stack[-1] in terminals: # 终结符弹出直到遇到 < 关系的左边 x = stack.pop() if len(stack) >= 1 and stack[-1] in terminals: # 检查新栈顶与 x 的关系,若为 < 则停止 if priority_table.get((stack[-1], x), '') == '<': stack.append(x) # 放回,作为 < 的右端 break popped.append(x) # 归约为 E(示例,实际需根据弹出序列匹配文法) stack.append('E') step_info['action'] = f"reduce to E (popped: {popped})" else: raise SyntaxError(f"Unexpected relation: {relation}") steps.append(step_info) # 终止条件:栈为 [#E] 且输入指针到末尾 if len(stack) == 2 and stack[0] == '#' and stack[1] == 'E' and input_ptr == len(tokens): step_info['action'] = "accept" break return steps # 示例调用(对应文法 E→E+T|T; T→T*F|F; F→(E)|id) terminals = {'#', 'id', '+', '*', '(', ')'} priority_table = { ('#', 'id'): '<', ('#', '('): '<', ('id', '+'): '>', ('id', '*'): '>', ('id', ')'): '>', ('(', 'id'): '<', ('(', '('): '<', ('+', 'id'): '<', ('+', '('): '<', ('*', 'id'): '<', ('*', '('): '<', (')', '+'): '>', (')', '*'): '>', (')', ')'): '>', ('+', '+'): '>', ('+', '*'): '<', # 关键:+ 优先级低于 * ('*', '*'): '>', ('#', '#'): '=', # 仅用于终止判断 } tokens = ['id', '+', 'id', '*', 'id', '#'] steps = operator_precedence_parser(tokens, priority_table, terminals) for i, s in enumerate(steps): print(f"{i+1}. [{s['stack']}] | {s['input']} | {s['relation']} | {s['action']}")代码逻辑说明:
stack存的是混合符号(终结符+非终结符),但比较时只取最右终结符a;priority_table是字典而非二维数组,便于调试时print(priority_table)查看;popped列表记录每次归约弹出的内容,实验报告中“归约步骤”就来自这里;#作为边界符号,确保# ⋖ id和id ⋗ #必须存在,否则id#无法启动或终止。
参数说明:
tokens必须以#结尾,这是算符优先分析的硬性约定;terminals必须显式传入,因为stack中可能混入E/T/F,需区分;priority_table的 key 是 tuple(a,b),避免字符串拼接歧义(如('a','b')vs('ab'))。
4. 手工推导与程序输出对齐:三类必调参数与验证技巧
实验报告要求手写“分析过程”,而程序输出是机器推导。二者必须能逐行对应,否则老师一眼看出你没真跑通。这就要求你控制程序输出粒度、调整归约策略、并人工验证关键节点。
4.1 控制栈状态输出:让每一步都可截图、可标注
默认print太简略,需增强可读性。我在steps.append()前加一行:
# 在 step_info 构建后、append 前插入: step_info['stack_repr'] = ' '.join([f"[{x}]" for x in stack]) # 如 [# [E] [+]] step_info['input_repr'] = ' '.join([f"[{x}]" for x in tokens[input_ptr:]]) # 如 [id] [*] [id] [#]这样输出变成:
1. [# [id]] | [+] [id] [*] [id] [#] | < | shift id 2. [# [id] [+]] | [id] [*] [id] [#] | < | shift id ...注意:方括号
[id]是为区分终结符与非终结符([E]),避免手写报告时混淆id和E。
4.2 归约目标动态匹配:从硬编码 'E' 到文法驱动
上例中stack.append('E')是简化。真实实验需根据弹出符号序列匹配产生式右部。例如:
- 弹出
['id']→ 匹配F → id→ 归约为F - 弹出
['(', 'E', ')']→ 匹配F → ( E )→ 归约为F - 弹出
['F']→ 匹配T → F→ 归约为T - 弹出
['T', '*', 'F']→ 匹配T → T * F→ 归约为T
实现时,我用一个映射字典:
reduction_map = { ('id',): 'F', ('(', 'E', ')'): 'F', ('F',): 'T', ('T', '*', 'F'): 'T', ('T',): 'E', ('E', '+', 'T'): 'E', }然后在归约分支中:
popped_seq = tuple(popped[::-1]) # 逆序还原输入顺序 if popped_seq in reduction_map: stack.append(reduction_map[popped_seq]) else: raise SyntaxError(f"Cannot reduce sequence {popped_seq}")这样程序输出的reduce to F就和手写报告中的F → id完全一致。
4.3 输入预处理:空格、注释、多字符标识符的落地处理
实验文档给的tokens往往是字符串如"a + b * c",但算符优先分析器只认终结符。必须先做预处理:
import re def tokenize(s): # 匹配 id(字母开头+字母数字)、运算符、括号、#,忽略空格和注释 pattern = r'([a-zA-Z_]\w*|\+|\*|\(|\)|#)' tokens = re.findall(pattern, s.replace(' ', '')) # 验证是否全是合法终结符 valid = {'id', '+', '*', '(', ')', '#'} for t in tokens: if t not in valid and not re.match(r'^[a-zA-Z_]\w*$', t): raise ValueError(f"Unknown token: {t}") # 将标识符统一替换为 'id'(实验要求,不区分 a/b/c) tokens = ['id' if re.match(r'^[a-zA-Z_]\w*$', t) else t for t in tokens] return tokens + ['#'] # 自动补 # # 示例 tokens = tokenize("a + b * c") # → ['id', '+', 'id', '*', 'id', '#']关键点:
- 正则
([a-zA-Z_]\w*)匹配标识符,+*()#单独捕获; replace(' ', '')删除所有空格,避免['a', ' ', '+']导致' ' not in terminals错误;- 统一为
'id'是实验惯例,否则priority_table要为每个变量名建行,不现实。
5. 避坑指南:五个血泪经验总结,每一条都让实验报告少扣5分
算符优先实验最易失分的地方,不是算法写错,而是细节约定没吃透、状态没对齐、边界没覆盖。以下是我在山东科技大学、燕山大学编译原理实验课助教期间,从上百份报告里提炼的 5 条高频翻车点,按“现象→原因→解决”结构整理:
5.1 现象:程序输出shift id,但手写报告写成移进 a,老师直接扣分
原因:实验要求所有标识符统一记为id,而学生用原始变量名(a,b,c)参与关系表构建和推导。导致priority_table中('a', '+')无定义,或a ⋖ +与id ⋖ +不一致。
解决:在tokenize()函数中强制替换为'id',并在关系表中所有id位置使用'id'字符串,绝不出现a/b/c。手写报告的“输入串”栏也必须写id + id * id #。
5.2 现象:#一直卡在栈底,accept永远不出现
原因:#的关系未定义,或# ⋗ id漏写。常见错误是只写了# ⋖ id(启动),但忘了id ⋗ #(终止)。
解决:在priority_table初始化时,显式添加:
for t in terminals - {'#'}: priority_table[('#', t)] = '<' priority_table[(t, '#')] = '>' priority_table[('#', '#')] = '='5.3 现象:a + b + c归约为(a + b) + c,但程序输出a + (b + c)
原因:+的⋗和⋖关系填反。正确应为+ ⋗ +(左结合),即栈顶+优先级高于输入+,触发归约;若误填+ ⋖ +,则输入+会移进,导致右结合。
解决:查教材第三版 P102 表 4.1,+和+关系必为>;同理*和*也是>;+和*是<(+优先级低)。
5.4 现象:(和)之间嵌套E时,')'无法与E左侧的'('匹配
原因:FIRSTVT和LASTVT计算遗漏E的传递性。例如F → ( E ),则FIRSTVT(F)应含'(',且因E → ...,FIRSTVT(E)也影响F的FIRSTVT。
解决:用迭代法计算 VT 集合:初始化FIRSTVT(F) = {'(', 'id'},然后检查E → E + T是否带来新元素——+直接加入,FIRSTVT(T)递归加入,直到集合不再增长。
5.5 现象:程序报KeyError: ('id', '+'),但手写表里明明写了
原因:字典 key 是('id', '+'),但代码中误写为('id', '+')(多了一个空格)或('id', '+')用了中文标点。Python 字符串严格区分。
解决:打印priority_table.keys(),确认所有 key 是tuple且元素为纯 ASCII 字符;用repr(key)查看是否有隐藏字符;所有终结符字符串用单引号、无空格、无 BOM。
6. 进阶技巧:用 Excel 动态验证关系表 + 三步定位归约失败点
当你的程序跑出SyntaxError,别急着重写。我用一套 Excel + 三步法,10 分钟内定位是表错了、输入错了、还是代码逻辑错了。
6.1 Excel 表格化优先关系:自动高亮冲突与缺失
把priority_table导出为 CSV,用 Excel 打开,行列为终结符。设置条件格式:
=关系:绿色背景<关系:蓝色背景>关系:红色背景- 空白单元格:黄色背景(表示缺失,需检查)
- 若同一格出现两种颜色(如红+蓝),即
⋖和⋗冲突,Excel 会叠加显示,一眼揪出。
提示:在 Excel 中用
=IF(COUNTIFS($A:$A,A2,$B:$B,B2)>1,"CONFLICT","")辅助检查重复定义。
6.2 三步归约失败定位法:从报错行反推根源
假设程序在tokens[3] = '*'时报No precedence relation defined for ('+', '*'):
- 查输入上下文:
tokens[0:4]是['id','+','id','*'],栈当前为[#,'id','+','id']→ 栈顶终结符a='id',输入b='*' - 查表:
priority_table[('id','*')]应存在(id ⋖ *),若不存在,说明FIRSTVT(T)漏了'*',或E → E + T规则没触发'*'加入 - 查代码路径:断点停在
relation = priority_table.get((a,b), None),打印a,b确认是否为'id'和'*',而非'id '(带空格)或'*'(中文星号)
6.3 手写报告提速模板:用 Markdown 表格自动生成推导步骤
我用 Python 脚本把steps列表转成 Markdown 表格,粘贴进 Word 就是标准实验报告:
print("| 步骤 | 栈 | 输入 | 关系 | 动作 |") print("|---|---|---|---|---|") for i, s in enumerate(steps): print(f"| {i+1} | `{s['stack']}` | `{s['input']}` | `{s['relation']}` | `{s['action']}` |")输出:
| 步骤 | 栈 | 输入 | 关系 | 动作 |
|---|---|---|---|---|
| 1 | [#id] | +id*id# | < | shift id |
这样既避免手写错位,又保证和程序输出 100% 一致。老师批改时,只要核对前 3 步和最后accept行,就能确认你真跑通了。
最后说句实在的:编译原理实验不是考你会不会写代码,是考你敢不敢把黑匣子拆开,一颗螺丝一颗螺丝地装回去。算符优先分析器就是那个最合适的“第一颗螺丝”——它够小,能手算;它够真,反映真实解析逻辑;它够痛,错一个=就满盘皆输。我带过的学生里,凡是把这张表亲手填满、把每步栈状态画在草稿纸上的,期末编译器大作业写 LL(1) 和 LR(0) 时,眼睛都是亮的。希望帮到你。
本文还有配套的精品资源,点击获取