语法分析核心考点精讲:FIRST/FOLLOW集与LL(1)、LR分析表构造指南
2026/9/17 12:14:37 网站建设 项目流程

语法分析在整个编译原理课程里,属于那种“一听就会,一做就废”的章节。词法分析好歹还能靠正则表达式和有限自动机硬刚一波,到了语法分析这儿,上下文无关文法、FIRST集、FOLLOW集、LL(1)、LR(0)、SLR(1)这些概念一股脑砸过来,课后习题更是变着花样让你构造分析表、写推导过程、判断文法类型。我当年学这块的时候,课堂笔记抄得满满当当,真做起作业来照样卡壳,最后是靠着反复刷题、把经典题型的套路摸清楚才真正通透的。

这篇文章不打算做成教材的复读机,而是以课后习题为主线,把语法分析的核心知识串成一条线。内容包括上下文无关文法的构建思路、自顶向下和自底向上两类主流分析方法的解题模板、FIRST/FOLLOW集的手算技巧、预测分析表与LR分析表的构造流程,再结合我踩过的坑和常见考点做一次系统性梳理。不管你是正在被课后作业折磨的本科生,还是准备考研、面试需要突击编译原理的同学,这篇文章都能帮你少走点弯路。

1. 语法分析在学什么:先理清它是编译器的哪一环

1.1 语法分析要解决的核心问题

从编译流程来看,词法分析负责把源代码字符串拆成一个个token,比如关键字、标识符、运算符、数字常量这些。但拿到一串token之后,编译器怎么知道这些token的组合是否符合语言的语法规则?比如int a = 3 + 4;是合法的C语言声明语句,而int + = 3 a;就是一堆乱码。语法分析干的就是这件事:按照语言的语法规则,检查token序列的排列组合是否合法,并在此基础上构建一棵语法树,为后续的语义分析和中间代码生成打基础。

换句话说,词法分析回答的是“这个词是什么”,语法分析回答的是“这些词按这个顺序排在一起,成不成一句话”。如果你写过手工解析器,比如用递归下降去解析JSON或者简单的数学表达式,那你其实已经在做语法分析的事了,只是当时可能没意识到这就是编译原理里的核心章节。

1.2 为什么需要上下文无关文法

小学语文课上学过,句子有主谓宾结构,可以用语法树来分析。编程语言也一样,但比自然语言要严格得多。编译原理里用来描述程序语言语法结构的工具叫上下文无关文法(Context-Free Grammar,CFG),它是比正则表达式更强的一种描述工具。

正则表达式描述不了嵌套结构,比如括号配对、if语句里再嵌套if语句,这些结构用正则去匹配会非常痛苦,甚至不可能实现。而上下文无关文法的产生式左部只有一个非终结符,它天然支持递归定义,所以能优雅地表达嵌套结构。比如经典的算术表达式就可以定义成:

E -> E + T | T T -> T * F | F F -> ( E ) | id

这个文法通过非终结符E、T、F之间的递归和分层,精准描述了加法和乘法的优先级差异。语法分析的核心工作之一,就是给定一个文法,判断一个输入串能不能由这个文法推导出来,并给出推导过程。

2. 从课后习题看核心概念:推导、语法树与二义性

2.1 推导过程:最左推导与最右推导的实战记忆法

课后习题里第一类常见题型是“给出文法,写出某句子的推导过程”。要理解推导,你得先分清两种方向:从开始符号出发,不断用产生式替换非终结符,直到得到目标句子,这叫推导;反过来从句子出发,不断归约回开始符号,这叫归约。语法分析的本质就是在做推导或者归约。

推导里最重要的概念是最左推导和最右推导。最左推导就是每次都替换最左边的非终结符,最右推导则反过来。我当年记这两个概念的时候容易混,后来找到了一个很笨但很有效的记忆方法:“最左”和“最右”指的是每次被替换的那个非终结符的位置,而不是替换出来的结果放在哪里。举个例子,有文法:

