想象你同时扮演两个角色:一位话剧导演和一位乐高拼装师。 导演的工作是拿着完整剧本,从第一幕开始,根据演员念出的每一句台词,判断剧情该往哪里走——这叫“自顶向下”的 LL 解析,导演就是 LL 解析器。乐高拼装师则完全不同,他不需要说明书,只凭手里积木的形状,看到几块能拼成标准组件就顺手拼好,组件越来越大,最后拼出整个模型——这叫“自底向上”的 LR 解析。 编译器把高级语言翻译成机器码,也像一场精密排练。在“语法分析”环节,程序员的代码(token 流)是演员的台词,而编程语言的语法规则(文法)就是剧本。LL 与 LR 正是两大流派解析器的代表。这篇文章带你从零理解这两种解析器的工作原理、分析表构建、冲突处理,以及各自的强项。学完你会发现:原来编译器的“阅读理解”,就是一门导演与拼装师的统筹艺术。
对 AI 从业者而言,语法分析是理解语言模型“结构理解”、构建结构化数据解析系统的基石。掌握 LL/LR 原理,等于拿到了阅读解析器源码、驾驭 Yacc/Bison/ANTLR 等工具的钥匙。
目录
可视化演示
对比LL自顶向下预测分析与LR自底向上移进归约两种语法解析范式,展示解析树构建方向、栈操作流程及分析表结构差异。
本节配有交互动效(进入视口后自动加载,约 3KB)。
解析过程对比 LL解析器从起始符号S出发,自顶向下展开解析树,通过FIRST集预测下一个产生式 LR解析器从输入串出发,自底向上移进归约,通过ACTION/GOTO表驱动栈操作 LL: S→AB | LR: ab→A→S LL(1)简单但表达力弱,LR(1)强大但实现复杂。
语法分析概览:解析器的“剧本与积木”
不管是导演还是拼装师,开工前都要有一份“总纲”。导演拿的是剧本——每一幕每一场怎么演写得明明白白;拼装师拿的是说明书——每个模型由哪些标准组件组成。在编译世界里,这份总纲就叫文法(Grammar)。
编译器读入源代码,先做词法分析,把字符流切成一个个 token,就像把一整篇文章切成带词性的单词;紧接着就是语法分析,负责按文法把这些“单词”组装成有结构的句子——语法树。这一层是编译器的“阅读理解”环节,语法树生成得对不对,直接决定后续语义分析和代码生成的质量。
文法由一条条产生式组成,比如下面的规则描述了一个最简表达式:一个表达式可以是一个表达式加一个项,也可以直接是一个项。
所以,无论导演方式还是拼装方式,大家都服务于同一份“剧本”(文法);差别只在于——谁从头念到尾,谁从碎片拼到顶。理解这一点,你就握住了两大解析流派的共同根基。
| 表示“或者”,是给导演和拼装师看的剧本选项。- 符号 E代表“表达式”这个语法范畴,属于非终结符——它还要继续展开,不能直接出现在最终串中。
- 推导箭头表示“可以展开为”,是产生式的核心关系,左边是左部,右边是右部。
- 右部 E+T表示递归地将 E 展开成“一个 E、一个加号、一个 T”,这是处理二元运算的经典写法。
- 竖线分隔表示文法为 E 提供了两种可选展开方式,解析器需要在两者间做选择,而“怎么选”正是 LL 与 LR 的分水岭。
LL自顶向下:导演按剧本排戏
导演拿到剧本后,从第一幕(开始符号)开始读。比如剧情是“一个表达式”,剧本写着 E → T+E,导演就布置三场戏:先演 T,再演 +,最后演 E。每场戏继续按各自的剧本展开,直到所有台词(终结符)与观众听到的完全一致。
这个“边念台词、边定下一步”的过程就是 LL 预测分析。LL 中第一个 L(Left-to-right)表示从左向右读输入,第二个 L 表示产生最左推导;括号里的 1 表示每次只需看一眼当前 token 就能决定下一步动作。导演因此自信满满:台词一出口,我立刻知道剧本翻到哪一页。
递归下降是 LL 家族最直观的实现——为每个非终结符写一个函数,函数内部按产生式逐个“调用”其他函数,代码几乎就是文法的一对一翻译。但导演对剧本有硬性要求:剧本不能“循环套娃”。像 E → E+T 这种左递归规则,会让导演永远往下钻、无法停止,必须先改写为等价的右递归形式。
LL 导演的秘诀是:剧本必须“一眼可断”——任何时刻看到一句台词,就能唯一确定下一步排哪场戏。如果一份文法能让导演随时做到这一点,它就被称为 LL(1) 文法。
- 推导记号表示“一步推导”,即把当前串中的某个非终结符按产生式替换成右部。
- 第一步展开把 E 按 E→T+E 展开,得到 T+E,导演先排“T 这场戏”。
- 展开 T 为 F继续把 T 按 T→F 展开,演出计划变成"F+E"。
- 落到终结符F 最终展开为 id(标识符),推导完成,台词与输入对齐。
FIRST与FOLLOW:导演的场记表
导演怎么做到“一眼可断”?他需要两本场记表。FIRST 表记录:“这场戏的第一句台词可能是什么”;FOLLOW 表记录:“这场戏演完后,观众接下来可能听到哪句台词”。两张表一配合,导演查表即可决策。
FIRST 集的计算是递归的:终结符的 FIRST 集就是它自己;非终结符的 FIRST 集,是其所有产生式右部首符号的 FIRST 集的并集;若某个右部能推导出空串 ε,还要继续看下一个符号。FOLLOW 集的计算则从开始符号出发,沿产生式右部“往后看”,收集可能紧跟其后的终结符。
有了这两张表,就能构建预测分析表:表的行是非终结符,列是终结符,格子里填“该用哪条产生式展开”。只要每个格子里至多有一个产生式,LL(1) 分析就畅通无阻;否则导演就会在岔路口迷茫。
LL(1) 中的"1"就建立在这两张表上:任何时刻查表都必须恰好有一格有值。场记表越精确,导演就越镇定;表上一旦出现空格或有重复,戏就排不下去了。
- FIRST(α)求符号串 α 的首终结符集合,它是预测分析表的核心索引。
- 集合构造式用一个条件来定义集合中的元素,竖线前是元素形式,竖线后是条件。
- 多步推导表明 α 可以经过零步或任意有限步推导,变成以终结符 a 开头的串。
- 终结符集合a 必须属于终结符集合 V_T,即最终要落在具体的单词上。
LR自底向上:拼装师的魔力
镜头切到拼装师。他不需要剧本,只需要把零件一个个拿起来——这个动作叫“移进”。手里攒了几个零件,发现它们的形状正好能拼成一个“项”(T),就啪嗒一声拼起来——这个动作叫“归约”。拼好的部件继续参与下一轮拼装,直到最后拼出完整的表达式模型。
移进-归约分析的流程很机械但很强大:分析器维护一个栈,每次把当前输入 token 压入栈顶(移进);一旦栈顶的若干符号恰好匹配某条产生式的右部(称为句柄),就用产生式左部替换它们(归约)。整个过程其实就是“最右推导的逆过程”——拼装师每归约一次,就是在逆着推导方向走一步。
LR 的含义:L 仍表示从左向右读输入,R 表示“最右推导的逆”。括号里的 1 表示归约时向前看一个符号。LR 分析器本质上是一个精密的自动机,所有“移进还是归约”的动作都预先写在分析表里,运行时只需机械查表。
拼装师的厉害之处在于:他不需要预先知道全貌,只凭局部形状判断“拼还是不拼”,照样能完成最复杂的拼装——这正是 LR 表达力强于 LL 的深层原因:决策永远基于已经看到的局部事实,而不是全局预测。
- 当前局面栈中已经积累了 E + id,输入中还剩后续符号,拼装师盯着手里的零件。
- 第一次归约id 是 T→id 的右部,把 id 替换为 T,栈变成 E + T。
- 第二次归约E+T 恰好是 E→E+T 的右部,整个替换为 E,拼出模型顶部。
- 逆推导方向所有箭头都是可逆的;LR 做的是从右往左读推导,LL 是从左往右推导。
LR分析表构建:状态机的幕后图纸
拼装师怎么知道“何时该拿、何时该拼”?他手上有一张精密的状态图。图中的每个状态用 LR(0) 项目描述,比如 E→E·+T,圆点表示“我已经看到了 E,正在等待一个 +”。
把所有可能的状态以及状态之间的转移连起来,就得到项目集规范族——本质是一个确定有限自动机(DFA)。每个状态记录“拼到一半”的所有可能情况,遇到不同输入符号就跳转到下一个状态。SLR 分析法在归约时用 FOLLOW 集过滤掉不可能的归约;LR(1) 则让每个项目额外携带一个向前看符号,决策更精细;LALR(1) 将“核心相同”的状态合并,在表大小与表达能力之间取得平衡。
Yacc/Bison 这类工具就是自动完成这套构建流程:输入文法,工具内部生成项目集规范族,再输出一张驱动解析器的分析表。你不需要手工画状态图,但理解这张图,才能看懂工具打印的冲突警告。
项目集规范族是 LR 家族共同的“幕后图纸”:SLR、LR(1)、LALR 的区别,只在于给这些状态补充了多少“向前看”的侦察信息。侦察越细致,决策越准,但状态也越多。
- 项目左部E 是归约目标的非终结符,表示拼到一半的组件类型。
- 圆点位置圆点前的 E 已出现在栈顶,说明本状态关心的是“如何继续拼成更大的 E”。
- 圆点后的符号加号 + 是当前正在等待的终结符,看到它才能继续前进。
- 状态转移语义每个圆点位置不同,对应 DFA 中一个不同的状态;圆点跨过符号的过程就是状态转移。
冲突处理:岔路口的选择题
无论导演还是拼装师,都会遇到岔路口。拼装师手里状态显示:既可以“继续拿起下一个零件”,又可以“把现有零件拼起来”——这就是移进-归约冲突;另一种情况是同一撮零件既能拼成 A 又能拼成 B——这就是归约-归约冲突。
绝大多数编程语言文法的冲突,可以用优先级与结合性化解。就像生活中“先乘除后加减”一样,Yacc 里一句 %left '+'、%left '*' 就能告诉解析器:乘号优先级更高,遇到冲突时先归约。结合性则处理连写问题:a-b-c 是 (a-b)-c 还是 a-(b-c)?左结合就选前者。
如果冲突无法用优先级解决,就需要动手改造文法:提取公共因子、改写递归结构、引入新的非终结符分层。这就像把“一团乱麻”的岔路口改造成“立交桥”——每一层只走一个方向,自然不冲突。
冲突处理是解析器工程中最“接地气”的部分。导演的剧本和拼装师的手感都难免遇到模棱两可,好在有优先级、结合性和文法改造这三件法宝——把岔路口改造成红绿灯井然的十字路口,解析才能一路畅通。
- 两个候选右部α 和 β 是同一非终结符的两条不同产生式的右部,代表两个可选方向。
- 集合交集∩ 表示两个集合共同拥有的元素;交集非空说明存在一个 token 同时是两个方向的开头。
- 空集条件交集等于空集,才保证任意时刻至多一个产生式可选,预测表才无冲突。
表达力对比:两种手艺的较量
拼装师的能力上限比导演更高。形式语言理论给出了清晰的包含链:LL(1) 文法类 ⊂ LALR(1) 文法类 ⊂ LR(1) 文法类。也就是说,凡是 LL 能处理的文法,LR 几乎都能处理;但反过来,很多 LR 可解析的文法(比如包含左递归的文法),LL 需要先改写才能用。
但导演有导演的优势。LL 自顶向下推进,一旦第一个 token 不符合预期,立刻就能报错——错误定位靠前、提示直观,非常适合交互式场景;而 LR 要等栈里攒够信息才做归约,发现错误时往往已经读了很远,错误定位相对滞后。LL 的预测表也小得多,逻辑直观,手写实现、断点调试都更轻松。
实际工程中,编程语言的语法通常比较复杂,包含大量左递归规则;若交给工具自动生成,LALR 是性价比之王(Yacc 默认);若团队想要友好错误提示和灵活扩展,手写递归下降的 LL 风格反而更受青睐。
两种手艺没有绝对的优劣,只有匹配的场景。导演适合“边演边看、随时纠错”的舞台,拼装师适合“默默堆叠、最后成型”的流水线。理解了表达力差异,你就知道在何种场景该请哪位出场。
- LL(1)最左侧,要求最严格、表最小,适合手写。
- 严格包含⊊ 表示“真子集”,左边集合严格包含于右边,且不等于右边。
- LALR(1)位于中间,Yacc 的默认选择,兼顾表大小与文法适用范围。
- LR(1)最右侧,能力最强,状态数也最多,常用于理论研究与小规模文法。
工具与实践:选择你的工具包
导演和拼装师都有成熟的工具链。导演系代表是 ANTLR,也有大量手写递归下降解析器;拼装师系则以 Yacc 与 Bison 为旗帜——只要提供文法,它们自动完成项目集构建、冲突检测并生成 C 代码解析器。
现实世界的选型往往是工程权衡。MySQL 使用 Bison(LALR)处理复杂 SQL 文法;Go 语言的编译器则完全手写递归下降,换取友好的错误信息和流畅的增量编译体验;Rust、Zig 等新语言也偏爱手写 LL 风格解析器,以便给出“这里缺了个分号”这类贴心提示。选择依据很简单:你的语言文法有多复杂?你需要多友好的错误提示?你的团队能不能维护一个自动生成的巨大分析表?
下面是一段 Bison 风格文法的骨架示意,它背后的形式文法就是本节关注的核心。
从文法理论到工具落地,是一条成熟的链路:你只需描述“剧本该怎么演”,Bison 或手写解析器负责把剧本变成可执行的分析逻辑。理解了 LL 与 LR 的原理对比,你就能在这条链路的源头做出理性选择,而不是被工具的默认行为牵着走。
expr: expr '+' term | term; 背后的文法形式。工具拿到这条规则,就为拼装师准备了一条“看到 E+T 就归约为 E”的组装指令。- expr语法树的根概念——表达式,对应 Yacc 规则左部。
- 递归右部expr+term 是递归形式,允许无限长的加法链,如 a+b+c。
- 终结符加号Yacc 中用单引号包裹的字面量就是终结符 token。
- 终止选项 term提供递归出口,表达式最终落到无法再拆的项上。
AI 应用场景
编译器前端开发:用 Bison 搭建新语言解析器
阶段:语法分析(Compiler Frontend)
解决的问题:设计一门新的 DSL(领域专用语言)时,手写解析器不仅耗时且易出错。传统做法是人工维护递归下降代码,文法一变就要大改函数逻辑;用 Bison 只需维护文法文件,工具自动生成 LALR 解析器 C 代码,文法调整后重新生成即可,把“导演的即兴发挥”变成“拼装师的流水线作业”。
具体案例:某团队设计配置语言,文法包含嵌套块与运算符优先级。用 Bison 声明 %left '+'、%left '*' 后,冲突自动消解,生成的分析器在 500 万行配置上解析零歧义。
IDE 语法高亮与代码补全:递归下降的友好错误
阶段:增量解析(Incremental Parsing)
解决的问题:IDE 需要在用户敲代码的瞬间给出反馈,错误信息必须精确到“第几行缺了什么”。LR 分析器错误定位滞后,LL 风格的手写递归下降则能边读边报,天然适合增量重解析——用户改了一个 token,只需重解析受影响的子树。
具体案例:Go 语言的 gopls 语言服务器使用手写递归下降解析器,在用户未输完的代码上也能生成不完整语法树,实现自动补全与括号匹配。
SQL 解析与查询优化:LALR 在数据库中的应用
阶段:查询编译(Query Compilation)
解决的问题:数据库系统每秒要解析大量 SQL,解析器既要快又要稳。SQL 文法含有大量左递归规则,LR 家族天然支持,无需改写;用 Bison 生成 LALR 解析器,语法树直接喂给查询优化器,效率与可维护性兼得。
具体案例:MySQL 的 sql_yacc.yy 文法文件包含上千条产生式,经 Bison 生成解析器后,一条复杂的联表查询在微秒级完成解析并进入优化环节。
Python 演示
# -*- coding: utf-8 -*-# LL(1) 预测分析器演示:解析 id + id * id# 文法(已消除左递归):# E -> T E'# E' -> + T E' | ε# T -> F T'# T' -> * F T' | ε# F -> ( E ) | id FIRST = { 'E': {'id', '('}, "E'": {'+', 'ε'}, 'T': {'id', '('}, "T'": {'*', 'ε'}, 'F': {'id', '('},} FOLLOW = { 'E': {')', '$'}, "E'": {')', '$'}, 'T': {'+', ')', '$'}, "T'": {'+', ')', '$'}, 'F': {'*', '+', ')', '$'},} TABLE = { 'E': {'id': ['T', "E'"], '(': ['T', "E'"]}, "E'": {'+': ['+', 'T', "E'"], ')': ['ε'], '$': ['ε']}, 'T': {'id': ['F', "T'"], '(': ['F', "T'"]}, "T'": {'*': ['*', 'F', "T'"], '+': ['ε'], ')': ['ε'], '$': ['ε']}, 'F': {'id': ['id'], '(': ['(', 'E', ')']},} def ll1_parse(tokens): """用预测分析表检查 token 串是否被文法接受""" tokens = list(tokens) + ['$'] stack = ['$', 'E'] print('栈'.ljust(16), '输入'.ljust(16), '动作') print('-' * 54) while stack: top = stack[-1] lookahead = tokens[0] print(str(stack).ljust(16), str(tokens).ljust(16), end=' ') if top == lookahead: stack.pop() tokens.pop(0) print('匹配 ' + lookahead) elif top in TABLE and lookahead in TABLE[top]: stack.pop() rule = TABLE[top][lookahead] if rule != ['ε']: stack.extend(reversed(rule)) print('展开 ' + top + ' -> ' + ' '.join(rule)) else: print('语法错误!') return False print('分析成功!') return True if __name__ == '__main__': ll1_parse(['id', '+', 'id', '*', 'id'])
TABLE = {...}预测分析表是 LL(1) 的“剧本索引”:行是非终结符,列是终结符,格子里存放要展开的产生式。stack = ['$', 'E']分析栈初始装有“结束标记 + 开始符号”,模仿导演手里最初的剧本页。top = stack[-1]每次只看栈顶符号,这是 LL 分析的“一眼可断”决策点。if top == lookahead栈顶与当前 token 相同,说明台词与剧本一致,弹出并推进输入。rule = TABLE[top][lookahead]栈顶是非终结符时查表,找到唯一正确的展开方式。stack.extend(reversed(rule))把产生式右部逆序压栈,保证左端符号在栈顶先被处理。if rule != ['ε']ε 产生式代表“空戏”,只弹出非终结符,不压入任何新符号。print('展开 ' + top + ' -> ' + ...)输出推导过程,让你看清导演每一步翻的剧本页。
常见误区
误区很多人以为 LL 是“预测式”的,LR 是“回溯式”的,所以 LL 更快,是更先进的技术。实际刚好相反,LR 在编译原理课程里通常是更“高级”的内容。
正解两类分析器的时间复杂度都是 O(n),严格线性扫描输入,性能差异微乎其微。就像导演和拼装师都能在相同时间内完成任务,区别不在速度,而在谁能处理更复杂的剧本。LR 表达力更强,能处理含左递归的自然文法;LL 需要改写文法,但错误定位更早。选型依据是表达力、错误提示和维护成本,而不是“快慢”。
为何常见:人们常把“自顶向下”与“递归”联想为简单直接,把复杂机制误判为慢速;且教学上常先讲 LL 后讲 LR,容易形成“后者更高级”的错误印象。
误区听说“左递归会导致死循环”,就以为 E→E+T 这种规则在任何解析器里都不可用,必须统统改写。
正解只有自顶向下的 LL 系解析器害怕左递归——导演按剧本推导,遇到 E→E+T 会无限钻下去。而 LR 系解析器天然支持左递归:拼装师是从输入往上游走,E+T 出现在栈顶时直接归约,毫无压力。所以 Yacc 里写 expr: expr '+' term 完全合法,这也是现实中 LR 更省心的原因之一。
为何常见:多数入门课程以 LL 开篇,递归下降的无限递归问题被反复强调,导致学 LR 时仍带着“左递归=万恶”的惯性思维。
误区以为只要按照算法算出 FIRST 集和 FOLLOW 集,预测分析表就自动完美,解析一定顺利。
正解FIRST/FOLLOW 只是构建表的原料。表建好后还必须逐格检查:同一格出现两个产生式说明有冲突;有些文法无论如何计算都不满足 LL(1) 条件,需要改造文法。好比场记表写得再全,剧本本身“一场戏两种演法”,导演照样卡壳。
为何常见:教材例题大多精心挑选了满足条件的文法,给人“按步骤算完就万事大吉”的错觉;实际工程中的文法几乎都要迭代调整。
误区看到 Bison 输出"shift/reduce conflict"警告就紧张,以为文法写错了,必须改到零警告为止。
正解Yacc 对冲突有默认策略——冲突时优先移进、按声明优先级处理,很多场景下默认行为正是你想要的。比如悬空 else 问题,Yacc 默认移进就实现了“就近匹配”语义。冲突警告是提醒你“这里存在多种解读”,需要确认默认选择是否符合语言设计意图,而不是一律视为错误。
为何常见:教学强调“冲突=LL(1) 失败”,但工程工具把冲突变成了可配置的语义选择,两种语境的目标不同,初学容易混淆。
面试题
Q1. LL 解析器属于哪一类语法分析方法?
- A.自顶向下预测分析← 正确答案
- B.自底向上移进归约
- C.算符优先分析
- D.通用 Chart 分析
答案:A。LL 从开始符号出发,按产生式逐步向下展开,直到匹配输入 token,本质是“导演照剧本排戏”,属于自顶向下预测分析。LR 才是自底向上移进归约。
Q2. LR 分析中的“移进”动作具体指什么?
- A.把当前输入 token 压入分析栈← 正确答案
- B.用产生式左部替换栈顶右部
- C.展开栈顶的非终结符
- D.从头重新扫描输入
答案:A。移进就是拼装师“拿起一个零件”——把输入符号压入栈中;归约才是“把零件拼成组件”,即用产生式左部替换右部。混淆两者是最常见的入门错误。
Q3. FIRST 集的定义是?
- A.从某符号出发可能推导出的所有串的集合
- B.从某符号出发可能推导出的串的首个终结符的集合← 正确答案
- C.可能出现在某非终结符之后的终结符集合
- D.所有产生式右部的符号集合
答案:B。FIRST 回答“这场戏第一句台词可能是什么”,即推导结果的首个终结符集合;FOLLOW 才回答“之后可能听到什么”。选项 A 范围太大,C 是 FOLLOW 的定义。
Q4. 文法 S → aS | b 是 LL(1) 文法吗?
- A.是← 正确答案
- B.否,因为含左递归
- C.否,因为 FIRST 集相交
- D.否,因为存在二义性
答案:A。这是右递归文法,两条产生式开头分别是 a 和 b,FIRST 集不相交,无左递归、无冲突,满足 LL(1) 条件。左递归才需要改写,右递归对 LL 完全友好。
Q5. LR 分析中“句柄”指的是?
- A.当前输入串的最右终结符
- B.栈顶能与某产生式右部匹配、且归约后形成规范句型的符号串← 正确答案
- C.栈中任意一段符号串
- D.语法树最左侧的叶子节点
答案:B。句柄是拼装师“认出的那一撮能拼成组件的零件”——它必须匹配某条产生式右部,并且归约方向符合最右推导的逆过程。任意符号串不算,最右终结符也不算。
Q6. Yacc 遇到移进-归约冲突时的默认处理策略是?
- A.优先归约
- B.优先移进← 正确答案
- C.立即报错终止
- D.随机选择一条路径
答案:B。Yacc 的默认策略是“移进优先”,这正好实现了悬空 else 的“就近匹配”语义。理解这个默认行为,才能正确解读 Bison 的冲突警告。
Q7. 下列哪些情况会导致 LL(1) 预测分析表出现冲突?
- A.文法中存在左递归← 正确答案
- B.同一非终结符的两条产生式 FIRST 集相交← 正确答案
- C.某非终结符可推导出 ε,且其 FOLLOW 集与该产生式 FIRST 集有交集← 正确答案
- D.产生式右部长度超过 10 个符号
答案:ABC。左递归会让 FIRST 集自相交;两条产生式右部以同一终结符开头,查表时同一格会有两个候选;ε 产生式则要求“FIRST 与 FOLLOW 不相交”,否则导演不知道是该展开还是该跳过。右部长度与冲突无关。
Q8. 关于 SLR 与 LR(1) 的关系,下列说法正确的是?
- A.SLR 使用 FOLLOW 集来决定归约动作← 正确答案
- B.LR(1) 通过向前看符号区分不同归约上下文← 正确答案
- C.LALR(1) 合并了 LR(1) 中核心相同的项目集← 正确答案
- D.LALR(1) 能解析所有 LR(1) 文法
答案:ABC。SLR 在归约时检查当前 token 是否在左部的 FOLLOW 集中;LR(1) 为每个项目携带更精确的向前看符号;LALR 合并同心项目集以压缩状态数。但合并会丢失部分上下文,故 LALR(1) 是 LR(1) 的真子集,D 错误。
Q9. 关于 LL 与 LR 表达能力的对比,正确的说法是?
- A.LL(1) 文法集合包含于 LR(1) 文法集合← 正确答案
- B.LR 解析器能直接处理左递归文法,LL 需要先改写← 正确答案
- C.LL 解析器通常比 LR 更早发现语法错误← 正确答案
- D.LR(1) 分析表通常比 LL(1) 预测表更小
答案:ABC。LL(1) ⊂ LALR(1) ⊂ LR(1),A 正确;左递归对 LR 天然友好,B 正确;LL 边读边预测,遇到不匹配立即报错,C 正确;LR 表因状态多通常比 LL 预测表大得多,D 恰好相反。
Q10. LALR(1) 合并同心项目集时,会引入哪种新的冲突?
- A.移进-归约冲突
- B.归约-归约冲突← 正确答案
- C.同时引入两类冲突
- D.不会引入任何冲突
答案:B。合并同心项目集后,不同上下文的归约项目可能聚到一起,若它们面对同一向前看符号却对应不同产生式,就产生归约-归约冲突;而移进-归约冲突在合并中不会出现,这是 LALR 的关键性质,也是它比 LR(1) 弱的原因。