十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

编译原理四大集合:FIRST、FOLLOW、FIRSTVT、LASTVT本质解析

编译原理四大集合:FIRST、FOLLOW、FIRSTVT、LASTVT本质解析 1. 这不是背公式而是理解语法分析器的“呼吸节奏”FIRST、FOLLOW、FIRSTVT、LASTVT——这四个缩写词对刚接触编译原理的同学来说像一堵贴满希腊字母和箭头的墙。你抄下定义默写规则做题时却总在“要不要加ε”“为什么这里要递归求”“FOLLOW(A)里怎么突然冒出个#”上卡壳。我带过七届编译原理实验课也给三家公司做过前端编译器模块的技术评审最常听到的抱怨不是“不会算”而是“算完不知道它到底在干啥”。其实这四个集合根本不是抽象数学游戏它们是语法分析器尤其是LL(1)和LR(0)/SLR(1)分析器在运行时赖以呼吸的氧气FIRST告诉你“接下来最可能看到什么”FOLLOW告诉你“这个非终结符后面紧跟着什么才合法”而FIRSTVT/LASTVT则是算符优先分析器的“视线范围”——它不关心整个推导链只盯住相邻两个终结符之间那道最短的缝隙。你手算的每一步都在模拟分析器读取输入流时那一毫秒的决策逻辑。比如当LL(1)分析表里M[A, a] A → α 这一格被填上背后就是 FIRST(α) 包含 a而当它遇到空串α ⇒* ε就必须查 FOLLOW(A) 是否包含 a否则就直接报错。这不是考记忆力是在训练你用机器的视角看文法。所以本文不罗列教科书定义而是从一个真实的手动构造LL(1)分析表的下午开始你盯着一张空白表格左边是非终结符A、B、C顶上是a、b、c、#你得填满每一格。填错一格整个分析器就会在某个输入上卡死或误判。这篇文章就是帮你把那张表填对的全程实录。2. 四大集合的本质定位与设计逻辑2.1 FIRST终结符的“首秀预测器”FIRST(X) 的核心任务是回答“如果我现在要展开符号XX可以是终结符、非终结符或符号串那么从它推导出的任意一个合法句子的最左端第一个可能出现的终结符有哪些”注意三个关键词最左端、终结符、可能出现。它不关心整个句子长什么样只锁定那个“第一眼看到”的字符。比如文法中有 E → T EE → T E | ε那么 FIRST(E) 就不能只看E → T E这一条因为T本身可能推导出以a开头的串也可能推导出ε此时就得继续看E能推出什么。所以FIRST的计算本质是一次“向左穿透”的传播从X出发沿着产生式右部逐个符号扫描只要前面的符号能推出ε就必须把后面的符号的FIRST集纳入考虑直到遇到一个不能推出ε的符号或者扫描到末尾。这个过程天然带有递归性因为非终结符的FIRST集依赖于其他非终结符的FIRST集。但关键在于它只传播“可能性”不传播“必然性”——FIRST(T) {a, b, ε} 意味着T有可能推出空串也有可能推出以a或b开头的串分析器必须为这三种情况都准备好预案。提示FIRST集永远只包含终结符和ε。哪怕X是一个非终结符FIRST(X)里也绝不会出现另一个非终结符。这是它的铁律。如果你算出来FIRST(A) {B, a}那一定是哪里出错了——B是另一个非终结符它必须被进一步展开。2.2 FOLLOW非终结符的“安全边界探测器”FOLLOW(A) 解决的是一个截然不同的问题“在某个合法的句子中非终结符A被成功展开后它的右边紧接着出现的终结符可能有哪些”注意这里没有“最左”二字也没有“推导”二字它完全脱离了A自身的产生式只关注A在整个文法中的“上下文位置”。比如S → a A b那么无论A自己能推出什么只要它出现在a和b之间FOLLOW(A)就必须包含b。再比如S → A BB → c那么A后面紧跟着的就是B而B最终会推出c所以c必须在FOLLOW(A)里。更隐蔽的是S → A这意味着A可以是整个句子的结尾而句子结尾的标志是结束符#所以#也必须加入FOLLOW(A)。FOLLOW集的设计逻辑是为了解决LL(1)分析中的“回溯”困境当分析器看到一个终结符a它需要知道“当前栈顶的非终结符A在什么情况下可以安全地用某条产生式展开使得展开后的结果能匹配上a”答案就是要么a ∈ FIRST(α)要么α ⇒* ε 且 a ∈ FOLLOW(A)。因此FOLLOW集本质上划定了一个非终结符的“影响半径”——它告诉分析器当A被弹出栈顶时下一个输入符号必须落在这个集合里否则就是语法错误。它不描述A能变成什么而是描述A“身后”允许站着谁。2.3 FIRSTVT与LASTVT算符优先关系的“邻接快照”FIRSTVT和LASTVT是为算符优先分析法量身定制的它们彻底抛弃了“推导”和“展开”的概念只聚焦于终结符之间的直接邻接关系。FIRSTVT(B) 的定义是“在B的任意一个规范句型中从B开始向右扫描遇到的第一个终结符是什么”注意这里说的是“句型”不是“句子”意味着它允许中间夹杂非终结符。例如若B → ( E )那么FIRSTVT(B) {(}因为从B出发第一个碰到的终结符就是左括号。若B → a C bC → d | ε那么FIRSTVT(B) {a, d}因为C可能为空所以a是第一个C也可能推出d所以d也是第一个。LASTVT(B) 则是镜像操作“在B的任意一个规范句型中从B开始向左扫描遇到的最后一个终结符是什么”比如B → a C bC → d则LASTVT(B) {d, b}。这两个集合的核心价值在于快速构建算符优先关系表。分析器不需要知道整个E → E T的推导链它只需要在读到一个号时立刻查表左边的终结符比如a和右边的终结符比如b之间是什么关系, , 。而这个关系的判定就依赖于FIRSTVT和LASTVT。例如若存在产生式P → … a Q b …那么a · FIRSTVT(Q) 且 LASTVT(Q) · b。它们是文法中所有“终结符-非终结符-终结符”这种三元组关系的静态快照是算符优先分析器得以摆脱复杂状态机、仅靠一张二维表就能工作的技术基石。2.4 四者关系图谱从LL到LR的演进脉络这四个集合并非孤立存在它们共同勾勒出语法分析技术的演进路线。LL(1)分析器是“前瞻驱动”的它看着输入流的下一个符号即“First Look”决定用哪条产生式展开栈顶的非终结符。因此它重度依赖FIRST和FOLLOW来构建无冲突的预测分析表。而LR系列分析器如SLR(1)、LALR(1)则是“归约驱动”的它不断移进输入符号直到栈顶形成一个可归约的句柄然后执行归约。此时它需要知道“在什么输入符号下才能安全地将句柄归约为某个非终结符”。这个判断依据就是该非终结符的FOLLOW集——SLR(1)直接使用FOLLOW而更强大的LALR(1)则使用“向前看符号集”它是FOLLOW的精细化版本。至于FIRSTVT/LASTVT则代表了另一条技术路径当文法具有明显的算符优先结构如算术表达式时我们干脆放弃“推导”的思维转而建立终结符之间的优先级关系。这牺牲了文法描述能力无法处理if-else二义性但换来了极简的分析逻辑和极高的效率。所以当你在吉林大学的课件里看到FIRST/FOLLOW在哈工大的讲义里看到FIRSTVT/LASTVT你看到的不仅是不同算法更是编译器设计者在“通用性”与“效率”、“精确性”与“简洁性”之间所做的不同权衡。掌握它们不是为了应付考试而是为了在将来设计一门新语言的语法时能一眼看出这里该用LL(1)还是LR(1)还是干脆上算符优先3. 手算全过程详解从零开始构建一个完整文法的四大集合3.1 文法选定与预处理为什么选这个例子我们选用一个经典但足够复杂的文法它涵盖了所有典型情况左递归、右递归、ε产生式、嵌套结构。这个文法描述了一个简化版的算术表达式并加入了赋值语句以便充分展示FOLLOW集的传播G: S → i E S → E E → E T | E - T | T T → T * F | T / F | F F → ( E ) | i | num首先进行预处理消除左递归。原E → E T | ... 是典型的左递归必须改写。标准方法是引入新非终结符EE → T E E → T E | - T E | ε T → F T T → * F T | / F T | ε F → ( E ) | i | num S → i E | E注意S有两个产生式且S是开始符号。预处理后我们得到7个非终结符S, E, E, T, T, F以及终结符i, , , -, *, /, (, ), num, ##是输入结束符。这个文法足够“脏”——有ε产生式E, T有嵌套F → ( E )有多个入口S的两个产生式能暴露出所有手算陷阱。3.2 FIRST集计算递归传播的“剥洋葱”过程计算FIRST集我们采用迭代法因为它比纯递归更直观也更容易发现错误。初始化对每个终结符aFIRST(a) {a}对每个非终结符AFIRST(A) ∅。第一轮扫描只处理终结符和形如 A → a… 的产生式FIRST(i) {i}, FIRST() {}, ..., FIRST(num) {num}F → i ⇒ FIRST(F) {i}F → num ⇒ FIRST(F) {num}F → ( E ) ⇒ FIRST(F) {(}所以 FIRST(F) {i, num, (}第二轮扫描处理含非终结符的右部利用已知的FIRSTT → F TF的FIRST已知且F不能推出ε因为F的所有产生式都以终结符开头所以FIRST(T) FIRST(F) {i, num, (}E → T E同理T不能推出ε所以FIRST(E) FIRST(T) {i, num, (}S → i ES → E所以FIRST(S) FIRST(i) ∪ FIRST(E) {i} ∪ {i, num, (} {i, num, (}第三轮扫描处理含ε的产生式T → * F T | / F T | ε前两条右部以终结符开头所以FIRST(T) {*, /}最后一条是ε所以FIRST(T) {ε}因此 FIRST(T) {*, /, ε}E → T E | - T E | ε ⇒ FIRST(E) {, -, ε}现在回看 T → F TF不能推出ε所以无需看T的FIRST。但看 E → T ET不能推出ε所以FIRST(E)不变。关键点来了S → i E没问题但S → EE不能推出ε所以FIRST(S)也不变。第四轮扫描检查是否有新的ε传播我们发现E和T都能推出ε但它们的父节点E和T的FIRST集已经确定且不依赖它们的ε。所以本轮无新增。最终FIRST集FIRST(S) {i, num, (}FIRST(E) {i, num, (}FIRST(E) {, -, ε}FIRST(T) {i, num, (}FIRST(T) {*, /, ε}FIRST(F) {i, num, (}注意这里有个极易犯的错——看到E → ε就以为FIRST(E)应该包含ε。这是错的FIRST(E) FIRST(T E)而FIRST(T)不包含ε所以E的ε对FIRST(E)没有贡献。FIRST集的ε只来自“整个右部能推出ε”而不是“右部中某个符号能推出ε”。3.3 FOLLOW集计算上下文传播的“涟漪效应”FOLLOW集的计算同样用迭代法。初始化FOLLOW(S) {#}因为S是开始符号它后面只能是结束符其余FOLLOW(A) ∅。第一轮扫描处理产生式右部中直接跟在非终结符后的终结符S → i EE后面没有符号但S → i E所以后面是E因此 FOLLOW(E) {#}不对规则是若A → αBβ则FOLLOW(B) FIRST(β) \ {ε}。这里S → i EB是Eβ是空所以不加。但S → i Ei后面是后面是E所以对i和的FOLLOW无影响。真正重要的是S → E所以E后面可以是#因此 FOLLOW(E) {#}。E → T ET后面是EE后面是空所以 FOLLOW(T) FIRST(E) \ {ε} {, -}E后面是空所以 FOLLOW(E) FOLLOW(E) {#}目前。E → T ET后面是E所以 FOLLOW(T) FIRST(E) \ {ε} {, -}已存在E后面是空所以 FOLLOW(E) FOLLOW(E)不规则是A → αB则FOLLOW(B) FOLLOW(A)。所以E → T EB是EA是E所以 FOLLOW(E) FOLLOW(E)这是废话。关键是E → ε这条它本身不产生FOLLOW传播。T → F TF后面是T所以 FOLLOW(F) FIRST(T) \ {ε} {*, /}T后面是空所以 FOLLOW(T) FOLLOW(T)。T → * F TF后面是T所以 FOLLOW(F) FIRST(T) \ {ε} {*, /}已存在。F → ( E )E后面是)所以 FOLLOW(E) {)}同时(后面是E所以对(的FOLLOW无影响。本轮后FOLLOW(S) {#}FOLLOW(E) {#, )}FOLLOW(E) {#}FOLLOW(T) {, -, #, )} 来自E → T E 和 S → EFOLLOW(T) ∅待定需FOLLOW(T)FOLLOW(F) {*, /, , -, #, )}第二轮扫描利用新获得的FOLLOW集进行传播由T → F T且T可以推出ε所以 FOLLOW(F) FOLLOW(T) {, -, #, )}。FOLLOW(F) 变为 {*, /, , -, #, )}。由E → T E且E可以推出ε所以 FOLLOW(T) FOLLOW(E) {#, )}。FOLLOW(T) 已包含这些无变化。由T → F TT后面是空所以 FOLLOW(T) FOLLOW(T) {, -, #, )}。由S → i EE后面是空所以 FOLLOW(E) FOLLOW(S) {#}已存在。由F → ( E )E后面是)已处理。本轮后FOLLOW(T) {, -, #, )}第三轮扫描检查是否还有传播T → * F TF后面是TT可以推出ε所以 FOLLOW(F) FOLLOW(T) {, -, #, )}已存在。无新增。最终FOLLOW集FOLLOW(S) {#}FOLLOW(E) {#, )}FOLLOW(E) {#, )}FOLLOW(T) {, -, #, )}FOLLOW(T) {, -, #, )}FOLLOW(F) {*, /, , -, #, )}实操心得FOLLOW集的传播像水波。起点是FOLLOW(S){#}然后通过产生式右部的“邻接”关系一层层向外扩散。最容易漏掉的是“因ε产生式而引发的FOLLOW传播”比如E → T EE → ε这就要求FOLLOW(T)必须包含FOLLOW(E)。我见过太多同学在作业里只写了FIRST(T) {, -}却忘了加FOLLOW(E)导致后续LL(1)分析表出现冲突。3.4 FIRSTVT与LASTVT计算终结符邻接的“快照提取”算符优先文法要求我们先改造文法使其只包含形如 A → a…, A → aB…, A → Ba…, A → B a C… 的产生式其中a是终结符。我们的文法已经是这种形式F → ( E )E → T E等。计算FIRSTVT(A)的算法是若A → a…则a ∈ FIRSTVT(A)若A → B…则FIRSTVT(A) FIRSTVT(B)若A → B β 且 B ⇒* ε则FIRSTVT(A) FIRSTVT(β)LASTVT(A)是镜像若A → …a则a ∈ LASTVT(A)若A → …B则LASTVT(A) LASTVT(B)若A → β B 且 B ⇒* ε则LASTVT(A) LASTVT(β)我们从终结符开始FIRSTVT(i) {i}, FIRSTVT() {}, ..., FIRSTVT(num) {num}LASTVT同理。计算FIRSTVTF → i | num | ( E ) ⇒ FIRSTVT(F) {i, num, (}T → F T ⇒ FIRSTVT(T) FIRSTVT(F) {i, num, (}因为F不能推出εE → T E ⇒ FIRSTVT(E) FIRSTVT(T) {i, num, (}同理E → T E | - T E ⇒ FIRSTVT(E) {, -}T → * F T | / F T ⇒ FIRSTVT(T) {*, /}S → i E ⇒ FIRSTVT(S) {i}S → E ⇒ FIRSTVT(S) FIRSTVT(E) {i, num, (}计算LASTVTF → i | num | ( E ) ⇒ LASTVT(F) {i, num, )}T → F T ⇒ T可以推出ε所以LASTVT(T) LASTVT(F) ∪ LASTVT(T) {i, num, )} ∪ {*, /} {i, num, ), *, /}E → T E ⇒ E可以推出ε所以LASTVT(E) LASTVT(T) ∪ LASTVT(E) {i, num, ), *, /} ∪ {, -} {i, num, ), *, /, , -}E → T E | - T E ⇒ LASTVT(E) {, -}T → * F T | / F T ⇒ LASTVT(T) {*, /}S → i E ⇒ LASTVT(S) LASTVT(E) {i, num, ), *, /, , -}S → E ⇒ LASTVT(S) LASTVT(E)已存在最终FIRSTVT(S) {i, num, (}FIRSTVT(E) {i, num, (}FIRSTVT(E) {, -}FIRSTVT(T) {i, num, (}FIRSTVT(T) {*, /}FIRSTVT(F) {i, num, (}LASTVT(S) {i, num, ), *, /, , -}LASTVT(E) {i, num, ), *, /, , -}LASTVT(E) {, -}LASTVT(T) {i, num, ), *, /, , -}LASTVT(T) {*, /}LASTVT(F) {i, num, )}提示FIRSTVT和LASTVT通常比FIRST/FOLLOW小得多因为它们只关心“第一个/最后一个终结符”不关心中间过程。这也是算符优先分析高效的原因——它忽略了很多细节。4. LL(1)与算符优先分析表的实战构建与冲突诊断4.1 从集合到LL(1)分析表填满那张决定命运的表格现在我们用前面算出的FIRST和FOLLOW集来手工构建LL(1)分析表M[A, a]。这张表的行是文法的非终结符S, E, E, T, T, F列是所有终结符i, , , -, *, /, (, ), num, #。填表规则对每个产生式 A → α对每个 a ∈ FIRST(α)置 M[A, a] A → α若 ε ∈ FIRST(α)则对每个 b ∈ FOLLOW(A)置 M[A, b] A → α我们逐条处理S → i EFIRST(i E) {i}所以 M[S, i] S → i ES → EFIRST(E) {i, num, (}所以 M[S, i] S → E, M[S, num] S → E, M[S, (] S → E冲突M[S, i] 被填了两次一次是S → i E一次是S → E。这就是著名的“FIRST-FIRST冲突”。它意味着当输入是i时分析器无法决定是把它当作一个赋值语句的开始i E还是当作一个简单表达式E。解决方案是重构文法例如将S拆分为Stmt和Expr或者强制规定赋值语句必须有特定前缀。这正是我们手算的价值——它提前暴露了文法设计的缺陷。E → T EFIRST(T E) FIRST(T) {i, num, (}所以 M[E, i] E → T E, M[E, num] E → T E, M[E, (] E → T EE → T EFIRST( T E) {}所以 M[E, ] E → T EE → - T EM[E, -] E → - T EE → εε ∈ FIRST(E)所以对每个 b ∈ FOLLOW(E) {#, )}置 M[E, #] E → ε, M[E, )] E → εT → F TFIRST(F T) FIRST(F) {i, num, (}所以 M[T, i] T → F T, M[T, num] T → F T, M[T, (] T → F TT → * F TM[T, *] T → * F TT → / F TM[T, /] T → / F TT → εε ∈ FIRST(T)FOLLOW(T) {, -, #, )}所以 M[T, ] T → ε, M[T, -] T → ε, M[T, #] T → ε, M[T, )] T → εF → ( E )FIRST(( E )) {(}所以 M[F, (] F → ( E )F → iM[F, i] F → iF → numM[F, num] F → num填完后我们检查冲突。除了S行的i列冲突外其他地方都是单值。这说明除了S的二义性这个文法对于LL(1)是“几乎”可行的。实际工程中我们会在这里停下来修改S的定义比如增加一个关键字letS → let i E | E这样FIRST(let i E) {let}与FIRST(E) {i, num, (}就不重叠了。4.2 算符优先关系表用FIRSTVT/LASTVT搭建“终结符高速公路”算符优先分析不关心非终结符只关心终结符之间的三种关系·小于、·等于、·大于。构建规则如下若有产生式 P → … a b …则 a · b若有产生式 P → … a B b …则 a · FIRSTVT(B) 的每个元素若有产生式 P → … a B则 LASTVT(B) 的每个元素 · a若有产生式 P → … B b则 LASTVT(B) 的每个元素 · b我们用前面算出的FIRSTVT/LASTVT来填充一张终结符×终结符的表。终结符集{i, , , -, *, /, (, ), num, #}步骤1找 · 关系F → ( E ) ⇒ ( · ) 因为(和)直接相邻其他产生式没有直接相邻的终结符对所以只有这一对。步骤2找 · 关系F → ( E ) ⇒ ( · FIRSTVT(E) {i, num, (}所以 ( · i, ( · num, ( · (T → F TT → * F T ⇒ * · FIRSTVT(F) {i, num, (}所以 * · i, * · num, * · (同理/ · i, / · num, / · (E → T EE → T E ⇒ · FIRSTVT(T) {i, num, (}所以 · i, · num, · (同理- · i, - · num, - · (S → i E ⇒ · FIRSTVT(E) {i, num, (}所以 · i, · num, · (步骤3找 · 关系F → ( E ) ⇒ LASTVT(E) · )。LASTVT(E) {i, num, ), *, /, , -}所以 i · ), num · ), ) · ), * · ), / · ), · )T → F TT → * F T ⇒ LASTVT(F) · *。LASTVT(F) {i, num, )}所以 i · *, num · *, ) · *同理i · /, num · /, ) · /E → T EE → T E ⇒ LASTVT(T) · 。LASTVT(T) {i, num, ), *, /, , -}所以所有这些都 · 同理所有这些都 · -S → i E ⇒ LASTVT(E) · #所以 i · #, num · #, ) · #, * · #, / · #, · #最终我们得到了一张完整的算符优先关系表。分析器的工作就变得极其简单维护一个符号栈读入一个终结符a比较栈顶终结符b和a的关系。若b · a或b · a则移进a若b · a则从栈顶开始向左找到一个句柄即满足b · ... · a的最短子串将其归约为某个非终结符。整个过程就像在一条高速公路上根据路标·, ·, ·决定是加速移进还是减速停车归约。实操心得算符优先分析表的构建其工作量远小于LL(1)表因为它只处理终结符。但它的代价是文法能力受限。我在开发一个配置文件解析器时曾试图用算符优先处理YAML风格的缩进结果发现FIRSTVT/LASTVT根本无法捕捉“空格数量”这个信息最后不得不切换到递归下降。所以选择哪种分析技术首先要问我的文法是“终结符关系明确”还是“结构层次清晰”5. 常见错误、调试技巧与面试真题拆解5.1 手算高频错误清单与自查指南在批改上百份学生作业和代码后我总结出以下TOP5错误它们几乎覆盖了所有扣分点错误类型具体表现自查方法修正方案ε传播滥用在计算FIRST(A)时看到A → B CB能推出ε就直接把FIRST(C)加进FIRST(A)而忽略了B是否真的能推出ε或者C是否是右部最后一个符号。检查每一条产生式右部。对A → X₁ X₂ … Xₙ必须从X₁开始若X₁不能推出ε则FIRST(A) FIRST(X₁)若X₁能推出ε则继续看X₂依此类推若所有Xᵢ都能推出ε则ε ∈ FIRST(A)。严格按顺序扫描每一步都要确认“Xᵢ能否推出ε”这个前提。FOLLOW传播遗漏计算FOLLOW(B)时只处理了A → α B β中β非空的情况漏掉了A → α B即β为空的情况从而忘记将FOLLOW(A)加入FOLLOW(B)。在扫描每条产生式时对右部的每一个非终结符B都检查它后面是否还有符号。如果没有即B是右部最后一个则必须执行FOLLOW(B) FOLLOW(A)。养成习惯看到A → … B就在草稿纸上立刻写下“FOLLOW(B) FOLLOW(A)”。终结符与非终结符混淆在FIRST集里写进了非终结符例如FIRST(E) {T, i}。FIRST集的定义决定了它只能包含终结符和ε。任何非终结符的出现都是计算链条中断的信号。立刻回溯找到那个“未展开”的非终结符重新计算它的FIRST集。算符优先的“”误判认为所有括号对都是·关系例如认为[ · ]但实际上只有在同一产生式中直接相邻的终结符才有·关系。·关系只来自P → … a b …这种模式。方括号通常来自不同产生式不存在·。严格对照产生式右部一个字符一个字符地找相邻对。忽略文法改造直接对含左递归的原始文法计算FIRST/FOLLOW导致结果完全错误。如果文法中存在A → A α那么它一定不是LL(1)文法FIRST/FOLLOW的计算也就失去了意义。动手前第一件事就是检查并消除左递归、提取左公因子。这是不可跳过的预处理。提示一个快速验证FIRST/FOLLOW是否正确的经验法则是所有终结符必须至少出现在一个FIRST集中所有非终结符其FOLLOW集不能为空除了那些绝对不可能出现在句型中间的符号但这种情况极少。如果发现某个终结符a从未出现在任何FIRST中或者某个非终结符A的FOLLOW是空集那一定是哪里出错了。5.2 调试实战当LL(1)分析表出现冲突时如何逆向定位假设你在填表时发现M[E, ]被填了两次一次是E → T E另一次是E → ε。这显然不可能因为一个格子里只能有一个产生式。这时你应该立即启动逆向排查确认E → T E的合法性FIRST( T E) {}没错。确认E → ε的触发条件这需要ε ∈ FIRST(E)且 ∈ FOLLOW(E)。我们算出FIRST(E) {, -, ε}所以前半部分OK。那么问题一定出在FOLLOW(E)上。回溯FOLLOW(E)的来源FOLLOW(E) FOLLOW(E)因为E → T E且E可以推出ε。所以问题转移到FOLLOW(E)。检查FOLLOW(E)的来源FOLLOW(E)来自S → E所以FOLLOW(E) FOLLOW(S) {#}以及F → ( E )所以FOLLOW(E) {)}。所以FOLLOW(E) {#, )}。发现问题 并不在 {#, )} 里所以M[E, ] E → ε 这一格是非法的是我们算错了FOLLOW(E)。这个过程揭示了一个关键调试思想**冲突不是终点而是错误的路标。它精准地指向了计算链条中最脆弱的一环
返回列表