S -> A B A -> a B -> b

从S出发做最左推导,第一步替换S为AB,此时最左非终结符是A,于是把A替换成a,得到aB;B再替换成b,得到ab。最右推导则是先替换最右边的B,再从A推下去。考试里经常让你分别写出最左推导和最右推导,并比较它们的异同,核心考点就在这个“替换顺序”上。

2.2 语法树的画法与二义性判断题的套路

语法树是把推导过程图形化的结果,根节点是开始符号,叶子节点是终结符(或者是句子中的词素),内部节点是非终结符。画语法树的技巧是直接按产生式展开,每一步推导对应一次替换,树就长出来了。

二义性文法是另一个高频考点。如果一个文法存在某个句子,它有两棵不同的语法树(等价地,有两个不同的最左推导),就说这个文法是二义性的。课后习题里最经典的例子就是E -> E + E | E * E | id,这个文法描述加减乘除会出乱子,因为字符串id + id * id既可以被解析成(id + id) * id,也可以被解析成id + (id * id),算术优先级直接乱了。

判断二义性的解题方法有三个层次。第一层次是“看结构”:产生式里同一个非终结符递归地出现在自己右边,且没有通过分层把优先级分配开,多半就有二义性风险。第二层次是“找句子”:尝试构造一个句子,画出两棵不同的语法树,能画出来就直接证明二义性。第三层次是“带条件的判断”:有些文法表面上看有歧义,但实际上因为额外约束(比如结合性声明)而没有二义性,这种题就要细心了。我在做这类题的时候养成了一个习惯,每一个句子都先试最左推导,再试最右推导,如果在某个句子那里两条推导链不同,就锁定了二义性证据。

2.3 消除二义性的标准手法

考试里如果让你消除二义性,最标准的答案思路不是去改语法树,而是改写文法。以E -> E + E | E * E | id为例,消除二义性后应该变成:

E -> E + T | T T -> T * F | F F -> ( E ) | id

核心手法是引入不同层次的非终结符。加减运算用E表示,乘除运算用T表示,原子表达式用F表示。推导时E会先“消耗”掉低优先级的+,再去处理高优先级的*,这样乘法的优先级天然高于加法。结合性也是同理:E -> E + T | T这种左递归写法,让+运算符天然左结合,符合算术直觉。碰到“给出二义性文法,要求改写为等价的无二义文法”的题目,思路就是把优先级和结合性体现在文法层次上

3. 自顶向下分析:LL(1)文法的判断与FIRST/FOLLOW集计算

3.1 从递归下降到LL(1):为什么需要FIRST集和FOLLOW集

自顶向下分析是从开始符号出发,逐步推导,直到匹配输入串。最简单的实现方式是递归下降,每个非终结符对应一个递归函数,根据输入token决定走哪条产生式。但问题来了,如果文法有公共左因子,比如A -> ab | ac,递归下降分析器看到a的时候不知道该选哪条路,这就产生了回溯。更麻烦的是左递归,比如E -> E + T,这会让递归函数无限调用自己,直接栈溢出。

为了消除这些问题,编译原理课程引入了LL(1)文法。LL(1)的含义是:从左向右扫描输入串(第一个L),产生最左推导(第二个L),每一步最多向前看一个输入符号(1)。LL(1)文法要求对于任意一个非终结符,通过查看下一个输入符号,就能唯一确定该用哪条产生式。FIRST集和FOLLOW集就是为了判断这个“唯一性”而设计的工具。

3.2 FIRST集的手算方法:先找终结符,再追非终结符

FIRST集的定义是:对于文法符号串α,FIRST(α)是所有能从α推导出的串的第一个终结符的集合。如果α能推导出空串ε,那么ε也属于FIRST(α)。

