LR 分析从左向右读取 token,并通过移进和归约构造最右推导的逆过程。它先积累已经看到的结构,再决定这些符号属于哪个产生式。与朴素递归下降不同,确定性 LR 能直接处理许多左递归文法。
运行时通常只是状态栈加查表循环;复杂的部分在于生成表、理解冲突以及设计语义动作。小文法可以手工构造,工程中通常由工具生成。
移进、归约与状态栈
以 E → E + n | n 和输入 n + n 为例:先移进 n,再把它归约为 E;随后移进加号和第二个 n,最后将 E + n 归约为 E。归约时可同时构造 AST 节点。
| 动作 | 输入是否前进 | 栈和语义值的变化 |
| shift s | 消费一个 token | 压入它的语义值与状态 s |
| reduce A → β | 不消费前瞻 | 弹出 β 对应内容,计算 A 的值,再按 GOTO 压入新状态 |
| accept | 不再继续 | 开始符号已完成且符合结束条件 |
| error | 通常尚未消费当前前瞻 | 报错或启动恢复 |
若分别维护状态栈与语义值栈,归约长度为 r 的右部就各弹出 r 项,状态栈底部还保留起始状态;若把状态与文法符号交错存储,弹出数量的说法会不同。空产生式弹出 0 项,但仍要通过 GOTO 进入归约后的状态。
项目:点标识一条产生式识别到了哪里
项目 E → E · + n 表示已识别一个 E,接下来可以识别加号与 n;E → E + n · 表示右部已经完整,可考虑归约。
一个状态往往包含多个项目,代表当前前缀可能处在多种语法上下文。它记录的是可行前缀信息,并不是只对应一个源码 token。
对 LR(0) 项目集合,closure 遇到 A → α · B β 时,把 B 的所有产生式 B → · γ 加入,并递归扩展。goto(I, X) 先将 I 中点前为 X 的项目向右移动一位,再取闭包。不断对新集合执行 goto,可构造项目集自动机。
终结符上的边形成移进动作,非终结符上的边形成 GOTO。至于完成项目在哪些前瞻上归约,正是 LR 各变体的关键区别。
五个状态跑完 n+n:归约以后先回到旧状态,再查 GOTO
为开头文法编号:规则 1 为 E→E+n,规则 2 为 E→n。采用以下状态编号,它们是本例手工指定的,不要求与生成器输出一致。
| 状态 | n | + | $ | GOTO(E) |
| 0 | shift 2 | 错误 | 错误 | 1 |
| 1 | 错误 | shift 3 | accept | — |
| 2 | 错误 | reduce 2 | reduce 2 | — |
| 3 | shift 4 | 错误 | 错误 | — |
| 4 | 错误 | reduce 1 | reduce 1 | — |
状态 0 的闭包含 E→·E+n 与 E→·n;识别 E 后到状态 1,期待加号或结束;识别 n 后到状态 2,可以归约 E→n。状态 3 表示 E→E+·n,状态 4 则完成 E→E+n·。FOLLOW(E)={+, $},因此在这两个前瞻上填写相应归约。
ACTION = {
(0, "n"): ("shift", 2), (1, "+"): ("shift", 3),
(1, "$"): ("accept", 0), (2, "+"): ("reduce", 2),
(2, "$"): ("reduce", 2), (3, "n"): ("shift", 4),
(4, "+"): ("reduce", 1), (4, "$"): ("reduce", 1),
}
GOTO = {(0, "E"): 1}
def parse(tokens):
if any(t not in ("n", "+") for t in tokens):
raise SyntaxError("invalid token")
source = list(tokens) + ["$"]
states, values, pos, trace = [0], [], 0, []
while True:
action = ACTION.get((states[-1], source[pos]))
if action is None:
raise SyntaxError(f"unexpected {source[pos]} at token {pos}")
kind, number = action
trace.append((tuple(states), pos, kind))
if kind == "shift":
states.append(number)
values.append(("n",) if source[pos] == "n" else "+")
pos += 1
elif kind == "reduce":
length = 3 if number == 1 else 1
rhs = values[-length:]
value = ("+", rhs[0], rhs[2]) if number == 1 else rhs[0]
del values[-length:]
del states[-length:]
states.append(GOTO[(states[-1], "E")])
values.append(value)
else:
assert len(values) == 1 and pos == len(tokens)
return values[0], trace
node, trace = parse(["n", "+", "n"])
assert node == ("+", ("n",), ("n",))
assert [p for stack, p, action in trace] == [0, 1, 1, 2, 3, 3]
assert parse(["n", "+", "n", "+", "n"])[0] == ("+", node, ("n",))
assert parse(["n"])[0] == ("n",)
for tokens in ([], ["+", "n"], ["n", "n"], ["n", "+"], ["n", "$"]):
try:
parse(tokens)
except SyntaxError:
pass
else:
raise AssertionError("invalid input accepted")
读完第二个 n 后栈为 [0,1,3,4],归约长度 3 弹回 [0],再查 GOTO(0,E)=1。此时前瞻仍是 $,下一步才接受。不能按归约前的栈顶 4 查 GOTO,也不能因为执行了归约就顺手消费 $。
本例没有空规则,故代码的 length 总大于零。扩展到 ε 时必须专门处理零长度;Python 的 del stack[-0:] 等同于清空整栈,不是“不弹出任何项”。这正是将算法说明直接照抄成切片代码时容易遗漏的边界。
归约改变了栈,前瞻为什么没有前进?
观察一次归约时,应同时保留弹栈前和弹栈后的状态。图中的前瞻有自己的位置,只有 shift 才移动它;reduce 则先移除右部,再用留下的栈顶查 GOTO。你可以直接选中任何动作,也可播放整段栈变化。无效输入同样走真实查表流程,在没有表项的位置停止,不会为了演示完整而跳过错误。
正在呈现知识画面
归约改变了栈,前瞻为什么没有前进?移进消费 token,归约按右部长度弹栈,再用弹栈后的状态查 GOTO。归约不消费前瞻,输入结束标记也不会被顺手跳过。
{"id":"lr-stack-and-lookahead","title":"归约改变了栈,前瞻为什么没有前进?","summary":"移进消费 token,归约按右部长度弹栈,再用弹栈后的状态查 GOTO。归约不消费前瞻,输入结束标记也不会被顺手跳过。","height":1000,"html":"<h2 data-i18n=\"heading\"></h2><p class=\"intro\" data-i18n=\"intro\"></p><div class=\"presets\" role=\"group\" data-i18n-label=\"input\"><button data-mode=\"short\" data-i18n=\"short\"></button><button data-mode=\"long\" data-i18n=\"long\"></button><button data-mode=\"bad\" data-i18n=\"bad\"></button></div><div class=\"control\"><label for=\"cursor\" data-i18n=\"cursor\"></label><output for=\"cursor\" id=\"cursor-value\"></output><input id=\"cursor\" type=\"range\" min=\"0\" max=\"6\" value=\"2\" step=\"1\"></div><div class=\"actions\"><button id=\"play\" data-i18n=\"play\"></button></div><section class=\"panel\"><h3 data-i18n=\"before\"></h3><div id=\"before\"></div></section><section class=\"panel\"><h3 data-i18n=\"after\"></h3><div id=\"after\"></div></section><div class=\"metrics\"><div><span data-i18n=\"look\"></span><strong id=\"look\"></strong></div></div><p id=\"action\" class=\"log\" role=\"status\"></p><details class=\"scope\"><summary data-i18n=\"scopeLabel\"></summary><p data-i18n=\"scope\"></p></details>","css":".intro{margin:8px 0 18px;color:var(--muted);font-size:14px}.caption,.hint{font-size:12px;color:var(--muted);margin-top:12px}.control{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:6px 12px;align-items:center;margin:14px 0;font-size:13px}.control label{font-size:13px}.control output{color:var(--accent);min-width:5em;text-align:right;font-variant-numeric:tabular-nums}.control input{grid-column:1/-1;width:100%;margin:0}.presets{display:flex;gap:4px;align-items:stretch}.presets button{flex:1 1 0;min-width:0;overflow-wrap:anywhere;font-size:13px}.actions{display:flex;gap:8px;flex-wrap:wrap;margin:14px 0}.actions button{flex:1;min-width:0;font-size:13px;min-height:42px}.copy{display:grid;font-size:14px;margin:16px 0}.copy>*{grid-area:1/1;margin:0}.reserve{visibility:hidden}.scope{margin-top:14px;color:var(--muted);font-size:12px}.scope summary{padding:8px 0;cursor:pointer}.scope p{margin-top:10px}.two{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px;margin:16px 0}.card{border:1px solid var(--rule);border-radius:9px;padding:14px;min-width:0}.card header{font-size:14px;font-weight:600;margin-bottom:10px}.card button{font-size:12px;width:100%;min-width:0}.card small{display:block;font-size:12px;color:var(--muted)}.card code{font-size:12px;overflow-wrap:anywhere}.metrics{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px;margin:16px 0}.metrics>div{border-left:2px solid var(--accent);padding-left:10px;min-width:0}.metrics span{display:block;min-height:3.2em;color:var(--muted);font-size:12px}.metrics strong{font-size:23px;display:block;font-weight:500;overflow-wrap:anywhere;font-variant-numeric:tabular-nums}.formula{border-top:1px solid var(--rule);padding-top:12px;margin-top:14px;font-size:13px;font-variant-numeric:tabular-nums;overflow-wrap:anywhere}.value{font:500 20px var(--viz-font-ui)}.legend{display:flex;flex-wrap:wrap;gap:6px 14px;font-size:12px;color:var(--muted);margin:12px 0}.legend span:before{content:'';display:inline-block;width:8px;height:8px;border-radius:2px;background:var(--accent);margin-right:6px}.legend .second:before{background:var(--second)}.check{display:flex;align-items:center;gap:8px;font-size:13px;margin:12px 0}.check input{width:16px;height:16px;flex-shrink:0;accent-color:var(--accent)}select,input[type=text],input[type=number]{background:var(--paper);color:var(--ink);font:inherit;font-size:13px;padding:10px;border:1px solid var(--rule);border-radius:7px;max-width:100%;min-width:0}select{width:100%;min-height:42px}.band{height:20px;display:flex;background:var(--surface);border-radius:4px;overflow:hidden}.band i{height:100%;display:block;background:var(--accent);transition:width .25s}.band i.second{background:var(--second)}button:disabled{cursor:default}.status{font-size:13px;min-height:4.8em}.mono{font-family:var(--viz-font-mono);font-variant-numeric:tabular-nums}.muted{color:var(--muted)}\n@media(max-width:480px){.two{gap:8px}.card{padding:12px 9px}.actions button,.presets button{font-size:12px}.metrics strong{font-size:20px}.metrics{gap:8px}}\n@media(prefers-reduced-motion:reduce){*,*::before,*::after{transition:none!important;animation:none!important}}\n.track-row{margin:16px 0}.track-row>span{display:block;font-size:12px;color:var(--muted);margin-bottom:7px}.track{display:flex;gap:5px;min-width:0}.cell{flex:1;min-width:0;min-height:48px;border:1px solid var(--rule);border-radius:5px;background:var(--surface);display:flex;align-items:center;justify-content:center;text-align:center;padding:5px 3px;font-size:12px;overflow-wrap:anywhere;font-variant-numeric:tabular-nums}.cell.on{background:var(--accent-soft);border-color:var(--accent)}.cell.gold{background:var(--second-soft);border-color:var(--second)}.cell.empty{border-style:dashed;color:var(--muted)}.cell.error{border-color:var(--second);text-decoration:line-through}.choices{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:6px;margin:14px 0}.choices button{min-width:0;padding:10px 4px;font-size:12px;overflow-wrap:anywhere}.choices button[aria-current=true]{border-color:var(--accent);background:var(--accent-soft)}.relation{display:grid;grid-template-columns:minmax(0,1fr) 28px minmax(0,1fr);align-items:center;gap:6px;margin:12px 0}.relation .cell{min-height:72px}.relation>i{text-align:center;font-style:normal;color:var(--accent)}.readout{font-size:13px;white-space:pre-line;min-height:6.4em;overflow-wrap:anywhere}.plot{position:relative;border-left:1px solid var(--rule);border-bottom:1px solid var(--rule);padding:8px 0;margin:18px 0}.plot-row{height:28px;position:relative;margin:4px 0;background:var(--surface)}.plot-row i{position:absolute;inset-block:5px;background:var(--accent);min-width:2px;transition:width .2s,left .2s}.plot-row b{position:relative;padding:4px 6px;font:11px/28px var(--viz-font-ui);background:var(--paper);border-radius:3px}.axis{display:flex;justify-content:space-between;font-size:11px;color:var(--muted)}\n\n/* Action labels must remain readable across locales and narrow embeds. */\n.actions{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:8px}\n.actions button{overflow-wrap:anywhere;white-space:normal;min-height:58px;padding:8px}\nbutton{overflow-wrap:anywhere}\n@media(min-width:640px){.actions{grid-template-columns:repeat(3,minmax(0,1fr))}}\n\n.metrics span{overflow-wrap:anywhere}\n.panel{border:1px solid var(--rule);border-radius:9px;padding:12px;margin:14px 0;min-width:0}.panel h3{font-size:13px;margin:0 0 10px;color:var(--muted)}.source{font:13px/1.8 var(--viz-font-mono);white-space:pre-wrap;overflow-wrap:anywhere;margin:12px 0}.source mark{background:var(--accent-soft);color:var(--ink);border-bottom:2px solid var(--accent)}.source .fault{background:var(--second-soft);border-color:var(--second)}.diagram{display:block;width:100%;height:auto;max-height:320px;margin:14px 0;overflow:visible}.diagram text{fill:var(--ink);font:12px var(--viz-font-ui)}.diagram .edge{stroke:var(--muted);stroke-width:1.5;fill:none}.diagram .edge.on{stroke:var(--accent);stroke-width:3}.diagram .node{fill:var(--surface);stroke:var(--rule);stroke-width:1.5}.diagram .node.on{fill:var(--accent-soft);stroke:var(--accent)}.diagram .node.gold{fill:var(--second-soft);stroke:var(--second)}.log{font:12px/1.8 var(--viz-font-mono);white-space:pre-line;overflow-wrap:anywhere;min-height:7.2em}.table{display:grid;gap:5px;margin:14px 0}.table .tr{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:5px}.table .tr>*{padding:8px 5px;background:var(--surface);border-radius:4px;min-width:0;overflow-wrap:anywhere;font-size:12px}.table .tr .on{background:var(--accent-soft)}.table .tr .gold{background:var(--second-soft)}.choices button{min-height:44px}.metrics strong{min-height:1.6em}.code-line{display:block}.code-line.on{background:var(--accent-soft);border-left:2px solid var(--accent);padding-left:6px}.status-badge{font-size:12px;color:var(--accent)}.tile-grid{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:6px}.tile-grid .cell{min-height:40px}.actions button{min-height:48px}.form-field{display:grid;gap:8px;margin:14px 0;font-size:13px}.form-field input{width:100%;font-family:var(--viz-font-ui)}.metrics.three{grid-template-columns:repeat(3,minmax(0,1fr))}.formula{min-height:3.2em}.presets{flex-wrap:wrap}.presets button{flex-basis:25%;min-height:44px}\n\n.actions{grid-template-columns:1fr}#before,#after{display:grid;grid-template-columns:repeat(6,minmax(0,1fr));gap:4px}.metrics strong{font-size:18px}","js":"const $=s=>document.querySelector(s);const set=(id,v)=>$('#'+id).textContent=v;const t=k=>viz.t(k);function cells(id,values){$('#'+id).replaceChildren(...values.map(v=>{const e=document.createElement('div');e.className='cell '+(v.cls||'');e.textContent=v.text;e.title=v.title||v.text;return e;}));}function pressed(mode){document.querySelectorAll('[data-mode]').forEach(e=>e.setAttribute('aria-pressed',String(e.dataset.mode===mode)));}const esc=s=>String(s).replace(/[&<>\"']/g,c=>({'&':'&','<':'<','>':'>','\"':'"',\"'\":'''}[c]));const fmt=s=>'{'+[...s].sort().join(', ')+'}';function graph(id,nodes,edges){const el=$('#'+id);el.setAttribute('viewBox','0 0 360 200');el.innerHTML='<defs><marker id=\"arrow-'+id+'\" viewBox=\"0 0 10 10\" refX=\"8\" refY=\"5\" markerWidth=\"5\" markerHeight=\"5\" orient=\"auto-start-reverse\"><path d=\"M 0 0 L 10 5 L 0 10 z\" fill=\"var(--muted)\"/></marker></defs>'+edges.map(e=>{const a=nodes[e[0]],b=nodes[e[1]],dx=b.x-a.x,dy=b.y-a.y,d=Math.hypot(dx,dy)||1;if(edges.some(r=>r[0]===e[1]&&r[1]===e[0])){const nx=-dy/d,ny=dx/d;return '<path class=\"edge '+(e[2]?'on':'')+'\" d=\"M '+(a.x+dx/d*22+nx*12)+' '+(a.y+dy/d*22+ny*12)+' Q '+((a.x+b.x)/2+nx*38)+' '+((a.y+b.y)/2+ny*38)+' '+(b.x-dx/d*25+nx*12)+' '+(b.y-dy/d*25+ny*12)+'\" marker-end=\"url(#arrow-'+id+')\"/>';}return '<line class=\"edge '+(e[2]?'on':'')+'\" x1=\"'+(a.x+dx/d*25)+'\" y1=\"'+(a.y+dy/d*25)+'\" x2=\"'+(b.x-dx/d*28)+'\" y2=\"'+(b.y-dy/d*28)+'\" marker-end=\"url(#arrow-'+id+')\"/>';}).join('')+nodes.map(n=>'<g><circle class=\"node '+(n.cls||'')+'\" cx=\"'+n.x+'\" cy=\"'+n.y+'\" r=\"25\"/><text x=\"'+n.x+'\" y=\"'+(n.y+4)+'\" text-anchor=\"middle\">'+esc(n.name)+'</text>'+(n.sub?'<text x=\"'+n.x+'\" y=\"'+(n.y+43)+'\" text-anchor=\"middle\">'+esc(n.sub)+'</text>':'')+'</g>').join('');}function table(id,rows){$('#'+id).replaceChildren(...rows.map(r=>{const row=document.createElement('div');row.className='tr';r.forEach(x=>{const c=document.createElement('div');if(typeof x==='object'){c.textContent=x.text;c.className=x.cls||'';}else c.textContent=x;row.append(c)});return row;}));}let mode='short',trace=[];const G={0:1},T={0:{n:['shift',2]},1:{'+':['shift',3],'$':['accept']},2:{'+':['reduce',1],'$':['reduce',1]},3:{n:['shift',4]},4:{'+':['reduce',3],'$':['reduce',3]}};function build(){const input={short:['n','+','n','$'],long:['n','+','n','+','n','$'],bad:['n','+','+','n','$']}[mode];let stack=[0],p=0;trace=[];for(let guard=0;guard<30;guard++){const before=[...stack],look=input[p],a=T[stack.at(-1)][look]||['error'];let detail=a[0],old=p;if(a[0]==='shift'){stack.push(a[1]);p++;detail+=' '+a[1]}else if(a[0]==='reduce'){stack.splice(-a[1]);const top=stack.at(-1),go=G[top];stack.push(go);detail+=' E → '+(a[1]===1?'n':'E + n')+'\\npop '+a[1]+' → '+fmt(stack.slice(0,-1))+'\\nGOTO('+top+', E) = '+go}trace.push({before,after:[...stack],look,next:input[p],detail,p:old,newp:p});if(['accept','error'].includes(a[0]))break}$('#cursor').max=trace.length-1;$('#cursor').value=Math.min(+$('#cursor').value,trace.length-1);}function draw(){const i=+$('#cursor').value,r=trace[i];set('cursor-value',(i+1)+' / '+trace.length);for(const id of ['before','after'])cells(id,Array.from({length:6},(_,j)=>({text:r[id][j]??'·',cls:j<r[id].length?'on':'empty'})));set('look',r.look+' → '+r.next);set('action',r.detail);$('#action').dataset.position=r.p;$('#action').dataset.next=r.newp;$('#after').dataset.stack=r.after.join(',');pressed(mode);}const playback=viz.playback({delay:1400,advance(){if(+$('#cursor').value<trace.length-1){$('#cursor').value=+$('#cursor').value+1;draw()}},canAdvance(){return+$('#cursor').value<trace.length-1},onChange(p){set('play',t(p?'pause':'play'))}});$('#play').onclick=()=>playback.toggle();$('#cursor').oninput=()=>{playback.pause();draw()};document.querySelectorAll('[data-mode]').forEach(b=>b.onclick=()=>{playback.pause();mode=b.dataset.mode;build();draw()});build();draw();","audio":false,"strings":{"action":"当前动作","after":"动作后:状态栈","bad":"n + + n","before":"动作前:状态栈","cursor":"观察哪一个动作","heading":"归约改变了栈,前瞻为什么没有前进?","input":"输入","intro":"直接选择任意动作,比较动作前后两份栈。播放只是自动移动观察位置,可随时手动选择。","long":"n + n + n","look":"前瞻变化","pause":"暂停","play":"播放栈变化","reset":"重置","scope":"固定文法 E→E+n / n,手工五状态表,无空产生式。完整执行由查表生成,再选择观察切片;不是通用 LR 生成器。","scopeLabel":"模型范围与约定","short":"n + n"}}
SLR、LR(1) 与 LALR 保留多少上下文
| 方法 | 归约前瞻的来源 | 可能丢失的信息 |
| LR(0) | 完成项目不区分前瞻终结符 | 最粗,容易冲突 |
| SLR(1) | 对 A 的归约使用全局 FOLLOW(A) | FOLLOW 混合不同出现位置 |
| 规范 LR(1) | 项目自带上下文前瞻 | 保留更细的上下文,状态可能多 |
| LALR(1) | 合并 LR(0) 核心相同的 LR(1) 状态,并合并前瞻 | 核心相同但来源不同的上下文被混合 |
LR(1) 项目 [A → α · B β, a] 的闭包,对 B 的产生式使用 FIRST(βa) 中的终结符作为前瞻。a 表示 A 完成后允许跟随什么,不能简单理解成点后立即要读的字符。
规范 LR(1) 比 LALR(1) 能处理更多文法,但仍不是全部上下文无关文法。歧义文法无法在没有消歧规则的情况下由确定性 LR 表给出唯一分析。
一个 LR(1) 但非 LALR(1) 的例子
S → a A d | b A e | a B e | b B d
A → c
B → c
该文法只生成 acd、bce、ace、bcd 四个串,各有唯一推导。读完 ac 时,后接 d 应把 c 归约成 A,后接 e 则归约成 B;读完 bc 时恰好相反。
这说明冲突可能来自表构造对上下文的压缩,看到 reduce/reduce 冲突不能立即断言原文法有歧义。SLR 的全局 FOLLOW 也会产生类似的精度问题。Bison 手册专门讨论了这种 LALR 限制造成的冲突及采用 IELR 或规范 LR 的办法。Mysterious Conflicts
用生成器区分“文法冲突”与“合并造成的冲突”
将上面的四串文法写成 conflict.y,字符终结符用单引号:
%start S
%%
S: 'a' A 'd' | 'b' A 'e' | 'a' B 'e' | 'b' B 'd';
A: 'c';
B: 'c';
%%
bison -Dlr.type=lalr --report=states,lookaheads -o lalr.c conflict.y
bison -Dlr.type=canonical-lr --report=states,lookaheads -o canonical.c conflict.y
这一步只生成表和报告,不需要 lexer,也还不是可运行程序。LALR 应出现两个 reduce/reduce 冲突,规范 LR 则能保留 ac 与 bc 后的不同上下文而无这类冲突。检查报告时定位完成项目 A→c· 与 B→c· 的前瞻,别只比较状态总数。
若继续使用有冲突的 LALR 默认选择,某些串可能走错归约;因此“工具仍生成了 C 文件”不等于语言已被正确实现。工程上应明确选择合适构造方法或重写文法,而不是忽略警告。Bison 手册源码中的算法说明
合并状态时,丢掉了哪一项对应关系?
合并前,两条来源分别保存“前缀、完成项目、允许前瞻”的对应关系。合并后 A 与 B 各自得到前瞻并集,却不再知道 d 是从哪条前缀带来的。下面始终保留原始两行,便于对照压缩丢失了什么。四个输入串在原文法中仍各有唯一推导,表的冲突不能替代对歧义的证明。
正在呈现知识画面
合并状态时,丢掉了哪一项对应关系?ac 与 bc 后有相同的 LR(0) 核心,但 A/B 各自允许的前瞻相反。合并前瞻会让 d、e 都同时支持两个归约,丢失的是来源与候选之间的配对。
{"id":"lalr-context-merge","title":"合并状态时,丢掉了哪一项对应关系?","summary":"ac 与 bc 后有相同的 LR(0) 核心,但 A/B 各自允许的前瞻相反。合并前瞻会让 d、e 都同时支持两个归约,丢失的是来源与候选之间的配对。","height":1000,"html":"<h2 data-i18n=\"heading\"></h2><p class=\"intro\" data-i18n=\"intro\"></p><div class=\"presets\" role=\"group\" data-i18n-label=\"prefix\"><button data-mode=\"ac\" data-i18n=\"ac\"></button><button data-mode=\"bc\" data-i18n=\"bc\"></button></div><label class=\"form-field\"><span data-i18n=\"look\"></span><select id=\"look\"><option>d</option><option>e</option></select></label><label class=\"check\"><input type=\"checkbox\" id=\"merge\"><span data-i18n=\"merge\"></span></label><div id=\"contexts\" class=\"table\"></div><section class=\"panel\"><h3 data-i18n=\"candidates\"></h3><div id=\"candidates\"></div></section><div class=\"copy\"><p id=\"explanation\" role=\"status\"></p><p class=\"reserve\" aria-hidden=\"true\" data-i18n=\"ok\"></p><p class=\"reserve\" aria-hidden=\"true\" data-i18n=\"conflict\"></p></div><details class=\"scope\"><summary data-i18n=\"scopeLabel\"></summary><p data-i18n=\"scope\"></p></details>","css":".intro{margin:8px 0 18px;color:var(--muted);font-size:14px}.caption,.hint{font-size:12px;color:var(--muted);margin-top:12px}.control{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:6px 12px;align-items:center;margin:14px 0;font-size:13px}.control label{font-size:13px}.control output{color:var(--accent);min-width:5em;text-align:right;font-variant-numeric:tabular-nums}.control input{grid-column:1/-1;width:100%;margin:0}.presets{display:flex;gap:4px;align-items:stretch}.presets button{flex:1 1 0;min-width:0;overflow-wrap:anywhere;font-size:13px}.actions{display:flex;gap:8px;flex-wrap:wrap;margin:14px 0}.actions button{flex:1;min-width:0;font-size:13px;min-height:42px}.copy{display:grid;font-size:14px;margin:16px 0}.copy>*{grid-area:1/1;margin:0}.reserve{visibility:hidden}.scope{margin-top:14px;color:var(--muted);font-size:12px}.scope summary{padding:8px 0;cursor:pointer}.scope p{margin-top:10px}.two{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px;margin:16px 0}.card{border:1px solid var(--rule);border-radius:9px;padding:14px;min-width:0}.card header{font-size:14px;font-weight:600;margin-bottom:10px}.card button{font-size:12px;width:100%;min-width:0}.card small{display:block;font-size:12px;color:var(--muted)}.card code{font-size:12px;overflow-wrap:anywhere}.metrics{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px;margin:16px 0}.metrics>div{border-left:2px solid var(--accent);padding-left:10px;min-width:0}.metrics span{display:block;min-height:3.2em;color:var(--muted);font-size:12px}.metrics strong{font-size:23px;display:block;font-weight:500;overflow-wrap:anywhere;font-variant-numeric:tabular-nums}.formula{border-top:1px solid var(--rule);padding-top:12px;margin-top:14px;font-size:13px;font-variant-numeric:tabular-nums;overflow-wrap:anywhere}.value{font:500 20px var(--viz-font-ui)}.legend{display:flex;flex-wrap:wrap;gap:6px 14px;font-size:12px;color:var(--muted);margin:12px 0}.legend span:before{content:'';display:inline-block;width:8px;height:8px;border-radius:2px;background:var(--accent);margin-right:6px}.legend .second:before{background:var(--second)}.check{display:flex;align-items:center;gap:8px;font-size:13px;margin:12px 0}.check input{width:16px;height:16px;flex-shrink:0;accent-color:var(--accent)}select,input[type=text],input[type=number]{background:var(--paper);color:var(--ink);font:inherit;font-size:13px;padding:10px;border:1px solid var(--rule);border-radius:7px;max-width:100%;min-width:0}select{width:100%;min-height:42px}.band{height:20px;display:flex;background:var(--surface);border-radius:4px;overflow:hidden}.band i{height:100%;display:block;background:var(--accent);transition:width .25s}.band i.second{background:var(--second)}button:disabled{cursor:default}.status{font-size:13px;min-height:4.8em}.mono{font-family:var(--viz-font-mono);font-variant-numeric:tabular-nums}.muted{color:var(--muted)}\n@media(max-width:480px){.two{gap:8px}.card{padding:12px 9px}.actions button,.presets button{font-size:12px}.metrics strong{font-size:20px}.metrics{gap:8px}}\n@media(prefers-reduced-motion:reduce){*,*::before,*::after{transition:none!important;animation:none!important}}\n.track-row{margin:16px 0}.track-row>span{display:block;font-size:12px;color:var(--muted);margin-bottom:7px}.track{display:flex;gap:5px;min-width:0}.cell{flex:1;min-width:0;min-height:48px;border:1px solid var(--rule);border-radius:5px;background:var(--surface);display:flex;align-items:center;justify-content:center;text-align:center;padding:5px 3px;font-size:12px;overflow-wrap:anywhere;font-variant-numeric:tabular-nums}.cell.on{background:var(--accent-soft);border-color:var(--accent)}.cell.gold{background:var(--second-soft);border-color:var(--second)}.cell.empty{border-style:dashed;color:var(--muted)}.cell.error{border-color:var(--second);text-decoration:line-through}.choices{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:6px;margin:14px 0}.choices button{min-width:0;padding:10px 4px;font-size:12px;overflow-wrap:anywhere}.choices button[aria-current=true]{border-color:var(--accent);background:var(--accent-soft)}.relation{display:grid;grid-template-columns:minmax(0,1fr) 28px minmax(0,1fr);align-items:center;gap:6px;margin:12px 0}.relation .cell{min-height:72px}.relation>i{text-align:center;font-style:normal;color:var(--accent)}.readout{font-size:13px;white-space:pre-line;min-height:6.4em;overflow-wrap:anywhere}.plot{position:relative;border-left:1px solid var(--rule);border-bottom:1px solid var(--rule);padding:8px 0;margin:18px 0}.plot-row{height:28px;position:relative;margin:4px 0;background:var(--surface)}.plot-row i{position:absolute;inset-block:5px;background:var(--accent);min-width:2px;transition:width .2s,left .2s}.plot-row b{position:relative;padding:4px 6px;font:11px/28px var(--viz-font-ui);background:var(--paper);border-radius:3px}.axis{display:flex;justify-content:space-between;font-size:11px;color:var(--muted)}\n\n/* Action labels must remain readable across locales and narrow embeds. */\n.actions{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:8px}\n.actions button{overflow-wrap:anywhere;white-space:normal;min-height:58px;padding:8px}\nbutton{overflow-wrap:anywhere}\n@media(min-width:640px){.actions{grid-template-columns:repeat(3,minmax(0,1fr))}}\n\n.metrics span{overflow-wrap:anywhere}\n.panel{border:1px solid var(--rule);border-radius:9px;padding:12px;margin:14px 0;min-width:0}.panel h3{font-size:13px;margin:0 0 10px;color:var(--muted)}.source{font:13px/1.8 var(--viz-font-mono);white-space:pre-wrap;overflow-wrap:anywhere;margin:12px 0}.source mark{background:var(--accent-soft);color:var(--ink);border-bottom:2px solid var(--accent)}.source .fault{background:var(--second-soft);border-color:var(--second)}.diagram{display:block;width:100%;height:auto;max-height:320px;margin:14px 0;overflow:visible}.diagram text{fill:var(--ink);font:12px var(--viz-font-ui)}.diagram .edge{stroke:var(--muted);stroke-width:1.5;fill:none}.diagram .edge.on{stroke:var(--accent);stroke-width:3}.diagram .node{fill:var(--surface);stroke:var(--rule);stroke-width:1.5}.diagram .node.on{fill:var(--accent-soft);stroke:var(--accent)}.diagram .node.gold{fill:var(--second-soft);stroke:var(--second)}.log{font:12px/1.8 var(--viz-font-mono);white-space:pre-line;overflow-wrap:anywhere;min-height:7.2em}.table{display:grid;gap:5px;margin:14px 0}.table .tr{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:5px}.table .tr>*{padding:8px 5px;background:var(--surface);border-radius:4px;min-width:0;overflow-wrap:anywhere;font-size:12px}.table .tr .on{background:var(--accent-soft)}.table .tr .gold{background:var(--second-soft)}.choices button{min-height:44px}.metrics strong{min-height:1.6em}.code-line{display:block}.code-line.on{background:var(--accent-soft);border-left:2px solid var(--accent);padding-left:6px}.status-badge{font-size:12px;color:var(--accent)}.tile-grid{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:6px}.tile-grid .cell{min-height:40px}.actions button{min-height:48px}.form-field{display:grid;gap:8px;margin:14px 0;font-size:13px}.form-field input{width:100%;font-family:var(--viz-font-ui)}.metrics.three{grid-template-columns:repeat(3,minmax(0,1fr))}.formula{min-height:3.2em}.presets{flex-wrap:wrap}.presets button{flex-basis:25%;min-height:44px}\n\n#candidates{min-height:3.6em;font:16px/1.8 var(--viz-font-ui)}","js":"const $=s=>document.querySelector(s);const set=(id,v)=>$('#'+id).textContent=v;const t=k=>viz.t(k);function cells(id,values){$('#'+id).replaceChildren(...values.map(v=>{const e=document.createElement('div');e.className='cell '+(v.cls||'');e.textContent=v.text;e.title=v.title||v.text;return e;}));}function pressed(mode){document.querySelectorAll('[data-mode]').forEach(e=>e.setAttribute('aria-pressed',String(e.dataset.mode===mode)));}const esc=s=>String(s).replace(/[&<>\"']/g,c=>({'&':'&','<':'<','>':'>','\"':'"',\"'\":'''}[c]));const fmt=s=>'{'+[...s].sort().join(', ')+'}';function graph(id,nodes,edges){const el=$('#'+id);el.setAttribute('viewBox','0 0 360 200');el.innerHTML='<defs><marker id=\"arrow-'+id+'\" viewBox=\"0 0 10 10\" refX=\"8\" refY=\"5\" markerWidth=\"5\" markerHeight=\"5\" orient=\"auto-start-reverse\"><path d=\"M 0 0 L 10 5 L 0 10 z\" fill=\"var(--muted)\"/></marker></defs>'+edges.map(e=>{const a=nodes[e[0]],b=nodes[e[1]],dx=b.x-a.x,dy=b.y-a.y,d=Math.hypot(dx,dy)||1;if(edges.some(r=>r[0]===e[1]&&r[1]===e[0])){const nx=-dy/d,ny=dx/d;return '<path class=\"edge '+(e[2]?'on':'')+'\" d=\"M '+(a.x+dx/d*22+nx*12)+' '+(a.y+dy/d*22+ny*12)+' Q '+((a.x+b.x)/2+nx*38)+' '+((a.y+b.y)/2+ny*38)+' '+(b.x-dx/d*25+nx*12)+' '+(b.y-dy/d*25+ny*12)+'\" marker-end=\"url(#arrow-'+id+')\"/>';}return '<line class=\"edge '+(e[2]?'on':'')+'\" x1=\"'+(a.x+dx/d*25)+'\" y1=\"'+(a.y+dy/d*25)+'\" x2=\"'+(b.x-dx/d*28)+'\" y2=\"'+(b.y-dy/d*28)+'\" marker-end=\"url(#arrow-'+id+')\"/>';}).join('')+nodes.map(n=>'<g><circle class=\"node '+(n.cls||'')+'\" cx=\"'+n.x+'\" cy=\"'+n.y+'\" r=\"25\"/><text x=\"'+n.x+'\" y=\"'+(n.y+4)+'\" text-anchor=\"middle\">'+esc(n.name)+'</text>'+(n.sub?'<text x=\"'+n.x+'\" y=\"'+(n.y+43)+'\" text-anchor=\"middle\">'+esc(n.sub)+'</text>':'')+'</g>').join('');}function table(id,rows){$('#'+id).replaceChildren(...rows.map(r=>{const row=document.createElement('div');row.className='tr';r.forEach(x=>{const c=document.createElement('div');if(typeof x==='object'){c.textContent=x.text;c.className=x.cls||'';}else c.textContent=x;row.append(c)});return row;}));}let prefix='ac';function draw(){const look=$('#look').value,merged=$('#merge').checked;table('contexts',[['prefix','A → c ·','B → c ·'],[{text:'ac',cls:prefix==='ac'?'on':''},'{d}','{e}'],[{text:'bc',cls:prefix==='bc'?'on':''},'{e}','{d}'],['merge','{d,e}','{d,e}']]);const a=prefix==='ac'?look==='d':look==='e',choices=merged?['A → c','B → c']:[a?'A → c':'B → c'];set('candidates',choices.join('\\n'));$('#candidates').dataset.count=choices.length;set('explanation',t(merged?'conflict':'ok'));pressed(prefix);}$('#look').onchange=draw;$('#merge').onchange=draw;document.querySelectorAll('[data-mode]').forEach(b=>b.onclick=()=>{prefix=b.dataset.mode;draw()});draw();","audio":false,"strings":{"ac":"ac","bc":"bc","candidates":"当前可归约候选","conflict":"合并后出现 reduce/reduce 冲突;原文法仍无歧义。","context":"两份原始 LR(1) 上下文","heading":"合并状态时,丢掉了哪一项对应关系?","intro":"分别选择已读前缀和下一字符,再对照原始 LR(1) 上下文与合并后的候选。","look":"下一字符","merge":"合并相同核心的状态","ok":"保留来源时,当前输入只有一个归约。","prefix":"已读前缀","reset":"重置","scope":"文法 S→aAd / bAe / aBe / bBd,A→c,B→c。四个串各有唯一推导;只展示关键完成项目,不构造整张表。","scopeLabel":"模型范围与约定"}}
区分两类冲突与解决依据
shift/reduce 表示同一状态与前瞻下,既能移进又能归约。例如 E → E + E | E * E | n 没有表达优先级,n + n * n 就有不同树形。可以重写分层文法,也可用工具的优先级、结合性声明表达语言规则。
reduce/reduce 表示同一位置有多个候选归约。它可能是真歧义,也可能是上述上下文合并结果。应查看项目、前瞻、最短触发输入和反例,而不是单纯压掉警告。
优先级声明是语义选择,不是对任意冲突的正确性证明。尤其是两个归约执行不同语义动作时,默认选择某条规则可能改变 AST 或副作用。
Bison 与 LALRPOP:名字不等于算法
Bison 支持 LALR、IELR、规范 LR 和 GLR 等配置。LALRPOP 虽然名字含 LALR,项目说明中的默认算法是 LR(1)。不能把所有工具统一写成“默认 LALR”,也不能给规范 LR 与 LALR 的状态数量规定一个固定倍率。Bison 文法与分析能力、LALRPOP 项目说明
GLR 可在冲突点保留多个分析分支,之后淘汰或合并,但歧义语义仍要定义,最坏成本也不同于确定性 LR。延迟语义动作、共享树表示和错误处理都需要额外设计。
错误恢复:error 是特殊机制
Bison 中可以写这样的恢复产生式:
statement: error ';' { yyerrok; };
error 是 parser 的特殊符号,不是 lexer 返回的“任意字符”规则。恢复时,parser 可以弹栈直到某个状态能移进 error,然后视需要丢弃输入以找到可继续的 token。同步到分号只是该规则提供的出口,不是所有错误都会自然停在分号。
被丢弃符号的资源需要正确释放;恢复后的语义值也应有明确表示,避免后续阶段把错误结构当正常节点。某些恢复会暂时抑制级联诊断,不能因此把“解析器返回”当作源码完全正确。Bison 错误恢复
工程判断
确定性 LR 的查表过程对输入长度通常是线性的,前提包括固定文法、无循环空归约以及受控的语义动作;构造表和错误恢复成本另算。增量编辑支持也依赖树结构、重用策略与词法上下文,不能仅凭 LL/LR 标签判断。
理解冲突时,先还原“已读前缀、当前项目、一个前瞻、候选动作”,再判断是语言规则、文法表达还是算法精度问题。继续阅读 AST 设计与错误恢复。
Keywords: LR, item, closure, goto, shift reduce, SLR, LALR, canonical LR, IELR, GLR, lookahead, conflict, Bison, LALRPOP