“验收时逐条核对:表项有 SELECT 依据;终结符匹配与展开分开;AST 保留括号决定的分组;错误位置来自第一条失效动作。进阶任务是在规范 LR(1)页解释一个 SLR 冲突,并在LALR页…”
形式陈述
LALR(1) 通过合并规范 LR(1)中只在向前看符号上不同的状态,减小解析表。对一个 LR(1) 状态
核心相同的状态归为一组,合并后的项目集就是组内全部 LR(1) 项目的并。对于同一产生式和点位置,这等价于把向前看集合取并。原来的边重定向到目标组,再按 LR(1) 规则填 ACTION/GOTO;如果仍无冲突,就得到 LALR(1) 解析器。
下文讨论构表状态数时,先删除不可达以及不能生成终结词的无用规则。若开始符号本身不能生成终结词,则语言为空,直接用始终拒绝的解析器处理,不进入后面的状态数比较。
同核心状态经同一符号转移后,目标也具有同核心,因此重定向不会要求同一组的同一符号边指向两个不同的组。这里的“核心”用全部去向前看项目定义;有些教材用 kernel 项目定义等价分组,在固定文法的闭包下得到同样分组,不应把两种记号中的项目数直接比较。
这是构表压缩,不是运行时合并多个正在解析的栈。解析器仍保持一条状态栈,每格仍必须唯一决定动作。
直觉
规范LR(1)可能为同一段识别进度保存多份状态,只因它们分别处于不同后续环境。合并后,解析器仍知道“识别到哪里”,却忘记“从哪类前缀环境来到这里”。许多文法不需要后一份区别,因此压缩没有影响。
然而不同前缀可能交换两条归约的允许token。分别看每个状态都能作决定,合并后却把两份名单交叉叠在一起;被忘掉的正是选择归约所需的信息。
例子与边界
一个 LR(1) 却不是 LALR(1) 的文法
取
语言恰含 acd、bce、ace、bcd 四个词。读过前缀 ac 时,规范LR(1)状态为
此时看见 bc 时则来到
二者的LR(0)核心都为
| 已读前缀/状态 | 看到 d | 看到 e |
|---|---|---|
| ac,对应 |
||
| bc,对应 |
||
| 合并状态 | 两条归约冲突 | 两条归约冲突 |
这份文法的规范LR(1)自动机有14状态,按核心合并后有13状态;唯一被合并的一对便是上述两态。节省一个状态,却恰好丢掉了重要区别。
若在冲突时固定优先归约 acd 与 bce 能继续,ace 与 bcd 则会在随后期待的终结符上失败;固定优先
为什么新冲突只能是归约/归约
假设原规范LR(1)表无冲突。若合并状态在 token
另一方面,合并后的归约
这个论证只讨论从无冲突规范LR(1)表合并的情况;若原表本来有移进/归约冲突,LALR当然不会凭这个定理把它自动消除。
推论与应用
LALR与DFA最小化不能互换。DFA最小化按未来输入的接受行为等价来合并;LALR按项目核心相同合并,可能失去确定解析能力。不能因为二者都减少状态,就把“最小化保持语言”的定理直接用于LALR表。
教学上先构造完整LR(1)表再合并最容易核查:保留分组映射,逐条合并向前看,再重建动作检查冲突。工程实现也可在LR(0)图上传播lookahead,避免先物化全部规范状态;这是计算同一类信息的更紧凑方法,不能在分析复杂度时把“已经建完巨大的LR(1)表”隐去。
若原表有
单元任务:检查压缩是否丢了信息
对上述四词文法,要求列出读过 ac 与 bc 的完整项目,构造合并行,并说明一个固定冲突优先级会丢掉哪些合法输入。解答就是表中的两行交换关系:优先 ace,bcd,优先 acd,bce。
验收必须把“合并前两态分别无冲突”“合并后只有一行”“每个合法词所需的归约”连起来。只报告状态数从14变13,无法说明压缩是否正确。若需要保留原文法与全部分析,继续使用规范LR(1),或让GLR显式保存冲突分支。
参考资料
- Cornell CS4120, LR(1) and LALR Parsing,LALR grammars:合并向前看及由合并引起的冲突
- Cornell CS4120, LR(1) and LALR slides,slides 8–9
- Aho, Lam, Sethi, Ullman, Compilers, 2nd ed., 2007,§4.7