手算FIRST集我总结了几个步骤。第一步,对所有非终结符,先把产生式右部以终结符开头的那些终结符直接加进FIRST集。第二步,处理右部以非终结符开头的产生式:如果产生式形如A -> Bγ,那么FIRST(B)里的全部终结符都属于FIRST(A),如果B能推导出ε,还要继续看γ的FIRST集。第三步,确定哪些非终结符能推导出ε,这是最容易漏的地方,要专门标记出来。

举个例子。考虑文法:

S -> A B A -> a | ε B -> b

求FIRST(A)很简单,就是{a, ε}。求FIRST(S)时,看S -> A B,A的FIRST里有a,所以a进FIRST(S);因为A可以推导出ε,所以要接着看B的FIRST,得到b。所以FIRST(S) ={a, b}。注意这里b是通过“A推导出空串,然后B推出b”这条路径进去的,如果A不能推出ε,b就进不了FIRST(S)。

3.3 FOLLOW集的手算方法:盯紧产生式右部

FOLLOW集的定义是:对于非终结符A,FOLLOW(A)是在所有句型中紧跟在A后面的终结符的集合。注意,如果A是开始符号,且出现在句型的最末尾,那么输入结束标记$(或#)也算在FOLLOW集里。

计算FOLLOW集有三个规则。规则一:把$放进FOLLOW(S)(S是开始符号)。规则二:如果有产生式A -> αBβ,那么FIRST(β)中除了ε之外的所有符号都要放进FOLLOW(B)。规则三:如果有产生式A -> αB或者A -> αBβ且β能推导出ε,那么FOLLOW(A)的全部符号都要放进FOLLOW(B)。

第三条规则是初学者最容易漏的。它的逻辑是:如果B后面没有东西了,或者B后面那些东西可以被“吃掉”变成空串,那么B后面实际能跟的符号,就是A后面能跟的符号。我把这个过程叫作“FOLLOW传递”。做课后习题时,我习惯先把所有产生式列成一排,然后逐个看每个非终结符出现在产生式右部的哪个位置,接着对“它后面有什么”做判断,最后检查有没有需要向左边非终结符“继承FOLLOW”的情况。

3.4 LL(1)判断条件与预测分析表构造

判断一个文法是否是LL(1)文法,核心条件有三条。第一条,对于每个产生式A -> α | β,必须满足FIRST(α) ∩ FIRST(β)为空,这就保证了同一非终结符的不同产生式不会因为第一个符号相同而产生冲突。第二条,如果存在A -> ε这样的产生式,那么对于FIRST(A)和FOLLOW(A),必须有FIRST(A) ∩ FOLLOW(A)为空,这是因为有空产生式时,解析器有时要靠“看后面跟什么”来决定是否走空串分支。第三条,一个文法要没有任何左递归和公共左因子。

预测分析表是一个二维表格,行是非终结符,列是终结符(包括$)。表格里的每个格子存放一个产生式,表示在遇到对应输入符号时,该非终结符应该展开成什么。构造方法是:对每个产生式A -> α,先求FIRST(α),把该产生式填到FIRST(α)中每个终结符对应的格子里;如果FIRST(α)包含ε,再把该产生式填到FOLLOW(A)中每个终结符对应的格子里。填完表之后,如果任何一个格子里出现了多于一个的产生式,就说明文法不是LL(1)的。这个表格直接决定了后续“预测分析程序”的走向,课设实验里写的表驱动预测分析程序,本质上就是照着这张表做跳转。

4. 自底向上分析:LR(0)、SLR(1)与冲突处理

4.1 从移进-归约到句柄:自底向上的直觉

自底向上分析是另一种主流方法,方向刚好反过来:从输入串出发,不断找到可以被归约的子串,用产生式左部的非终结符替换它,直到归约成开始符号。这个过程中,每一步归约的“子串”叫句柄(handle)。如果能每次都准确找到句柄并归约,分析就是正确的。

移进-归约分析器的核心操作有两个:移进就是读入下一个输入符号,把它压入栈顶;归约就是栈顶的某些符号匹配某个产生式的右部,把它们弹出,把产生式左部压入。这里有个经典陷阱:怎么知道什么时候该移进、什么时候该归约?盲目归约可能导致局部看起来合法、整体却推不回去。LR(k)分析器就是通过一张状态转移表和一个栈来自动完成这个决策过程的,它每走一步都依据当前状态和输入符号查表决定动作。

4.2 LR(0)项目集规范族的构建方法

构建LR分析表的第一步是构造LR(0)项目集规范族。所谓LR(0)项目,就是在产生式右部的某个位置加一个圆点,表示“已经看到了圆点左边的部分,还没看到右边的部分”。比如E -> E + T可以对应三个项目:E -> .E + TE -> E. + TE -> E +.TE -> E + T.(其中点在不同位置)。圆点在最右边表示这个产生式已经完整匹配了,可以归约了,这叫“归约项目”。

构造项目集规范族的算法核心是一个“闭包”操作。当一个项目中圆点后面紧跟着一个非终结符B时,要把所有以B为左部的产生式加进当前项目集,并在它们最左边加个圆点,也就是B -> .γ这种形式。这个操作叫求闭包。接下来根据圆点后面的符号分门别类做状态转移:圆点后面是终结符就按该终结符转移,是非终结符就按该非终结符转移。反复做闭包和转移,直到没有新状态产生,最终就得到一整张LR(0)自动机。

我初学的时候觉得这个流程很绕,后来发现把它类比成NFA转DFA就好理解了:每个项目集的闭包等价于ε-闭包,转移则等价于读入符号后能到达的所有状态的集合。编译原理课程之所以让你学LR(0)自动机,是为了后面构造SLR(1)分析表时,你能真正理解“状态”是怎么来的,而不是死记表格样式。

4.3 SLR(1)与LR(1)的核心区别:什么时候用FOLLOW集解决冲突

LR(0)分析表有个很大的问题:它在归约时不看输入符号,只要某个项目集里有归约项目,它就会盲目归约,导致大量“移进-归约冲突”和“归约-归约冲突”。SLR(1)(Simple LR)的改进是:当遇到冲突时,利用FOLLOW集来排除可能性。具体的说,如果项目集里有归约项目A -> α.,只有在当前输入符号属于FOLLOW(A)时才执行归约,否则不归约。这样很多冲突就被消解了。

但SLR(1)也有局限。有些文法即使在SLR(1)下冲突,在LR(1)下却可以无冲突。LR(1)分析对每个项目额外附带了一个“展望符”,表示归约时下一个输入符号允许是什么。这个展望符更精确,所以LR(1)分析能力更强。代价是状态数量大得多,通常会有几千个状态,工程上不实用,所以实际用的时候往往用LALR(1)(Look-Ahead LR),它是LR(1)的简化版,状态数量与SLR(1)相近,分析能力却接近LR(1)。Yacc和Bison这类语法分析器生成器,底层用的其实就是LALR(1)算法。

考试做题的优先级是:先判断文法能不能用LR(0)直接搞定,不行就试SLR(1),再不行就上LR(1)。课后习题一般只让你做到SLR(1)为止,个别提高题才要求构造LR(1)分析表。构造LR(1)分析表前,一定要先算FOLLOW集,因为SLR(1)表里归约动作的选择完全依赖它——这是我当时踩过最大的坑,FOLLOW集算错一个符号,整张表全废。

4.4 典型习题:构造SLR(1)分析表的完整套路

下面用经典文法做个完整示范。给定文法:

E -> E + T | T T -> T * F | F F -> ( E ) | id

第一步,先把文法拓广,加一条E' -> E,目的是让开始符号有一个唯一的接受状态。第二步,求所有LR(0)项目集。第三步,画出状态转换图。第四步,在每个项目集里,若圆点后是终结符,则在该终结符对应的列填“移进到对应状态”;若圆点后是非终结符,则填“转移到对应状态”;若出现归约项目A -> α.,则对FOLLOW(A)中的每个终结符所在的列填“用A -> α归约”。第五步,检查填出来的动作表(ACTION)和状态转移表(GOTO)有没有一个格子待多个动作,有就是冲突,要考虑换别的分析方法。

这道题做完你会发现一个规律:在含有F -> ( E ).的项目集里,归约项目要用到FOLLOW(F),而FOLLOW(F)中包含+ * $ )这些终结符。有些符号同时还可能触发其他项目要求移进,于是冲突就冒头了。真正理解SLR(1)冲突来源的地方,恰恰就在这类题目上。

