编译原理里的冲突到底是什么?

复习编译原理的时候观看了 b 站视频,大概会区分各种文法(LR(0)、SLR(1)、LR(1)、LALR(1))但是不理解为什么需要这么多文法,以及有什么用?所以总结一下

问题

文法=规定语言长什么样

比如表达式可以写成:

这告诉编译器:a+b*c怎么被拆分成语法数

实际编译时,语法分析器做的就是:读token,然后决定是 移进 还是 归约。

eg:

复制代码
输入id+id*id

词法分析器首先把源码变成token

复制代码
id + id * id

语法分析器一边读,一边问自己:

复制代码
现在这个 id 是不是已经可以归约成 T / E?
还是应该先继续读后面的 +、*?

1."移近"和"归约"是什么

这是理解冲突的核心。

假设有文法:

复制代码
E -> E + E
E -> E * E
E -> id

输入:

复制代码
id + id * id

当分析到:

复制代码
id + id · * id

也就是已经看到 id + id,下一个符号是 *

这事parser(解析器)有两个选择:

选择一:归约

复制代码
id + id

可以先归约成:

复制代码
E + E

再归约成:

复制代码
E

这相当于理解成:

复制代码
(id + id) * id

选择二:移进

先不要归约,继续读 * id,最后理解成:

复制代码
id + (id * id)

这才符合通常的乘法优先级。

所以这里 parser 不知道该怎么办:
到底归约,还是继续读?

这就叫:移进-归约冲突。

2. 什么是"冲突"?

冲突就是:在同一个状态、面对同一个输入符号时,分析表里出现两个动作。

LR 分析表里每个格子本来应该只有一个动作,比如:

复制代码
ACTION[状态 i, 符号 a] = shift

或者:

复制代码
ACTION[状态 i, 符号 a] = reduce

但如果同一个格子里同时出现:

复制代码
shift(移进) 和 reduce(归约)

就叫 移进-归约冲突

如果同时出现两个 reduce:

复制代码
reduce A -> α
reduce B -> β

就叫 归约-归约冲突

冲突 = 编译器在某一步无法唯一决定下一步怎么做。

3. 为什么会冲突?

通常有两个原因。

第一种:文法本身有歧义

比如:

复制代码
E -> E + E
E -> E * E
E -> id

这个文法没有说明 +* 谁优先。

所以:

复制代码
id + id * id

可以有两种语法树:

复制代码
(id + id) * id

或者:

复制代码
id + (id * id)

编译器当然会犹豫。

解决办法是改文法,例如写成:

复制代码
E -> E + T | T
T -> T * F | F
F -> id

这样 * 在更底层,优先级更高。

第二种:文法不歧义,但 parser 看得不够远

这就是 LR(0)、SLR(1)、LR(1)、LALR(1) 的区别。

它们的区别本质是:

parser 在做决定时,能利用多少上下文信息。

4. LR(0)、SLR(1)、LR(1)、LALR(1) 怎么理解?

我们可以把它们理解成四种"眼神不同"的 parser。

LR(0):最莽

LR(0) 不看下一个输入符号。

只要看到某个项目变成:

复制代码
A -> α ·

它就想归约。

所以 LR(0) 很容易冲突。

比如状态里同时有:

复制代码
A -> α ·
B -> α · b

LR(0) 会想:

复制代码
A -> α ·      可以归约
B -> α · b    看到 b 又可以移进

但 LR(0) 不看后面到底是不是 b,所以容易冲突。


SLR(1):稍微聪明一点

SLR(1) 会看 FOLLOW 集。

如果有:

复制代码
A -> α ·

它不是在所有符号上都归约,而是只在:

复制代码
FOLLOW(A)

里的符号上归约。

也就是说:

复制代码
只有当下一个 token 可能跟在 A 后面时,才归约 A -> α

所以 SLR(1) 比 LR(0) 少很多冲突。

但是 SLR(1) 的问题是:FOLLOW(A) 是全局的,太粗糙。

它不管你当前在哪个上下文里,只要某个符号在 FOLLOW(A) 里,就认为可以归约。

所以有些情况下它还是会误判。


LR(1):最精细

LR(1) 的项目长这样:

复制代码
A -> α · β, a

后面的 a向前看符号 lookahead

意思是:

复制代码
只有当后面真的看到 a 时,才允许这个归约。

所以 LR(1) 不是用全局 FOLLOW 集,而是给每个项目单独带一个 lookahead。

因此 LR(1) 判断最准确,冲突最少。

缺点是:状态数很多,分析表很大。


LALR(1):实际编译器常用的折中

LALR(1) 的思想是:

LR(1) 太大了,那我把"核心相同"的状态合并一下。

比如两个 LR(1) 状态:

复制代码
A -> α ·, a

和:

复制代码
A -> α ·, b

核心都是:

复制代码
A -> α ·

只是 lookahead 不同。

LALR(1) 会把它们合并成:

复制代码
A -> α ·, a/b

这样表变小很多。

所以

复制代码
LR(1) ⊂ LALR(1)

就是在讲:LALR(1) 是在 LR(1) 基础上合并同心项目集。

实际中的 yacc / bison 这类工具就常用 LALR(1) 或类似技术。


