编译原理基础:LL(1) 文法与 LL(1) 分析法
嗨!学编译原理是不是觉得有点像解谜游戏?今天我们要聊聊 LL(1) 文法和 LL(1) 分析法,这俩是编译器设计里的"预测大师",能让解析代码变得简单又高效。别怕,我会用大白话讲明白它们是啥,还会带点形式化的定义,帮你一步步搞懂!
LL(1) 文法:不纠结的文法
感性理解
想象你在做选择题,每个题目的开头都长得不一样,你一看就能知道选哪个答案,完全不用犹豫。LL(1) 文法就像这样的"完美选择题":编译器读到代码的某个符号,只看一眼(一个 token),就能立刻决定走哪条路解析,不用猜来猜去。简单说,它是让编译器"果断"的文法。
形式化表达
LL(1) 是一种上下文无关文法(CFG),名字里的意思是:
- 第一个 L:从左到右扫描输入。
- 第二个 L:用最左推导(leftmost derivation)。
- 1:只看一个符号(lookahead 一个 token)就能决定用哪个产生式。
一个文法是 LL(1) 的,必须满足:
- 对于非终结符 A 的每个产生式 A → α | β,FIRST(α) 和 FIRST(β) 没有交集(开头不能撞车)。
- 如果 α 或 β 能推 ε(空串),则 FIRST(β) 或 FIRST(α) 不能和 FOLLOW(A) 有交集(空串时后续也不能冲突)。
例子:
css
S → a S | b
- FIRST(a S) = { a },FIRST(b) = { b },没交集,是 LL(1)。 但如果改成:
css
S → a S | a T
- FIRST(a S) = { a },FIRST(a T) = { a },有交集,就不是 LL(1)。
LL(1) 分析法:一步到位的解析
感性理解
LL(1) 分析法就像是拿着一个"导航表"开车。编译器一边读代码,一边查表,看到当前符号和栈顶的非终结符,就能立刻知道下一步是推导啥,还是跳过啥。整个过程干净利落,像玩拼图一样,步步到位,最后拼出整个程序。
形式化表达
LL(1) 分析法是一种自顶向下的预测分析法,用一个预测分析表(parsing table)和栈来解析输入。核心步骤:
- 构建预测分析表 :
- 行是文法的非终结符,列是终结符(包括 $)。
- 根据 FIRST 和 FOLLOW 集合填表:
- 对于 A → α,若 t ∈ FIRST(α),表[A, t] = A → α。
- 若 α 能推 ε,则对 t ∈ FOLLOW(A),表[A, t] = A → α。
- 解析过程 :
- 用栈保存推导状态,从开始符号 S 入栈。
- 读输入符号,栈顶如果是:
- 终结符:匹配就弹出,继续读。
- 非终结符:查表,替换成对应产生式。
- 栈空且输入读完,解析成功。
例子: 文法:
css
S → a S | b
-
FIRST(S → a S) = { a },FIRST(S → b) = { b }。
-
FOLLOW(S) = { $ }(假设 S 是开始符号)。
-
预测分析表:
a b $ S S→aS S→b -
输入 "aab":
- 栈:[S],输入:aab$
- 查表 S,a → S→aS,栈:[a S],匹配 a,栈:[S]
- 查表 S,a → S→aS,栈:[a S],匹配 a,栈:[S]
- 查表 S,b → S→b,栈:[b],匹配 b,栈:[]
- 输入读完,栈空,成功!
为啥 LL(1) 这么重要?
- 简单高效:只看一个符号就能决定下一步,解析速度快。
- 好实现:预测分析表直接告诉编译器怎么走,不用回溯。
- 有限制但实用:虽然不是所有文法都是 LL(1),但通过提取公共左因子、消除左递归,可以改成 LL(1)。
小结
LL(1) 文法是"果断"的文法,保证编译器不纠结;LL(1) 分析法是"导航"的方法,让解析一步到位。它们靠 FIRST 和 FOLLOW 集合合作,FIRST 管开头,FOLLOW 管后续。试着自己写个小文法,建个分析表跑跑看,动手玩玩就更明白了!加油哦!