5. 课后习题精讲:四个高频题型手把手过一遍

5.1 题型一:给定文法,求FIRST集和FOLLOW集

这种题属于语法分析的基本功,几乎每次考试都有。做的时候按部就班来,先标注所有能推出ε的非终结符,再算FIRST,最后算FOLLOW。以文法:

S -> a S e | B B -> b B | ε

为例。第一步,看哪些非终结符能推出ε:B能推出ε,S可以通过S -> BB -> ε推出ε,所以S和B都能推出ε。第二步,算FIRST:FIRST(B) ={b, ε};FIRST(S) 包含a(来自S -> aSe)、FIRST(B)里的b,因为S能推出ε,所以ε也属于FIRST(S)。所以FIRST(S) ={a, b, ε},FIRST(B) ={b, ε}。第三步,算FOLLOW:给FOLLOW(S)放入$;看S -> a S e,e跟在S后面,所以e进FOLLOW(S);看S -> B,B在末尾,FOLLOW(S)的所有元素都要给FOLLOW(B),所以FOLLOW(B)至少包含$和e。另外FOLLOW(S)里还有a?不对,a出现在S后面吗?没有,a在S前面。所以FOLLOW(S) ={$ , e},FOLLOW(B) ={$ , e}

这类题做完要多检查一遍,尤其注意“A在产生式右部末尾”的传递情况。我考前一晚上用十来道题专门练这个,把每个符号的FIRST和FOLLOW都仔细推导一遍,效果比抄十遍笔记好得多。