5. 理论和实际编译怎么结合?

实际编译器大概是这样:

复制代码
源代码
  ↓
词法分析 lexer
  ↓
token 流
  ↓
语法分析 parser
  ↓
语法树 AST
  ↓
语义分析 / 类型检查 / 中间代码 / 优化 / 目标代码

我们现在学的 LR、SLR、LALR 主要在这一步:

复制代码
token 流 → 语法树 AST

也就是 parser 的理论基础。

比如写:

复制代码
int x = 1 + 2 * 3;

词法分析得到:

复制代码
int id = num + num * num ;

语法分析器要根据文法判断:

复制代码
1 + 2 * 3

这里的文法可能是:

复制代码
E -> E + T | T
T -> T * F | F
F -> num | id | ( E )

E 表示表达式 expression
T 表示项 term
F 表示因子 factor

应该被理解为:

复制代码
1 + (2 * 3)

而不是:

复制代码
(1 + 2) * 3

所以,文法不是为了考试凭空造出来的,它决定了编译器怎么理解代码结构。

为什么不会推成 (1 + 2) * 3

因为如果想得到:

复制代码
(1 + 2) * 3

那最外层应该是:

复制代码
T -> T * F

也就是说整个 1 + 2 必须先成为一个 T

但是在这个文法里,T 只能生成:

复制代码
F
T * F
T / F

它不能直接生成:

复制代码
1 + 2

因为 + 只在 E 这一层出现。

所以:

复制代码
1 + 2

只能是 E,不能是 T

T * F 的左边必须是 T,不是 E

因此不加括号时,文法不允许把它理解成:

复制代码
(1 + 2) * 3

如果真的想这样写,就必须输入:

复制代码
(1 + 2) * 3

因为括号规则:

复制代码
F -> ( E )

允许一个完整的 E 被包起来,变成一个 F,再进一步变成 T


这和实际编译器是一回事吗?

是的,思想是一回事。

实际编译器里一般会有类似的表达式规则,只是更复杂,比如还要处理:

复制代码
赋值 =
逻辑或 ||
逻辑与 &&
比较 < > ==
加减 + -
乘除 * /
一元运算 ! -
函数调用 f()
数组访问 a[i]
成员访问 obj.x
括号

它们通常会按优先级分很多层,例如:

复制代码
assignment
logical_or
logical_and
equality
relational
additive
multiplicative
unary
primary

比如可以写成:

复制代码
Expr           -> Assignment
Assignment     -> id = Assignment | LogicalOr
LogicalOr      -> LogicalOr || LogicalAnd | LogicalAnd
LogicalAnd     -> LogicalAnd && Equality | Equality
Equality       -> Equality == Relational | Relational
Relational     -> Relational < Additive | Additive
Additive       -> Additive + Multiplicative | Multiplicative
Multiplicative -> Multiplicative * Unary | Unary
Unary          -> - Unary | Primary
Primary        -> num | id | ( Expr )

这跟我们学的:

复制代码
E -> E + T | T
T -> T * F | F
F -> id | num | (E)

本质完全一样,只是实际语言的层级更多。


所以

语法分析器就是根据文法来理解表达式的。

1 + 2 * 3 能被正确理解,是因为文法把 + 放在 E 层,把 * 放在 T 层,从结构上规定了:

复制代码
* 的优先级高于 +

不是语法分析器自己猜出来的。


6. 一句话记住这些文法的关系

从弱到强大概是:

复制代码
LR(0) < SLR(1) < LALR(1) < LR(1)

意思是:

能处理的文法范围越来越大,冲突越来越少,但代价也越来越高。

更直观一点:

复制代码
LR(0):不看后面,直接判断
SLR(1):看 FOLLOW 集,粗略判断
LR(1):每个项目都带 lookahead,精确判断
LALR(1):接近 LR(1),但合并状态,适合实际使用

核心:

冲突就是 parser 在某个状态不知道该 shift 还是 reduce。
LR 系列算法的目的,就是构造分析表,让 parser 每一步都有唯一动作。

相关推荐
爱跳舞的烤冷面6 小时前
自学嵌入式第22天(数据结构——哈希)
数据结构·算法·哈希算法
猎嘤一号6 小时前
博弈论(Game Theory)的理论、算法与工程
人工智能·算法·安全·博弈论
马拉AI6 小时前
腾讯开源 Agent 记忆系统,AI“换对话就忘”的问题有了新解法(附安装使用教程)
人工智能·算法·开源·科研
地平线开发者8 小时前
征程6工具链模型X86推理方式说明
算法
地平线开发者8 小时前
【征程6】校准量化中HistogramObserver解析
算法
OuO-29 小时前
笔试强训 Day 34:ISBN 号码、kotori 和迷宫、矩阵最长递增路径
java·算法·矩阵
程序喵大人9 小时前
【C++进阶】STL算法与函数对象 - 04 find、count和any_of把查询写成意图
开发语言·c++·算法
豆沙沙包?9 小时前
c++中引用(P7-P11)
java·c++·算法
不会代码的小猴10 小时前
标准模板库(STL)
开发语言·c++·笔记·算法
ZhouDevin10 小时前
算法论文/高效微调4——DoRA:权重分解的低秩适配方法
算法