☰
LL(1)分析法实现IF-ELSE翻译程序:四元式与真假链回填
2026/10/10 21:10:38 网站建设 项目流程

简介:一份面向编译原理学习者的IF-ELSE条件语句翻译程序设计资料,基于LL(1)预测分析法,完成词法分析、语法分析并输出四元式中间代码。资料以Visual Studio工程形式组织,共17个文件,包含C++源码、头文件、工程配置文件,以及调试生成的中间文件,便于对照完整项目结构理解编译器前端各模块的实现思路。压缩包整体约417KB,体积小巧,便于快速部署和运行;已有540人浏览学习,适合正在做编译原理课程设计或想动手实践语法分析的读者。借助该工程,可查看LL(1)文法设计、预测分析表构建、IF-ELSE嵌套与跳转语句的四元式生成等关键流程,同时从工程配置与调试产物中学习实际编译项目的组织方式和排错方法;将编译原理理论落实到可运行的程序实现中,是课程设计或实验报告的重要参考。

1. 用 LL(1) 法写 IF-ELSE 条件语句翻译程序:难点不在条件,在真假链回填

这个标题我在带编译原理课程设计时几乎每年都会被学生问到。IF-ELSE 条件语句的翻译程序设计(LL(1) 法、输出四元式),一句话说就是:用 LL(1) 分析法做语法分析,在分析过程中同步生成中间代码四元式,把「条件成立走这段、否则走那段」的逻辑变成一串带跳转的四元组。适合三类人:正在做编译原理课设的学生、要给自己写的脚本语言加条件分支的开发者、以及想搞懂编译前端「分析 + 翻译」到底怎么衔接的从业者。实际动手会发现,IF-ELSE 本身不复杂,复杂的是布尔表达式的真假链回填和悬空 else 的处理,这两块才是四元式能不能生成对的关键。

2. 文法改造与 LL(1) 判定:悬空 else 是第一个坑

2.1 IF-ELSE 的原始文法为什么过不了 LL(1) 判定

先给一版最直觉的 IF-ELSE 文法:

S -> IF E THEN S | IF E THEN S ELSE S | ID := E;

这版文法存在两个致命问题。第一是左公因子:IF E THEN S在两条产生式里重复出现,导致预测分析表里M[S, IF]这一格有两个候选,LL(1) 判定直接不通过。第二是悬空 else:输入IF a>b THEN IF b>c THEN x:=1 ELSE x:=2时,这个ELSE既可以匹配内层 IF,也可以匹配外层 IF,文法存在二义性。

LL(1) 判定有三个硬条件:同一非终结符的多个候选 FIRST 集互不相交;若某个候选能推导出 ε,那么 FIRST(该候选) 与 FOLLOW(该非终结符) 不能有交集;每个终结符在分析表中只能落到一个格子。原始文法连第一条都满足不了,所以必须改造。

悬空 else 的行业共识是「就近匹配」:ELSE 永远匹配离它最近的、尚未匹配的 IF。这个原则用自然语言说很简单,但要在文法里把它固化下来,就需要下面的改造。

2.2 三步改造成 LL(1) 文法:提取公因子与处理空产生式

改造分三步:消除左公因子、消除左递归、用显式 ENDIF 终结符收尾。

第一步处理最外层 IF 语句的左公因子,把IF E THEN S提出来,后面接一个尾巴S',由S'决定是否存在 ELSE 分支。第二步处理表达式里的左递归,比如E -> E OR T这种写法在 LL(1) 下无解,必须转成右递归。第三步给每个 IF 加一个显式ENDIF作为闭合标记,这样回填跳转目标时有一个确定的锚点。

改造后的完整文法如下:

P -> SS SS -> S SS | ε S -> IF E THEN S S' | ID := E; S' -> ELSE S | ENDIF E -> T E' E' -> OR T E' | ε T -> F T' T' -> AND F T' | ε F -> NOT F | (E) | R R -> ID R_tail | NUM R_tail -> RELOP ID | ε