5.2 题型二:判断文法是否为LL(1)文法

判断步骤是死的:先消除左递归和公共左因子(如果存在),再计算FIRST集和FOLLOW集,最后检查同一非终结符不同产生式的FIRST集是否相交,以及存在ε产生式时FIRST和FOLLOW是否相交。

有一道很经典的习题是这样的:

A -> a B A' A' -> a A' | ε B -> b

这里A'的产生式有a A'ε两条。FIRST(A') ={a, ε},FOLLOW(A') = FOLLOW(A)(因为A'在产生式末尾),如果FOLLOW(A)里含有a,就会产生冲突。考试时这个FOLLOW(A)怎么算,要看其他产生式对A的引用。如果某个产生式是S -> A a,那么a就进了FOLLOW(A),于是FIRST(A') ∩ FOLLOW(A') 非空,文法不是LL(1)。这个题极好地考察了“FIRST集与FOLLOW集不交集”这一条。

5.3 题型三:给定文法构造预测分析表

构造预测分析表的核心环节是“面对非终结符和终结符的组合,该选哪条产生式”。一旦FIRST和FOLLOW算对,填表只是体力活。但如果某个格子出现两条以上产生式,那就是LL(1)冲突,你需要在答卷上明确指出这表示文法不具有LL(1)性质。

我建议做题时先画一个空表,行是非终结符,列是所有终结符加$,然后逐条产生式填。对于产生式A -> α,若FIRST(α)含终结符a,则在格子(A, a)里填A -> α;若FIRST(α)含ε,则在所有(A, b)b属于FOLLOW(A) 的格子里填A -> α。每填完一个产生式,都要回头check一遍,看有没有格子被填了多次。被填多次的地方,就是解“用该文法构造预测分析器是否可行”这类题的关键证据。

5.4 题型四:LR分析过程中写出分析栈的变化

这类题要求你手写输入串的LR分析过程,通常给一个已经构造好的ACTION表和GOTO表,让你模拟分析栈和输入串的变化。模拟时维护两个数据结构:状态栈和符号栈(有的教材合二为一)。每一步查表:如果ACTION[当前状态][当前输入符号]是“移进”,就把输入符号和新的状态压栈;如果是“归约”,就按产生式右部长度弹出栈顶若干项,查GOTO表,把左部非终结符和新状态压栈;如果是“接受”,分析成功。

有一次我做题做迷糊了,把移进符号的顺序搞反了,结果分析过程跟答案差了好几步。后来总结出一个小技巧:每一步先把当前状态栈的栈顶和输入串的第一个符号单独写出来,再考虑该查哪一格。这样不容易乱,写字也快。模拟LR分析是考试中比较繁琐但拿分稳的题型,练熟后基本是送分题。

6. 复习笔记:语法分析高频考点速查表

6.1 必背考点清单

下面这份清单是我当年期末复习时整理的,覆盖了语法分析章节最常见的考点,考前两天对着过一遍很有用。

考点核心要点常见题型
上下文无关文法定义四元组:终结符、非终结符、产生式、开始符号选择题/判断题
推导与归约最左推导、最右推导、句柄大题前几问
语法树根为开始符号,叶子为终结符画图题
二义性存在两棵不同语法树的句子证明/改写题目
FIRST集可能推导出的串的首终结符集合计算题
FOLLOW集句型中紧跟某非终结符的终结符集合计算题
LL(1)条件FIRST集不相交,与FOLLOW集无交集判断题
预测分析表行列分别为非终结符和终结符构造题
LR(0)项目圆点位置决定项目类型构造题
SLR(1)用FOLLOW集解决归约冲突构造/判断题
LR(1)与LALR(1)展望符、能力与状态数权衡概念选择题

6.2 最容易扣分的五个细节

细节一:忘记给开始符号的FOLLOW集放入$。这个错误非常隐蔽,因为有时FOLLOW(S)本身就有内容,你光顾着加终结符,忘了输入结束标记。考试阅卷时这个$往往是一个扣分点。

细节二:求FIRST集时忽略空串影响。A -> B C这条产生式里,如果B能推出ε,FIRST(A)就还得并入FIRST(C)的内容。漏掉“B推空串进一步看后续符号”这一步,答案就会少符号。

细节三:构造LR(0)项目集闭包时,忘了把新的非终结符产生式带圆圈加进去。很多时候写了圆点后面的非终结符,但没继续展开它的产生式,项目集就不全,后面的状态转移也跟着错。

细节四:SLR(1)的归约动作看FOLLOW集,但FOLLOW集本身算错。FOLLOW集错一处,ACTION表里归约列的符号就全偏了。所以我在做题时有个原则:先单独用一小块地方算FOLLOW,算完再回填到表里。

细节五:二义性文法消除后,没有验证新文法和原文法描述的是同一种语言。有些改写虽然消了二义,但把能接受的句子集合也改了,这在语义上是错的。

7. 常见问题与面试向考点

7.1 复习时容易卡壳的几个点

很多同学会纠结“LR(1)和LALR(1)到底要不要掌握到能手工构造的程度”。我的经验是:为了考试,重点掌握LR(0)和SLR(1),手工构造LR(1)分析表属于提高题;为了工程和面试,理解LALR(1)的动机就够。真让你在考场上构造LR(1)分析表,状态数会爆炸,时间根本不够。但概念题里会问它们之间的关系,比如“为什么说LALR(1)是LR(1)的压缩版”“LALR(1)合并状态后可能引入什么冲突”,这些要能讲清楚。

还有同学经常会问“递归下降和LL(1)有什么关系”。递归下降是一种程序实现方法,LL(1)是一种文法性质。理论上,任何LL(1)文法都可以写一个不带回溯的递归下降分析程序;反过来,手写的递归下降分析器如果遇到需要回溯的地方,可以看作是试图处理非LL(1)文法。实际工程里很多手写解析器会做一些预读和分支处理,本质上就是在LL(1)基础上扩展。理解这个对应关系,面试时被问到“你写过解析器吗”就不会发虚。

7.2 面试高频问题与答题思路

面试题里关于语法分析的提问,往往不会太深,但会结合工程实践。比如:

问:写一个表达式解析器,你会怎么处理运算符优先级?

这类题我建议从文法和递归下降两个层面回答。先给出分层文法Expr -> Term (+|-) TermTerm -> Factor (*|/) FactorFactor -> number | ( Expr ),然后说明用递归下降函数parseExprparseTermparseFactor分别对应这三层,每个函数循环处理同优先级的运算符。这种回答既展示了编译原理功底,又表明你真写过代码。

问:什么是左递归?为什么需要消除?

从产生式A -> A α说起,指出其会导致自顶向下分析直接死循环。解决方案是改写为右递归形式A -> β A'A' -> α A' | ε,并说明直接转成右递归后会产生左结合性问题,必要时需要语义动作或中间表示来处理结合性。表达清楚“消除左递归是为了适配LL分析方法,而LR类方法本身能处理左递归”这个对比,基本就能拿到不错评价。

问:你了解语法分析器生成器吗?

可以从Yacc/Bison的用法谈起:写文法规则,用语义动作生成抽象语法树,生成器内部处理LR/LALR分析。最好举一个真实的例子,比如用Bison解析一个mini计算器,或者解析配置文件。只要你能说清“规则由用户提供,状态机由工具生成”这个分工,面试官就知道你不是只背了概念。

8. 实操心得与扩展:从课后题到实验室再到工程

语法分析的课后习题做完,只是掌握了纸面上的套路。我印象里比较深的是第一次做词法分析实验和语法分析实验的衔接:词法分析器输出token流,语法分析器读入token流构建语法树。一开始我偷懒,把词法分析的结果打印到文本文件,语法分析再读这个文件,后来发现直接通过管道传递更方便。这个看似工程上的小决策,其实也体现了“模块间接口设计”的思路,语法分析器不关心token是怎么来的,只关心token的顺序对不对。

如果你正在做一个完整实验项目,比如用Java做一个C语言子集的编译器,我建议语法树的数据结构提前设计好。每个节点至少要有节点类型、Token信息、子节点列表。用LL(1)做递归下降时,节点构造几乎和产生式一一对应;用Yacc/Bison生成解析器时,每个归约动作里也要new一个节点。提前把节点定义写清楚,后面做语义分析和中间代码生成时能少改很多代码。

做实验还有一个容易被忽视的问题:错误处理。教材里教你构造分析表,都是假定输入是合法的,但真实输入永远有错误。课堂上说的“恐慌模式”恢复策略在实验里非常好用:当预测分析遇到表项为空时,不断跳过输入符号,直到遇到同步符号(比如分号、右括号、$)为止。同步符号的选择来自FOLLOW集,这正好能把课上学到的FIRST/FOLLOW知识用起来。

我个人的感受是,语法分析是编译原理课程里承上启下的部分,前面词法分析相对独立,后面语义分析和代码生成都要建立在语法树之上。如果这块没学扎实,后面的实验会越做越吃力。反过来,一旦把FIRST集、FOLLOW集、预测分析表、LR自动机这些概念真正吃透了,你会发现写解析器不再神秘,无论是手写递归下降还是使用解析器生成器,都能有自己的判断。希望这篇复习笔记和习题思路能帮你少走点弯路,也欢迎你在评论区聊聊自己做题时的困惑。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询