资讯动态

手算算符优先分析器:从FIRSTVT/LASTVT到栈驱动实现

发布时间:2026/10/14 11:01:56 来源:尧图企业网站定制
简介本资源是一份面向计算机专业本科生与编译原理初学者的语法分析实践教学材料聚焦算符优先分析法的核心实现与验证。文档完整呈现了从FirstVT/LastVT集合求解、算符优先关系表构造含详细数组初始化代码到对典型表达式如(ii)*i和ii)*i进行归约过程输出的全流程实验报告覆盖理论推导、代码实现与结果分析三大环节。资源为单个Word文档.doc格式大小540KB内容包含华北水利水电学院实验要求、源码注释、关键函数说明init/analyse/testchar等及优先关系表数值填充细节便于对照学习与复现实验。已有131人下载学习适合课程实验参考、期末复习或编译器原理课程设计辅助尤其利于理解算符优先分析中栈操作、移进-归约判定及错误检测机制。1. 为什么手写一个算符优先分析器比调库更能让编译原理“长进脑子里”你不是没跑过yacc或ANTLR生成的语法分析器——但那黑匣子一跑就出结果中间栈怎么推、优先关系怎么查、冲突怎么消解全被封装在.c文件里看不见。而这份《编译原理_实验报告实验二__语法分析算符优先.doc》要你亲手实现的恰恰是教科书里最“反直觉”的部分不靠递归下降不靠 LR 表驱动只用一张二维优先关系表 一个字符栈 一个符号栈就能把ab*c#这种带优先级和结合性的表达式一步步规约成E。这不是玩具代码它是真实编译器前端中运算符优先解析如 Shell、SQL 解析器的简化原型它能让你看清为什么和*不能同级处理、为什么abc要左结合、为什么abc在 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就是idLASTVT(F) { ), id }(E)最右是)id就是id再看E → E TFIRSTVT(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 TB TFIRSTVT(T) { (, id, * }因T → T * F | FF的FIRSTVT是{ (, id }*直接加入→ 所以 ⋖ ( ⋖ id ⋖ *a ⋗ b当且仅当存在产生式A → …Bb…且a ∈ LASTVT(B)→T → T * FB TLASTVT(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(fInvalid token at position {input_ptr}: {b}) relation priority_table.get((a, b), None) if relation is None: raise SyntaxError(fNo 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] fshift {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] freduce to E (popped: {popped}) else: raise SyntaxError(fUnexpected 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→ET|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{i1}. [{s[stack]}] | {s[input]} | {s[relation]} | {s[action]})代码逻辑说明stack存的是混合符号终结符非终结符但比较时只取最右终结符apriority_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(fCannot 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(fUnknown 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]→ 栈顶终结符aid输入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| {i1} | {s[stack]} | {s[input]} | {s[relation]} | {s[action]} |)输出步骤栈输入关系动作1[#id]id*id#shift id这样既避免手写错位又保证和程序输出 100% 一致。老师批改时只要核对前 3 步和最后accept行就能确认你真跑通了。最后说句实在的编译原理实验不是考你会不会写代码是考你敢不敢把黑匣子拆开一颗螺丝一颗螺丝地装回去。算符优先分析器就是那个最合适的“第一颗螺丝”——它够小能手算它够真反映真实解析逻辑它够痛错一个就满盘皆输。我带过的学生里凡是把这张表亲手填满、把每步栈状态画在草稿纸上的期末编译器大作业写 LL(1) 和 LR(0) 时眼睛都是亮的。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