逐个说下设计意图:SS表示语句串,可以连续出现多个 S;S的两条产生式分别以IF和ID开头,FIRST 集不冲突;S'用ELSE和ENDIF两个终结符区分有无 else 分支;E系列做布尔表达式翻译,OR和AND被拆成E'、T'两个尾巴,避免左递归;R处理关系表达式ID > NUM这类比较,其中ID RELOP ID的左公因子通过R_tail消除。

这套文法里S'、SS、E'、T'、R_tail都可能推导出 ε,空产生式的启用时机要严格看当前输入 token 是否落在对应非终结符的 FOLLOW 集里,这一点在 2.3 的表格里能直接查到。

2.3 FIRST 与 FOLLOW 集:手工算一遍,分析表才有依据

预测分析表不是凭空写的,每一格都来自 FIRST 和 FOLLOW 集。下面这个表是按程序结束符$、以显式ENDIF闭合 IF 的简化文法算出来的,常见课设和教学场景可以直接抄:

非终结符FIRSTFOLLOW
S{ IF, ID }{ IF, ID, ENDIF, $ }
S'{ ELSE, ENDIF }{ IF, ID, ENDIF, $ }
E{ NOT, (, ID, NUM }{ THEN, ;, ) }
E'{ OR, ε }{ THEN, ;, ) }
T{ NOT, (, ID, NUM }{ OR, THEN, ;, ) }
T'{ AND, ε }{ OR, THEN, ;, ) }
F{ NOT, (, ID, NUM }{ AND, OR, THEN, ;, ) }
R{ ID, NUM }{ AND, OR, THEN, ;, ) }
R_tail{ RELOP, ε }{ AND, OR, THEN, ;, ) }

注意几个关键点。FOLLOW(S)里的IF、ID来自SS -> S SS,因为一个语句后面还能跟另一个语句;FOLLOW(E)里的THEN来自S -> IF E THEN S S',;来自赋值语句ID := E;,)来自F -> (E)。E'、T'、R_tail这些带 ε 的产生式,只有在当前 token 落到 FOLLOW 集时才启用,这是后面查分析表最容易出错的地方。

有了这张表,预测分析表的构造就是机械动作:对每个产生式A -> α,把 α 的 FIRST 集中每个终结符填入M[A, token] = 产生式编号;如果 α 能推导出 ε,再把 FOLLOW(A) 中每个终结符也填入M[A, token] = 产生式编号。手算一遍这张表,比直接背任何现成分析表都靠谱。

3. 四元式设计与真假链:回填方案决定整个翻译程序的骨架

3.1 四元式的四个字段如何表示跳转和赋值

四元式是 (op, arg1, arg2, result) 四元组,本质上是一条扁平化的中间指令。翻译 IF-ELSE 语句时,涉及的操作符就这几类::=赋值、关系比较><==等、逻辑跳转JZ(arg1 为假则跳转到 result)、无条件跳转JMP(直接跳转到 result)。

操作符arg1arg2result语义
:=常量或变量-变量赋值
><==操作数操作数临时变量比较结果存临时变量
JZ临时变量-四元式序号为假(0)则跳转
JMP--四元式序号无条件跳转

result 字段在四元式里有两种完全不冲突的用法:运算型四元式里它存临时变量名或目标变量名,跳转型四元式里它存跳转目标的四元式序号。初学者最容易忽略的是这一点——跳转目标往往在生成时还没确定,于是 result 字段先留空,等目标算出来再回填。

3.2 一个 IF-ELSE 的四元式长什么样

看一个具体例子。输入语句:

IF A > B THEN C := 1 ELSE C := 2 ENDIF

期望输出的四元式序列是:

1 (>, A, B, T1) // 条件比较,结果存临时变量 T1 2 (JZ, T1, -, 5) // T1 为假(条件不成立),跳到 ELSE 分支 3 (:=, 1, -, C) // THEN 分支:C = 1 4 (JMP, -, -, 6) // 执行完 THEN 跳过 ELSE 分支 5 (:=, 2, -, C) // ELSE 分支:C = 2 6 (后续四元式) // ENDIF 之后的代码

