想象一下,你的城市里有一个老旧的十字路口,它没有红绿灯,也没有斑马线,所有的车都拥在一起。神奇的是,不管司机怎么绕路,最后都能到达市中心同一个广场——但走法完全不同:有的先左拐再直行,有的先直行再右拐,沿途看到的风景也完全不同。如果这样的路口越来越多,整个城市的交通就会陷入混乱。 在计算机的世界里,文法(Grammar) 就是这样的“路网”,而句子就是驶入路网的车辆。如果一个句子在路网里能走出多条截然不同的路线(也就是生成多棵不同的语法树),我们就说这个文法是二义的(Ambiguous)。二义性会让解析器(编译器里负责理解语法的模块)面对同一段代码时产生不同的理解——这就像司机们对“到底该走哪条路”吵个不停。 好在,城市的工程师有办法:要么给路口装红绿灯(声明优先级和结合性规则),要么干脆修一座分层立交桥(改写文法结构)。本文就用这个“城市路口”的故事,带你走一遍从识别二义、剖析根源,到用工具消歧的完整旅程。
学习二义文法与消歧,是每个编译原理学习者的必修课,也是编译器前端工程师面对成千上万行语法冲突报告时最核心的作战能力。掌握了它,你就能听懂解析器的“抗议”,并用漂亮的规则让代码恢复秩序。
可视化演示
二义文法可对同一输入产生多棵语法树,需通过文法改写或优先级规则消除歧义。本文详解二义性的不可判定性、通过改写文... 切换模式查看不同维度。
本节配有交互动效(进入视口后自动加载,约 3KB)。
一、二义文法:核心原理 动态展示关键流程与数据流动 交互控件调节参数实时渲染 f(x) = transform(x) 核心概念的可视化呈现
二义基本认识
老城中心的“塘湾路口”是出了名的怪:从城南进城的车辆,无论走左边那条贴着河的小路,还是走右边穿过集市的宽街,最后都能到同一个广场。但司机们吵翻了天——左边近却容易堵,右边远但路况好,每个人理解的“最佳路线”都不一样。在文法世界里,这个“塘湾路口”就叫二义文法:同一个句子,存在多棵不同的语法树。
技术上讲,文法是由非终结符、终结符和产生式组成的规则系统。语法树形象地展示了一个句子从开始符号出发,如何一步步展开到终结符的过程。如果存在某个句子 $w$,能够用两棵不同的语法树(等价地,两个不同的最左推导)生成,我们就把产生这个句子的文法称为二义的。最经典的例子是表达式文法 $E \to E+E \mid E\times E \mid (E) \mid \mathbf{id}$:输入 "3+4×5" 时,既可以先展开外面的加号,也可以先展开外面的乘号,两棵树的计算结果甚至不同——23 与 35。
最后一个看似轻松的“有没有两棵树”问题,实际吊诡得令人窒息:它一般不可判定。理论上不存在一个万能的检查器,能把所有有隐患的路口一次性扫描出来。这也是为什么工程师必须依靠解析器生成器的冲突报告和自身的经验来“人工巡逻”。
回到城市比喻:交警队曾经幻想开发一套自动识别“怪路口”的系统,结果数学家告诉他们此路不通。于是交警们换了个思路——与其指望自动扫描,不如在每一个新路口通车前,先请最有经验的规划师走一遍,再看看模拟车流会不会“窜道”。识别二义,靠的是眼光和工具,而不是一台万能机器。
- 存在性$L(G)$ 是文法 $G$ 生成的全部句子的集合,公式要求存在至少一个句子 $w$ 落在其中。
- 两棵树$w$ 必须能对应两棵结构不同的语法树,这说明推导路径并不唯一。
- 判定之痛对任意给定的文法 $G$,不存在一个通用算法能在有限步内回答“它是否二义”。
根源剖析
为什么“塘湾路口”会乱?规划师蹲了一整天,终于找到了病根:路口没有主干道和支路的分层——左转的车、直行的车、右转的车享有完全相同的路权,谁也没有让行的义务。文法二义也一样,根源通常不在“路修错了”,而在规则没有分层。
先看最著名的“悬空 else”问题。经典语句文法包含这样两条产生式:
除了悬空 else,表达式的二义也来自类似的根源:$E \to E+E \mid E\times E$ 把加法和乘法放在同一层级,没有规定谁的“路权”更高;连续减法 $3-2-1$ 也没有规定到底是左结合还是右结合。递归结构一旦没有级别,就像没有红绿灯的十字路口,所有方向同时放行。
规划师在调研报告里写下三行潦草的结论:第一,缺少优先级分层,导致操作符互相抢占;第二,else 这种配对型结构缺少匹配规则;第三,递归产生式没有方向约束。这三条,几乎覆盖了日常开发中 90% 的二义文法。找到病根之后,交警队心里有了底:要么立规则,要么改道路,总有一款对症药。
- 两条主产生式第一个产生式描述没有 else 的 if 语句;第二个产生式描述带 else 的 if 语句。两者并列,地位相同,else 的归属也就悬空。
- 歧义句子考虑输入 $\text{if}\ c_1\ \text{then}\ \text{if}\ c_2\ \text{then}\ s_1\ \text{else}\ s_2$,其中 $c_1, c_2$ 是条件,$s_1, s_2$ 是语句。
- 两种归属else 既可以与内层 if($c_2$)配对,也可以与外层 if($c_1$)配对,形成两棵不同的语法树,语义截然不同。
文法等价改写
交警队最终决定先试“立交桥方案”:既然路口交汇太乱,那就把车流从空间上隔开——直行的走地面,左转的走二层,右转的走匝道。文法等价改写就是这个思路:不动语言本身(生成句子的集合不变),只重新组织产生式结构,让每个句子只有一条“物理通道”。
对于算术表达式,经典改写法是引入多级非终结符,把运算符钉在不同层级的“路面上”:
与改路配套的还有“消除左递归”工程,尤其对 LL 型分析器意义重大:左递归会让递归下降分析器掉进无限循环。通用的改写公式如下:
值得一提的是,改写过程必须保证语言不变性:修一座立交桥可以改变车流走向,但不能让原本能到的广场消失,也不能凭空多出一个广场。工程师每改写一步,都要检查是否改变了文法接受的语言。
立交桥建成那天,原先那个纷乱的十字路口只剩两条笔直的通道。司机们不再纠结,因为规划师把“谁先走、谁让谁”直接浇筑进了混凝土里。这就是文法改写的魅力:规则吃进去的是清晰的结构,吐出来的是唯一的语法树。不过,立交桥造价高昂,而且不是每个路口都值得动土——于是交警队开始研究另一种更轻的方案。
- 左递归形态$A$ 的产生式以 $A$ 自身开头,形如 $A\alpha$,分析器会反复调用 $A$ 而无法终止。
- 抽取前缀把不以 $A$ 开头的选项 $\beta$ 提到最前面,作为新产生式的起点。
- 右递归接续引入新的非终结符 $A'$,将 $\alpha A'$ 接在 $\beta$ 之后,并以空串 $\varepsilon$ 终止。
优先级与结合
立交桥好是好,可工期长、造价高。交警队决定先在每个路口装一块电子指示牌:牌上写着“左转车辆待直行车辆通过后再走;右转不受限”。不用改路,规则一到,秩序就能立刻恢复。解析工具里的 优先级声明 正是这样的电子指示牌。
Yacc、Bison、PLY 等工具提供了 %left、%right、%nonassoc 声明。它们的核心思想是:给终结符规定一个“路权等级”,等级高的符号先归约(优先让行),等级低的符号后归约。其中 结合性 解决同一等级的连环运算方向问题。比如 $3-2-1$,既可以是 $(3-2)-1$,也可以是 $3-(2-1)$:
同时,不同运算符之间的优先级关系可以被形式化为一个简单的不等式:
电子指示牌的本质,是用一条条声明替代复杂的文法分层改造。它的优势是轻量、可读、易改,但代价是它只对“操作符竞争”有效——遇到悬空 else 这种结构性的二义,光靠优先级声明往往不够。交警队心里清楚:指示牌能规范直行与左转的关系,却无法替司机决定“if 家的孩子 else 该归谁带”。
于是,电子指示牌一夜之间铺满了全城所有算术路口的顶端。$+$、$-$ 与 $\times$、$/$ 各就各位,左结合右结合一目了然。但规划师在验收单上又圈出了那个老顽固:if c1 then if c2 then s1 else s2。这个路口的 else 至今还在两个 if 之间来回张望。
- 优先级函数$P$ 把每个终结符映射到一个数值等级,数值越大表示路权越高、绑定越紧。
- 分层比较乘法的优先级数值大于加法,所以在表达式 $3+4\times5$ 中,乘号两侧的内容会先结合成 $4\times5$。
悬空else破解
悬空 else 的问题就像一对闹离婚的父母争夺孩子的抚养权:else 这个“孩子”夹在内外两个 if 之间,到底跟谁走?光贴指示牌没用,因为这属于家庭内部的结构纠纷,需要一份更细致的抚养协议——也就是对文法本身做手术。
标准的破解方法,是把语句分成两类:匹配语句(M) 和 未匹配语句(U)。匹配语句内部的 else 都已经被“认领”完毕,不会再有悬挂;未匹配语句则保留尚未配对的 else:
这样改写以后,if c1 then if c2 then s1 else s2 就只有一种归法:else 必须和最内层、最近的 if 配对(因为若想与外层 if 配对,内层就必须是 $M$,而 $s_1$ 不是 if 语句无法引入 $M$ 的 else 结构)。与此同时,Yacc 等工具对这类冲突还有一个默认倾向:移进优先于归约。也就是说,看到 else 时,工具默认把它“移进”到当前 if 内部——效果上与“最近匹配”完全一致。
交警队的抚养协议最终定稿:每个 else 都明确登记在册,要么属于“已匹配家庭”,要么属于“待匹配家庭”,绝不模棱两可。从那天起,if 与 else 的“抚养权争议”在文法层面被彻底根除。当然,手工改写文法有时过于繁琐,工程师们更希望工具直接给出答案——这就要请出真正的实战兵器 Yacc 了。
- 匹配语句$M$ 的两个分支(then 部分和 else 部分)都必须是匹配语句,从结构上杜绝内部再出现悬挂的 else。
- 未匹配语句的两种形态$U$ 的第一条产生式表示 then 后面可以跟任意语句 $S$(可能是不匹配的,因此整个句子仍未配对);第二条产生式要求 then 后面的 $S$ 已经匹配,把唯一“悬挂”的机会留给最后的 $E\ \text{else}\ U$。
- 整体文法最终以 $S \to M \mid U$ 汇总,所有语句要么完全匹配、要么保持唯一的悬挂点,不再有第二种解释。
Yacc实战消歧
交警大队的指挥中心里,有一块巨大的电子总控屏:所有路口的红绿灯参数都在后台统一配置。Yacc/Bison 就是这块总控屏。它允许你在语法文件开头一次性声明所有运算符的优先级与结合性,而不必手动改写每一组产生式。这不是魔法,而是把“电子指示牌”接入了一个集中管理平台。
以算术表达式加一元负号为例,Yacc 文件通常这样写:
看到这段代码,很多初学者会困惑:“文法还是那个二义文法啊,怎么就不冲突了?”答案藏在 Yacc 生成的 LR 自动机里:每当某个状态同时面临“移进”和“归约”两种选择,Yacc 会查阅当前输入终结符的优先级。如果输入符号的优先级高于待归约产生式,就选择移进;反之选择归约。这背后的默认偏好可以写作一个简单的关系式:
配合 %prec NEG 声明,一元负号的优先级被单独抬高,-2*3 会正确解析为 -(2*3) 而不是 (-2)*3——虽然两者数学上相等,但在更复杂的表达式里,这个区分会决定程序的行为。工具生成时若未声明这些规则,屏幕上会赫然出现 17 shift/reduce conflicts 之类的警告;声明齐全后,警告归零。冲突报告的“清零”是每个解析器工程师最爱的绿灯。
总控屏亮起绿灯的那一刻,全城的路口按同一套规则运行:%left 让加减法从左往右结伴,%left 让乘除法在更高层结伴,%right NEG 让负号在最高层独行。但 Yacc 的能力边界也在此刻显现——它擅长处理“谁先谁后”的竞争,却无法替一个设计混乱的文法做慈善。红灯还需要工程师手中的“文法改写”这把钳子来修。
- 移进与归约LR 分析器中,移进是读取下一个符号压入栈,归约是将栈顶若干符号规约为一个非终结符。
- 偏好顺序在悬空 else 场景中,看到 else 时默认选择移进,即把 else 并入当前 if 语句,等价于最近匹配。
%left '+' '-'%left '*' '/'%right NEG%%expr : expr '+' expr | expr '-' expr | expr '*' expr | expr '/' expr | '-' expr %prec NEG | '(' expr ')' | NUM ;
工程实践技巧
老练的交通规划师从不一上来就大兴土木。他的工作习惯是:先看三个月车流数据,再派一台移动红绿灯车过渡观察,最后才决定要不要修立交桥。解析器工程师的实践流程,与这位规划师如出一辙。
一名有经验的工程师拿到一个新语言的文法需求,通常会走这样一条路:第一步,先写一版最接近自然描述的“松弛文法”——比如直接写 $E \to E+E \mid E\times E$,故意让冲突暴露出来;第二步,让 Yacc 打印冲突报告,对着 report 一行行数 shift/reduce 冲突发生的位置;第三步,优先用 %left、%right、%prec 声明解决操作符类冲突,因为声明式的修复成本最低、最直观;第四步,遇到声明解决不了的结构性二义(比如 C 语言的类型声明与表达式之争),再动手改写文法分层;最后一步,为文法建立回归测试集,把历史上踩过的坑全部固化成用例。
1. 全称量化:对语言 $L(G)$ 中的任意句子 $w$ 都要求成立,一个例外都不允许。
2. 唯一树计数:$|\text{Tree}(w)|$ 表示句子 $w$ 对应语法树的数量,取值必须恒为 1。
实践中有几个常用心法值得念叨:其一,LR 类工具不怕左递归,左递归还能自然表达左结合,所以不必为了“消除左递归”而消除;其二,%prec 只能修饰它紧邻的那一条产生式,别指望它隔空保护整个文法;其三,测试集要覆盖左结合(3-2-1)、右结合(a=b=c)、括号覆盖((3+4)*5)、一元负号(-2*3)等边界场景,就像给立交桥做压力测试。
规划师合上工作手册,城市的交通已经井然有序:先观察、再声明、后改写、终测试。每一句话进入编译器这座大城市,都会沿着唯一的路线抵达唯一的出口。这是理论、工具与工程智慧共同铺就的康庄大道。
AI 应用场景
编译器前端冲突诊断
阶段:语法分析器构建与调试
解决的问题:在解析器开发中,Yacc/Bison 经常报出大量 shift/reduce 冲突。传统做法是拿着屏幕逐行猜冲突原因,既慢又容易改出新问题。通过系统理解二义文法、优先级声明与文法改写,工程师可以快速从冲突报告中定位是“操作符竞争”还是“结构性二义”,并选择声明或改写方案精准修复。
具体案例:某编译器团队开发嵌入式脚本语言时,表达式文法一上来就报出 17 个 shift/reduce 冲突。他们先把运算符按优先级从低到高逐行声明 %left/%right,冲突降到 3 个;再针对剩下的冲突,用 %prec 处理一元负号,最终冲突清零,所有表达式回归测试通过。
DSL 查询语言的优先级设计
阶段:领域特定语言(DSL)规则定义
解决的问题:设计内部报表查询语言时,用户期望 a + b * c > d 与主流编程语言的语义一致。若直接发布未消歧的文法,用户会得到不可预期的解析结果。利用本文学到的优先级与结合性声明,可以在不改变文法骨架的情况下,用最低成本定义出符合直觉的运算顺序。
具体案例:某数据分析平台用 PLY 编写报表 DSL,通过 precedence = (('left', 'OR'), ('left', 'AND'), ('left', 'COMPARE'), ('left', 'PLUS'), ('left', 'TIMES')) 一行声明将整个表达式体系理顺,配合 20 个回归用例,交付后未再出现因二义导致的解析 bug。
NLP 句法分析中的歧义消除思维
阶段:句法解析与语义消歧
解决的问题:自然语言句子的二义性与编译原理中的二义文法本质同源,比如“咬死了猎人的狗”可解析为“猎人被狗咬死”或“狗被咬死”。NLP 工具通常枚举所有可能的句法树再交给下游重排。理解文法的二义结构和消歧原理,有助于设计更合理的特征与候选剪枝策略。
具体案例:知识图谱抽取管线中,工程师为关系从句设计了一套候选树生成规则,借助“最近匹配”思想,优先保留动词与最近宾语组成的结构,将解析候选从几十条压缩到前 3 条,显著提升了关系抽取的准确率。
Python 演示
import ply.lex as leximport ply.yacc as yacc tokens = ('NUM', 'PLUS', 'TIMES', 'LPAREN', 'RPAREN') t_PLUS = r'\+'t_TIMES = r'\*'t_LPAREN = r'\('t_RPAREN = r'\)'t_ignore = ' \t\n' def t_NUM(t): r'\d+' t.value = int(t.value) return t def t_error(t): raise SyntaxError('非法字符: ' + t.value[0]) lexer = lex.lex() # 优先级声明:按从低到高排列,后声明的优先级更高precedence = ( ('left', 'PLUS'), ('left', 'TIMES'),) # 若去掉 precedence,PLY 会报 shift/reduce conflictdef p_expr(p): '''expr : expr PLUS expr | expr TIMES expr | LPAREN expr RPAREN | NUM''' if len(p) == 2: p[0] = p[1] elif p[2] == '+': p[0] = p[1] + p[3] else: p[0] = p[1] * p[3] def p_error(p): raise SyntaxError('语法错误') parser = yacc.yacc() if __name__ == '__main__': for s in ['3 + 4 * 5', '(3 + 4) * 5', '2 * 3 + 4 * 5']: print(s, '=', parser.parse(s))
precedence 声明消除 E → E + E | E * E 这类二义文法,并正确计算表达式的值。tokens = ('NUM', 'PLUS', 'TIMES', 'LPAREN', 'RPAREN')声明词法记号集合,后续产生的规则都围绕这些记号展开。t_PLUS = r'\+'用正则表达式把字符+映射为 PLUS 记号,让词法分析器认识加号。precedence = (('left', 'PLUS'), ('left', 'TIMES'))核心救星:把乘号声明在加号之上,同时规定两者都是左结合。def p_expr(p):定义语法规则,p[0]是归约结果,p[1]起是产生式右侧的各个符号。if len(p) == 2:对应expr : NUM的归约,直接把数字返回。p[0] = p[1] + p[3]处理expr PLUS expr,把左右两个子表达式的值相加。parser = yacc.yacc()让 PLY 根据规则生成 LALR 解析器,若存在未解决的冲突会在此刻打印警告。print(s, '=', parser.parse(s))用三个用例验证解析结果,3 + 4 * 5应输出 23 而不是 35。
常见误区
误区“我看到文法的左递归就害怕,以为只要把左递归全部消掉,二义性就自动消失了。”
正解左递归与二义性是两码事。文法 $E \to E + T \mid T$ 存在左递归,但完全无二义;而 $E \to E + E \mid E \times E$ 没有直接左递归改写时同样二义。消除左递归是为了让 LL 分析器避免无限循环,属于分析算法的适配手段;消歧则是为了让每个句子只有一棵语法树,属于文法结构的重新组织。二者目标不同、手段不同,不可混为一谈。
为何常见:初学者往往先学到“左递归会死循环”,再学到“二义不好”,大脑很容易把两个“不好的东西”划上等号。
误区“只要 Yacc 报了 shift/reduce conflict,说明我的文法一定是二义的,必须把冲突全部消灭。”
正解shift/reduce conflict 是 LR 自动机中的一种状态竞争,它可能是二义性的信号,也可能只是默认策略下无害的选择。比如悬空 else 文法在 Yacc 中报冲突,但默认移进策略产生的行为恰好就是最近匹配,语义完全符合预期;此时不去改写文法,反而能得到更简洁的语法文件。工程师判断的依据是“冲突产生的语义是否符合语言设计”,而不是盲目追求零冲突。
为何常见:工具的输出是红色警告,人的第一反应总是见红就报警;加上很多教程都把“零冲突”当作目标,导致教条化理解。
误区“只要把 %left、%right 声明写满,任何二义文法都能被救回来。”
正解优先级声明擅长处理“同一结构中不同运算符竞争”的场景,但它无法应对结构层面的二义。例如悬空 else 虽然能靠默认移进策略兜底,但 C 语言“类型声明 vs 表达式”的经典两义性,靠声明根本无从下手,必须回到文法改写。优先级声明本质是给 LR 自动机提供比较规则,它不会改变文法本身的结构。
为何常见:看到几个例子中声明一行就解决了问题,容易高估工具的能力,忽略了工具背后的适用边界。
误区“优先级越高绑定越紧,那我干脆把所有运算符都声明成最高优先级,绑定越紧越不容易出错。”
正解优先级的意义在于相对高低,而不是绝对数值。所有运算符同一优先级时,它们之间的竞争退化为“先来后到”,结合性会接管一切——但这往往是错误的语义。比如 3 + 4 * 5 若加法和乘法同级且都是左结合,会解析成 (3+4)*5,结果从 23 变成 35。正确的做法是仔细设计每一层运算符的相对顺序,并在测试用例中验证。
为何常见:“越高越好”是日常生活中常见的朴素直觉,但在需要精细排序的文法世界里,绝对高低毫无意义,相对顺序才是王道。
误区“二义文法就是坏文法,永远不应该出现在正式代码里。”
正解二义文法作为语言的定义方式往往更简洁、更接近人的直觉,例如 $E \to E + E \mid E \times E$ 一眼就能看出表达式的全部结构。而且存在固有二义语言——任何等价文法都是二义的,无论怎么改写都救不回来。对这类语言,工程上只能容忍一定的歧义,依靠语义分析和后端处理来区分意图。二义性不是原罪,不加处理才是问题。
为何常见:教科书为了强调危害,常把二义文法塑造成反面教材;读者缺少对固有二义语言和工具兜底机制的认识,容易形成非黑即白的判断。
面试题
Q1. 一个文法 G 是二义的,指的是:
- A.G 中所有产生式都含有递归
- B.存在某个句子 w,能由两棵不同的语法树推导出来← 正确答案
- C.文法包含超过 100 条产生式
- D.文法无法生成任何字符串
答案:B。二义性的核心定义是“一个句子对应多棵语法树”。递归只是产生式的形式特征,与二义性无必然联系;产生式数量、是否能生成字符串都与二义性无关。就像同一个路口能走出两条不同的路,才叫“怪路口”。
Q2. 关于“判断一个任意上下文无关文法是否二义”的问题,正确的说法是:
- A.存在 O(n) 时间的多项式算法
- B.可以用动态规划完美解决
- C.该问题不可判定← 正确答案
- D.只有极少数文法无法判断
答案:C。二义性判定问题在理论上被证明不可判定——不存在一个通用算法,能在有限步内回答任意文法是否二义。这是形式语言理论中的经典结论。因此工程师只能依靠工具冲突报告、人工审查和测试用例来发现二义,而不是指望自动化检查器。
Q3. 在经典的悬空 else 文法中,Yacc 等解析器生成工具默认选择的冲突处理策略是:
- A.归约优先
- B.移进优先← 正确答案
- C.随机选择
- D.立即报错终止
答案:B。面对“移进 else”与“按 if 语句归约”的冲突,Yacc 默认选择移进,效果是 else 与最近的 if 配对。这与大多数编程语言的语义一致,也是悬空 else 问题能靠工具默认行为缓解的原因。
Q4. 在 Yacc/Bison 文件中,若要声明 + 和 - 为左结合且优先级低于 * 和 /,正确的写法是:
- A.
%left '+' '-'在前,%left '*' '/'在后← 正确答案 - B.
%left '*' '/'在前,%left '+' '-'在后 - C.
%right '+' '-'在前,%right '*' '/'在后 - D.只需要声明一次
%left即可
答案:A。Yacc 规定后声明的优先级更高,因此要让乘法高于加法,必须先声明 %left '+' '-',再声明 %left '*' '/'。若顺序颠倒,加法反而变得比乘法绑定更紧,计算结果会完全错误。
Q5. 对文法 E → E + E | E * E | ( E ) | id,若希望乘法优先于加法且两者都左结合,在 PLY 中的正确声明是:
- A.
('left', 'TIMES'), ('left', 'PLUS') - B.
('left', 'PLUS'), ('left', 'TIMES')← 正确答案 - C.
('right', 'PLUS'), ('right', 'TIMES') - D.不需要任何声明,PLY 默认正确
答案:B。PLY 与 Yacc 规则一致,precedence 元组中靠后的符号优先级更高。先声明 PLUS 再声明 TIMES,乘法优先级才会高于加法。选项 A 会让加法优先,选项 C 把结合性搞反,选项 D 则会产生大量冲突警告且计算结果为 35 而非 23。
Q6. 下列哪些手段可以用于消除或缓解文法二义:
- A.将表达式文法改写为多层非终结符结构← 正确答案
- B.声明运算符的优先级与结合性← 正确答案
- C.增加更多左递归产生式
- D.利用生成工具的默认移进策略处理悬空 else← 正确答案
答案:ABD。改写文法分层(立交桥)和声明优先级(电子指示牌)是两种经典消歧手段;利用 Yacc 默认“移进优先”策略可以缓解悬空 else 的歧义。增加左递归只会加剧推导路径的多样性,并不能消除二义。
Q7. 在 LR 分析器的某个状态中,同时存在“读入下一个符号并压栈”与“按产生式把栈顶符号归约”两种可选动作,这种冲突被称为:
- A.归约-归约冲突
- B.移进-归约冲突← 正确答案
- C.语法栈溢出
- D.状态死锁
答案:B。同时有移进和归约两个选项时称为 shift/reduce conflict,同时有两个不同归约选项时称为 reduce/reduce conflict。悬空 else 正是典型的 shift/reduce conflict,而移进-归约冲突未必代表二义,也可能只是默认策略下的无害竞争。
Q8. 关于二义文法的说法,正确的是:
- A.改写文法时,必须保证新文法与原文法生成的语言相同← 正确答案
- B.通过优先级声明后,每个句子的语法树结构可以变得唯一← 正确答案
- C.所有二义文法都能改写成等价的无二义文法
- D.消除左递归等同于消除二义
答案:AB。文法改写必须保持语言不变(立交桥不能改变可达广场的集合);优先级声明通过比较规则让操作符的竞争有了确定胜负,语法树也因此唯一。但存在固有二义语言,任何等价文法都二义,所以选项 C 错误;左递归与二义无关,选项 D 错误。
Q9. 二义文法最直接的危害是:
- A.编译速度变慢
- B.同一句子可能产生不同的语义← 正确答案
- C.无法识别任何单词
- D.生成的代码体积一定变大
答案:B。二义意味着同一段代码可以对应两棵语法树,而不同的树在语义分析阶段可能被翻译成不同的行为——这比编译慢几步严重得多。编译速度和代码体积虽然也可能受影响,但那只是次要现象,真正的危害是语义不确定。
Q10. 已知语言 L 是固有二义语言(即任何等价的上下文无关文法都是二义的),下列说法正确的是:
- A.存在一个无二义的上下文无关文法生成 L
- B.任何等价上下文无关文法都存在某个句子对应两棵语法树← 正确答案
- C.可以通过 %left 声明让 L 的二义完全消失
- D.L 不是上下文无关语言
答案:B。固有二义语言是形式语言理论中的反例——无论工程师如何改写文法,总存在至少一个句子拥有两棵不同的语法树。这说明“消歧”并不是万能的,有些语言在上下文无关文法框架下注定带刺。%left 只能规范操作符竞争,无法消除结构性二义;而 L 本身是上下文无关语言,只是每一种文法都二义。