这里第 2 条JZ一开始生成时,ELSE 分支入口序号还是未知的,所以 result 先留空;第 4 条JMP生成时,ENDIF 后的序号也是未知的,同样要留空。等到语法分析继续往下走,ELSE 分支第一条四元式确定了、ENDIF 之后的第一条四元式确定了,才能把这两个坑填上。

3.3 真假链与回填:跳转目标后确定怎么处理

跳转目标后确定,这是翻译 IF-ELSE 的核心矛盾。解决办法是真假链 + 回填:把「等待回填」的跳转四元式序号用链表串起来,链头用一个变量保存,等目标序号确定后,沿着链表把每个节点的 result 字段改写成真正的目标序号。

具体到 IF-ELSE 语句,需要在文法产生式右部插入语义动作标记,常见做法是插入M1到M5这样的标记符号:

S -> IF M1 E M2 THEN M3 S1 M4 S' S' -> ELSE M5 S2 | ENDIF

每个语义动作标记的职责用表格列清楚:

标记触发位置动作
M1IF 之后记录 IF 之前的四元式序号(调试用)
M2E 分析完成处取出 E 产生的真链与假链
M3THEN 之后记录 THEN 分支入口序号,把 E 的真链全部回填到这里
M4S1 分析完成处生成一条JMP,序号入「待回填链」,用于跳过 ELSE 分支
M5ELSE 之后记录 ELSE 分支入口序号,把 E 的假链回填到这里

S' -> ENDIF的语义动作是最后一步收尾:把 M4 积累的JMP链全部回填到 ENDIF 之后的四元式序号;如果这个 IF 没有 ELSE 分支,那么 E 的假链也要回填到 ENDIF 之后的序号,因为条件不成立时整个 IF 语句直接结束。这套标记方案就是整个翻译程序的骨架,第 4 章的代码实现完全围绕它展开。

4. 预测分析表驱动的代码实现:栈、回填与语义动作的配合

4.1 程序框架:token 流、分析栈、四元式表三件套

翻译程序的整体流程分三个阶段:词法分析先把源码拆成 token 流,终结符包括IF、ELSE、THEN、ENDIF、ID、NUM、RELOP、:=、;、(、)、OR、AND、NOT;然后 LL(1) 预测分析器拿着 token 流驱动分析栈查表;每次按产生式展开时触发对应的语义动作,往四元式表里写记录。

这里的关键设计是:分析栈里存的不是单纯的文法符号,而是三类元素的混合体——终结符用 token 的 type 值,非终结符用自定义编号,语义动作标记用另一套编号。这样在主循环里遇到栈顶元素时,一眼就能看出该走「匹配终结符」还是「展开产生式」还是「执行语义动作」。

4.2 核心数据结构:四元式表、分析栈、符号表

C 语言风格的核心数据结构如下:

#define MAX_QUAD 256 #define MAX_STACK 128 #define MAX_SYM 64 typedef struct { char op[8]; // 操作符:":="、"JZ"、"JMP"、">"、"<" 等 char arg1[32]; char arg2[32]; char result[32]; } Quad; Quad quad[MAX_QUAD]; int qc = 0; // 下一个可用四元式序号,也是新四元式插入位置 int stack[MAX_STACK]; int top = 0; char sym_table[MAX_SYM][32]; int sym_count = 0; // 非终结符与语义动作编号 #define NT_S 200 #define NT_SP 201 // S' #define NT_SS 202 #define NT_E 203 #define NT_EP 204 // E' #define NT_T 205 #define NT_TP 206 // T' #define NT_F 207 #define NT_R 208 #define NT_RT 209 // R_tail #define ACT_M1 301 #define ACT_M2 302 #define ACT_M3 303 #define ACT_M4 304 #define ACT_M5 305

四元式表quad是全局数组,qc是游标,所有语义动作都通过qc拿到「当前四元式序号」,这也是回填时最常使用的锚点。分析栈用 int 数组实现,压栈的是编号;符号表只做一件事——记录临时变量和用户变量的名字与值,方便最后验证四元式执行结果。栈里的终结符直接复用 token 的 type 值,这样主循环里比较栈顶与当前 token 时不需要额外映射。

4.3 主循环:展开产生式的同时执行语义动作

LL(1) 预测分析的主循环是典型的「查表、压栈、匹配」三步走。核心逻辑如下:

while (top > 0) { int sym = stack[top]; if (sym == token.type) { // 栈顶是终结符且与当前 token 匹配:弹出并读下一个 token top--; next_token(); } else if (sym >= 300) { // 栈顶是语义动作标记:弹出并执行对应动作 top--; switch (sym) { case ACT_M2: { // E 分析完毕,此时 true_chain/false_chain 已被 E 的产生式填好 true_chain = get_true_chain(); false_chain = get_false_chain(); break; } case ACT_M3: { // THEN 分支入口就是当前四元式序号 int entry = qc; backpatch(true_chain, entry); break; } case ACT_M4: { // THEN 分支结束,生成一条 JMP 并加入待回填链 emit("JMP", "", "", ""); jmp_chain = merge_chain(jmp_chain, qc - 1); break; } case ACT_M5: { // ELSE 分支入口就是当前四元式序号,回填 E 的假链 int else_entry = qc; backpatch(false_chain, else_entry); break; } } } else { // 栈顶是非终结符:查预测分析表,展开产生式并逆序压栈 int row = nt_row(sym); int col = token_col(token.type); int prod = parse_table[row][col]; if (prod < 0) error("语法错误"); top--; // 把该产生式的右部符号逆序压栈 for (int i = prod_len[prod]; i >= 1; i--) { stack[++top] = prod_symbols[prod][i]; } } }

这个循环里最容易写错的是最后那个逆序压栈:产生式右部符号是按从左到右的顺序存的,压栈必须从右往左压,否则语义动作的执行顺序会整个颠倒。prod_symbols[prod][i]是预先定义好的产生式右部符号表,下标从 1 开始,0 留给左部非终结符。parse_table的行列索引分别由nt_row和token_col两个映射函数算出,表中的值是产生式编号,-1 表示该格子为空、语法错误。

4.4 回填函数、四元式生成与一颗完整输出

回填是四元式翻译最重要的工具函数,链的每个节点就是一条等待回填的跳转四元式序号,节点间的「下一跳」信息借用 result 字段存放:

void backpatch(int chain, int dst) { while (chain != 0) { int next = atoi(quad[chain].result); // 链的下一节点序号存在 result 中 quad[chain].result = dst; // 回填真正的跳转目标 chain = next; } } int merge_chain(int c1, int c2) { if (c1 == 0) return c2; int p = c1; while (atoi(quad[p].result) != 0) { p = atoi(quad[p].result); } quad[p].result = c2; return c1; } void emit(char *op, char *arg1, char *arg2, char *result) { strcpy(quad[qc].op, op); strcpy(quad[qc].arg1, arg1); strcpy(quad[qc].arg2, arg2); strcpy(quad[qc].result, result ? result : ""); qc++; }

backpatch的参数chain是链头序号,dst是回填目标;merge_chain把两条链接成一条,返回合并后的链头;emit的四个参数与四元式四字段一一对应,qc在调用后自增。注意merge_chain里借用 result 字段找链尾的写法,前提是链中每个节点都是未回填状态,一旦回填过就不能再入链。

对IF A > B THEN C := 1 ELSE C := 2 ENDIF这行输入,按上述代码走完,四元式表内容如下:

序号 op arg1 arg2 result 1 > A B T1 2 JZ T1 5 3 := 1 C 4 JMP 6 5 := 2 C

观察输出与语义动作的对应关系:第 1 条由R -> ID RELOP ID的关系表达式产生式生成,第 2 条由E产生式的语义动作生成但目标留空,第 3 条由赋值语句产生式生成,第 4 条由 M4 触发生成,第 5 条则是 M5 回填后恰好成为 ELSE 分支入口。整个过程中「生成时留空 + 后续回填」交替出现,这就是回填机制在驱动翻译。

5. 实现路上的 5 个典型踩坑:从悬空 else 到回填错位

5.1 悬空 else 归属翻车:ELSE 总是被内层 IF 抢先匹配

现象:嵌套 IF 语句翻译出来的四元式逻辑不对,比如IF a>b THEN IF b>c THEN x:=1 ELSE x:=2 ENDIF ENDIF里的 ELSE 被错误地匹配到外层 IF,导致外层条件不成立时反而执行了x:=2。

原因:这里通常有两种情况。一种是你偷懒用了原始二义性文法构造分析表,LL(1) 判定本身就过不了;另一种是递归下降实现里,S'看到ELSE就直接归约,没有把 ELSE 绑定到「最近未闭合的 IF」的栈帧上。

解决:严格使用 2.2 改造后的文法,S'的两个候选ELSE S和ENDIF用当前 token 区分。实现递归下降时,S'遇到ELSE就递归处理S,遇到ENDIF就返回,这样每个 ELSE 天然属于最近的S'。分析栈驱动版本注意假链要用栈结构管理,先入栈的 IF 的假链后回填,保证匹配顺序。

5.2 THEN 分支执行完没跳过 ELSE:四元式里少了一条 JMP

现象:输入包含完整 THEN 和 ELSE 分支时,THEN 分支的四元式执行完直接「落入」ELSE 分支,两组赋值都执行了。

原因:这是 M4 语义动作漏写,或者只在检测到 ELSE 存在时才生成JMP。如果没有 ELSE 分支,THEN 分支执行完自然走到 ENDIF 后,不需要跳转;但一旦存在 ELSE,THEN 分支末尾必须有一条JMP跳过 ELSE 分支的代码。

解决:不要用「有没有 ELSE」来决定是否生成JMP,在 M4 处无条件生成JMP并入待回填链。如果没有 ELSE 分支,这个JMP回填到 ENDIF 后的序号,等价于顺序执行,多一条空跳不影响正确性;如果有 ELSE 分支,它精确跳过 ELSE 代码块。用这个统一策略,实现时只需要一套回填代码,少一个条件分支也少一个 bug。

5.3 FIRST/FOLLOW 集手算对,程序查表却报「无候选」

现象:分析表构造时某些格子缺项,比如栈顶是E'、当前 token 是THEN,查表直接报语法错误;但你手工算的 FOLLOW(E') 里明明有THEN。

原因:基本都是 FOLLOW 集漏算。FOLLOW(E)既要算S -> IF E THEN S S'里的THEN,也要算S -> ID := E;里的;,还要算F -> (E)里的)。三个来源缺一个,空产生式E' -> ε就可能在对应 token 上无法启用。

解决:构造分析表前,把 FOLLOW 集三个来源逐一核对:右部末尾的非终结符继承左部的 FOLLOW;A -> αBβ中 B 的 FOLLOW 包含 FIRST(β)(去掉 ε);若 β 可推导出 ε,B 的 FOLLOW 还包含 FOLLOW(A)。自查时对着 2.3 的表格逐项勾选,重点看那些带 ε 的产生式。

5.4 语义动作标记入栈顺序颠倒,四元式序号全乱

现象:生成的四元式顺序完全不对,回填出来的目标序号差 1 或者指向错误四元式,调试时发现qc跳变。

原因:产生式展开时忘了逆序压栈。S -> IF M1 E M2 THEN M3 S1 M4 S'的右部符号列表是正序的,压栈必须从最后一个符号开始反向压入,否则第一个弹出的不是IF而是S',语义动作 M1 到 M5 的执行顺序会彻底倒过来。

解决:压栈循环统一写成for (int i = prod_len; i >= 1; i--) push(prod_symbols[i]),并且把语义动作标记也当作普通符号参与逆序。调试时在压栈后打印整个栈的内容,对照产生式右部检查顺序,一眼就能看出问题。

5.5 布尔表达式真假链在 NOT 和 OR 处断开,回填跳反

现象:IF NOT (A > B) THEN ...或IF A > B OR C > D THEN ...这类条件翻译后,跳转方向与预期相反,真分支和假分支互换。

原因:F -> NOT F需要把子表达式的真假链互换:子表达式的真链变成整个表达式的假链,假链变成真链。E' -> OR T E'的处理更隐蔽:左侧 E 的假链不能直接回填到整个 OR 表达式的假出口,而应该回填到 OR 右侧操作数的入口,因为只要右侧为真,整个表达式就为真。很多实现把假链直接存起来,最后统一回填到 ENDIF,导致 OR 的「短路」语义丢失。

解决:对NOT在做完子表达式翻译后立即调用swap_chain()交换真假链。对OR,在M2处保存假链后,构造T E'之前先把假链回填到右侧操作数的第一条四元式序号,再把右侧生成的真链与左侧真链合并作为整个 OR 的真链。这里建议单独写一个translate_or_tail()函数,别把 OR 的语义动作堆在主循环里,不然短路逻辑会和普通顺序翻译混在一起。

6. 验证四元式,再谈进阶:让这段中间代码真正跑起来

6.1 用极简解释器让四元式跑起来

四元式生成后,最直接的验证方式是写一个几十行的极简解释器,按顺序执行四元式,观察变量值变化:

int pc = 0; int temp_flag = 0; while (pc < qc) { Quad *q = &quad[pc]; if (strcmp(q->op, ">") == 0) { int left = sym_get(q->arg1); int right = sym_get(q->arg2); temp_flag = left > right; strcpy(temp_name, q->result); } else if (strcmp(q->op, "JZ") == 0) { if (temp_flag == 0) { pc = atoi(q->result); continue; } } else if (strcmp(q->op, "JMP") == 0) { pc = atoi(q->result); continue; } else if (strcmp(q->op, ":=") == 0) { sym_set(q->result, atoi(q->arg1)); } pc++; }

sym_get和sym_set是符号表读写函数,temp_flag模拟临时变量T1的取值。验证时先给A=10, B=5跑一遍,确认 THEN 分支执行;再给A=3, B=5跑一遍,确认 ELSE 分支执行;嵌套 IF 则给多组数据交叉验证。

6.2 进阶:从四元式还原 AST 或直接生成目标代码

四元式验证通过后,这个翻译程序就算真正闭环了。更有价值的进阶方向是把四元式还原成抽象语法树:跳转四元式对应 IF 节点,赋值四元式对应赋值节点,还原后的 AST 能做常量折叠、死代码删除等优化;或者直接把四元式映射成栈式虚拟机的指令,例如JZ对应条件跳转指令、JMP对应无条件跳转指令,这条路走向的是完整解释器与 JIT。

6.3 我的习惯

我带课设时反复强调一个习惯:先跑「THEN 和 ELSE 都有副作用」的测试用例,例如两个分支都打印输出或都改变不同变量,确认两条路径都被执行到;再跑嵌套 IF 的用例,专门检查悬空 else 归属;最后才跑 NOT、OR 组合的复杂布尔条件。每次都把四元式输出与手推结果逐行对照,而不是只看最终变量值。回填这块的玄学坑我当年也踩过不少,现在回看,出问题的地方几乎都集中在 5.3 的 FOLLOW 集漏算和 5.4 的压栈顺序这两个点上。希望这篇拆解能帮你把 LL(1) 翻译程序的骨架一次搭对。

本文还有配套的精品资源,点击获取

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

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

立即咨询