☰
LL(1)分析法与四元式生成:IF-ELSE语句翻译实战
2026/10/14 6:44:32 网站建设 项目流程

简介:这份资源是编译原理课程中IF-ELSE条件语句翻译程序设计的完整实现包,面向正在学习语法分析与中间代码生成的高校学生及自学者。它围绕LL(1)预测分析法展开,演示如何对条件语句进行词法识别、语法分析并输出四元式,帮助读者理解编译器前端从源码到中间代码的转换过程。压缩包共17个文件,约417KB,包含cpp与h源文件、vcproj与sln工程文件、rc与res资源文件,以及pdb、idb、ncb等调试与浏览信息文件,可直接在Visual Studio中打开编译运行。其中compare相关模块很可能涉及比较操作的四元式生成,便于对照理解条件判断的中间代码表示。目前已有540人学习下载,适合作为课程实验参考,帮助读者掌握嵌套IF-ELSE的解析流程、四元式序列构造及跳转指令的生成思路,为后续代码优化与目标代码生成打下基础。

1. 从一段 if-else 到四元式:编译前端里最值得手写一遍的翻译链路

很多人第一次接触编译原理,都会被 LL(1) 分析表和四元式这两样东西同时卡住。前者是一张冷冰冰的预测表,后者是一串看不出感情的(op, arg1, arg2, result)。可一旦把「if (a > b) x = 1; else x = 2;」这样一段代码,从字符流一路推到四元式序列,你会突然明白:所谓翻译程序设计,本质就是把语法结构映射成中间代码结构的机械过程。IF-ELSE 条件语句恰好是这条链路上最有代表性的节点——它同时涉及布尔表达式求值、回填、控制流跳转和语句块归约。LL(1) 法负责把语法结构识别出来,四元式负责把识别结果落成可执行的三地址码。这套组合是很多高校编译原理课程设计的标准题目,也是理解「语法分析如何驱动语义动作」最直接的入口。适合正在做课程设计、想手写一遍编译器前端、或者对中间代码生成一直没打通的人。

2. LL(1) 法拆解 IF-ELSE:文法、FIRST 集与预测分析表怎么落地

2.1 为什么 IF-ELSE 的文法不能直接拿来做 LL(1)

IF-ELSE 语句最经典的文法写法是这样的:

S → if E then S else S | if E then S | id := E

这个文法有一个致命问题:两条以if开头的产生式,FIRST 集完全相同。LL(1) 要求同一非终结符的任意两条产生式,FIRST 集不相交;如果某条产生式能推出空串,还要看 FOLLOW 集。这里if E then S和if E then S else S的前缀一样,分析器读到if之后根本不知道该选哪条,只能靠往后多看几个 token 才能决定——那就不是 LL(1) 了。

常见做法是提取左公因子,把文法改写成:

S → if E then S S' | id := E S' → else S | ε

这样S的两条产生式分别以if和id开头,FIRST 集不相交;S'的两条产生式分别以else开头和推出空串,FIRST 集也不相交。改写之后,文法满足 LL(1) 条件,可以构造预测分析表。

提示:提取左公因子是 LL(1) 文法改造的标准手段,但它改变的是语法结构,不是语言本身。改写前后接受的语句集合完全一致,只是分析路径不同。

2.2 FIRST 集、FOLLOW 集与预测分析表的构造步骤

构造 LL(1) 分析表之前,先把 FIRST 和 FOLLOW 算清楚。以改写后的文法为例,终结符集合为{if, then, else, id, :=, ε},非终结符为{S, S', E}。E 这里简化成单个id或id relop id,实际课程设计里可以再展开。

FIRST 集的计算规则:

  • 若 X 是终结符,FIRST(X) = {X}
  • 若 X → ε 是产生式,把 ε 加入 FIRST(X)
  • 若 X → Y1Y2...Yk,把 FIRST(Y1) 中除 ε 外的元素加入 FIRST(X);若 Y1 能推出 ε,继续看 Y2,以此类推

FOLLOW 集的计算规则:

  • 把$加入 FOLLOW(开始符号)
  • 若 A → αBβ,把 FIRST(β) 中除 ε 外的元素加入 FOLLOW(B)
  • 若 A → αB 或 A → αBβ 且 β 能推出 ε,把 FOLLOW(A) 加入 FOLLOW(B)

算完之后,对每条产生式 A → α,对 FIRST(α) 中每个终结符 a,把 A → α 填入 M[A, a];如果 α 能推出 ε,对 FOLLOW(A) 中每个终结符 b,把 A → ε 填入 M[A, b]。

下面用 Python 把这张表构造出来,方便后续驱动分析器:

# LL(1) 预测分析表构造(简化版 IF-ELSE 文法) grammar = { 'S': [['if', 'E', 'then', 'S', "S'"], ['id', ':=', 'E']], "S'": [['else', 'S'], ['ε']], 'E': [['id'], ['id', 'relop', 'id']] } terminals = {'if', 'then', 'else', 'id', ':=', 'relop', 'ε', '$'} non_terminals = {'S', "S'", 'E'} first = {t: {t} for t in terminals} first.update({nt: set() for nt in non_terminals}) follow = {nt: set() for nt in non_terminals} follow['S'].add('$') def compute_first(): changed = True while changed: changed = False for nt, prods in grammar.items(): for prod in prods: if prod == ['ε']: if 'ε' not in first[nt]: first[nt].add('ε'); changed = True continue for sym in prod: if sym in terminals: if sym not in first[nt]: first[nt].add(sym); changed = True break else: add = first[sym] - {'ε'} if not add <= first[nt]: first[nt] |= add; changed = True if 'ε' not in first[sym]: break else: if 'ε' not in first[nt]: first[nt].add('ε'); changed = True def compute_follow(): changed = True while changed: changed = False for nt, prods in grammar.items(): for prod in prods: for i, sym in enumerate(prod): if sym not in non_terminals: continue rest = prod[i+1:] if rest: add = set() for r in rest: if r in terminals: add.add(r); break add |= first[r] - {'ε'} if 'ε' not in first[r]: break else: add |= follow[nt] if not add <= follow[sym]: follow[sym] |= add; changed = True else: if not follow[nt] <= follow[sym]: follow[sym] |= follow[nt]; changed = True compute_first() compute_follow() # 构造预测分析表 table = {} for nt, prods in grammar.items(): for prod in prods: first_alpha = set() for sym in prod: if sym in terminals: first_alpha.add(sym); break first_alpha |= first[sym] - {'ε'} if 'ε' not in first[sym]: break else: first_alpha.add('ε') for a in first_alpha - {'ε'}: table[(nt, a)] = prod if 'ε' in first_alpha: for b in follow[nt]: table[(nt, b)] = prod for k in sorted(table, key=lambda x: (x[0], x[1])): print(k, '->', table[k])

这段代码的逻辑分三步:先迭代计算 FIRST 集直到不动点,再计算 FOLLOW 集,最后按规则填表。参数方面,grammar字典的 key 是非终结符,value 是产生式列表,每个产生式用符号列表表示,空串统一写成['ε']。实际课程设计里如果 E 展开成表达式文法,只需要往grammar里加产生式,FIRST/FOLLOW 和表的构造逻辑不用改。

跑出来之后,table[('S', 'if')]应该是['if', 'E', 'then', 'S', "S'"],table[("S'", 'else')]是['else', 'S'],table[("S'", '$')]是['ε']。这三个条目就是 IF-ELSE 翻译的核心决策点。

2.3 用预测分析表驱动语法分析的最小实现

有了表之后,分析器就是一个带栈的循环。栈里放符号,初始为['$', 'S'],输入串末尾补$。每次看栈顶和当前输入符号,查表决定展开还是匹配。

def parse(tokens, table): stack = ['$', 'S'] pos = 0 tokens = tokens + ['$'] while stack: top = stack[-1] cur = tokens[pos] if top in terminals or top == '$': if top == cur: stack.pop() pos += 1 if top == '$': return True else: raise SyntaxError(f"期望 {top},实际 {cur}") else: prod = table.get((top, cur)) if prod is None: raise SyntaxError(f"无法匹配 {top} 遇到 {cur}") stack.pop() if prod != ['ε']: for sym in reversed(prod): stack.append(sym) return False # 测试:if id relop id then id := id else id := id tokens = ['if', 'id', 'relop', 'id', 'then', 'id', ':=', 'id', 'else', 'id', ':=', 'id'] print(parse(tokens, table))

parse函数里,stack初始压入开始符号和结束符,pos指向当前 token。栈顶是终结符时直接匹配并前进;是非终结符时查表展开,展开时逆序压栈保证左部先处理。这个骨架就是后面挂语义动作的地方——每展开一条产生式,就可以触发对应的四元式生成逻辑。

3. 四元式生成:从语法树到 (op, arg1, arg2, result) 的映射规则

3.1 四元式的结构约定与 IF-ELSE 的控制流表达

四元式是典型的三地址码,每条指令形如(op, arg1, arg2, result)。对于 IF-ELSE,核心问题不是算术运算,而是控制流跳转。常见约定是:

四元式含义
(j>, a, b, T)若 a > b 则跳转到 T
(j, _, _, T)无条件跳转到 T
(:=, 1, _, x)把 1 赋给 x
(label, _, _, L)定义标号 L(可选)

对于if (a > b) x = 1; else x = 2;,一种直观的四元式序列是:

0: (j>, a, b, 2) 1: (j, _, _, 4) 2: (:=, 1, _, x) 3: (j, _, _, 5) 4: (:=, 2, _, x) 5: ...

这里第 0 条判断条件,真则跳到第 2 条执行 then 分支;否则顺序执行第 1 条无条件跳到第 4 条执行 else 分支。then 分支执行完第 3 条跳过 else。这种「跳转目标写死」的方式在简单例子里能看懂,但真正写翻译程序时,生成 then 分支的时候还不知道 else 分支的起始位置,所以需要回填。

3.2 回填技术:先占坑,后填地址

回填(backpatching)是 IF-ELSE 翻译里最关键的工程技巧。思路是:生成跳转指令时,目标地址先留空,把这条指令的编号记在一个列表里;等目标位置确定后,再统一回填。

在 LL(1) 递归下降或表驱动分析里,通常给每个非终结符配一套语义变量:

  • S.code:语句对应的四元式序列
  • S.true_list:条件为真时需要回填的跳转指令编号列表
  • S.false_list:条件为假时需要回填的跳转指令编号列表
  • S.nextlist:语句执行完后需要跳出的指令编号列表

对于 IF-ELSE 语句S → if E then S1 else S2,语义动作可以写成:

E.true_list = makelist(nextquad) # 生成条件跳转,目标待填 E.false_list = makelist(nextquad+1) # 生成无条件跳转,目标待填 S1.code = ... S2.code = ... S.code = E.code || S1.code || S2.code backpatch(E.true_list, S1 起始位置) backpatch(E.false_list, S2 起始位置) backpatch(S1.nextlist, S 结束位置)

下面用 Python 实现一个带回填的四元式生成器,配合前面的 LL(1) 分析器使用:

class QuadGenerator: def __init__(self): self.quads = [] # 四元式列表 self.next_temp = 0 # 临时变量计数 def emit(self, op, arg1, arg2, result): self.quads.append((op, arg1, arg2, result)) return len(self.quads) - 1 def makelist(self, i): return [i] def merge(self, *lists): result = [] for l in lists: result.extend(l) return result def backpatch(self, lst, target): for i in lst: op, a1, a2, _ = self.quads[i] self.quads[i] = (op, a1, a2, target) def new_temp(self): self.next_temp += 1 return f"t{self.next_temp}" def gen_if_else(self, cond_op, cond_a1, cond_a2, then_stmts, else_stmts): # 条件跳转:真则跳到 then 起始 j_true = self.emit(f"j{cond_op}", cond_a1, cond_a2, None) # 无条件跳转:假则跳到 else 起始 j_false = self.emit("j", "_", "_", None) then_start = len(self.quads) for stmt in then_stmts: self.emit(*stmt) j_end = self.emit("j", "_", "_", None) else_start = len(self.quads) for stmt in else_stmts: self.emit(*stmt) end = len(self.quads) self.backpatch([j_true], then_start) self.backpatch([j_false], else_start) self.backpatch([j_end], end) return self.quads # 生成 if (a > b) x = 1; else x = 2; 的四元式 gen = QuadGenerator() quads = gen.gen_if_else( ">", "a", "b", [(":=", "1", "_", "x")], [(":=", "2", "_", "x")] ) for i, q in enumerate(quads): print(f"{i}: {q}")

gen_if_else的逻辑是:先发射条件跳转和假跳转两条占位指令,记录 then 和 else 的起始位置,分别生成两个分支的代码,最后统一回填。backpatch遍历列表,把每条指令的 result 字段改成目标编号。参数cond_op是关系运算符,then_stmts和else_stmts是已经生成好的语句四元式列表。这个结构可以直接嵌到 LL(1) 分析器的语义动作里——每当分析器归约出if E then S1 else S2,就调用一次gen_if_else。

3.3 把语义动作挂到 LL(1) 分析器上

表驱动分析器本身不区分「语法阶段」和「语义阶段」,常见做法是在展开产生式时同步执行语义动作。对于S → if E then S1 else S2,可以在分析器维护一个语义栈,每个非终结符对应一个语义值对象。

class SemVal: def __init__(self): self.code = [] self.true_list = [] self.false_list = [] self.nextlist = [] self.place = None def semantic_action(prod, sem_stack, gen): if prod == ['if', 'E', 'then', 'S', "S'"]: # 展开时先占位,等 S' 归约后再回填 e = sem_stack[-1] j_true = gen.emit("j?", e.place, "_", None) j_false = gen.emit("j", "_", "_", None) e.true_list = [j_true] e.false_list = [j_false] elif prod == ['else', 'S']: s = sem_stack[-1] s.nextlist = gen.merge(s.nextlist, [gen.emit("j", "_", "_", None)]) elif prod == ['ε']: pass

这段代码展示的是语义动作的挂载方式,实际实现里需要根据分析器的归约时机调整。关键点是:条件表达式的place在 E 归约时已经确定,跳转指令在展开 if 产生式时发射,回填在 S' 归约时完成。这样整个 IF-ELSE 的翻译就和 LL(1) 分析过程融为一体,不需要单独建语法树。

4. 避坑与排查:LL(1) 翻译 IF-ELSE 时最容易翻车的 5 个地方

4.1 文法没提取左公因子,预测分析表出现多重入口

现象:构造预测分析表时,M[S, 'if']同时被两条产生式占用,程序报冲突或者随机选一条,导致if E then S else S被错误解析成if E then S后面多出一个else无法处理。

原因:原始文法里两条 if 产生式共享前缀,FIRST 集相同,不满足 LL(1) 条件。

解决:提取左公因子,把S → if E then S else S | if E then S改写成S → if E then S S',S' → else S | ε。改写后重新算 FIRST/FOLLOW,冲突消失。

4.2 回填时目标地址算错,跳转跳到四元式中间

现象:生成的代码执行时跳到了某条四元式的操作数字段上,或者跳转目标指向了不存在的编号。

原因:回填的目标位置用的是「归约时的四元式数量」,但归约发生在语义动作之后,实际下一条指令的编号已经变了。常见错误是把len(quads)当成目标,而正确目标应该是「下一条将要生成的指令编号」。

解决:统一用nextquad语义变量记录下一条指令编号,回填时以它为准。每次emit之后nextquad += 1。回填前打印一遍四元式列表和待填列表,肉眼核对编号。

4.3 嵌套 IF-ELSE 的 nextlist 合并丢失

现象:if a then if b then x=1 else x=2 else x=3这种嵌套结构,内层 else 匹配到了外层 if,或者执行完内层 then 分支后没有跳过外层 else。

原因:S' → ε时没有正确传递 nextlist,导致外层语句的跳出列表为空,回填时漏掉了该跳的指令。

解决:在S' → ε的语义动作里,把S.nextlist合并到外层;在S' → else S里,把 else 分支的 nextlist 和外层合并。嵌套时用栈保存每层的 nextlist,归约一层弹一层。

4.4 条件表达式的 place 在跳转生成时还没确定

现象:生成(j>, _, _, _)时 arg1 是空的,因为 E 还没归约完就急着发射跳转指令。

原因:LL(1) 是自顶向下分析,展开if E then的时候 E 还没处理,E.place自然为空。

解决:把跳转指令的发射推迟到 E 归约之后。常见做法是在S → if E then S S'的 E 归约动作里发射条件跳转,而不是在展开产生式时发射。如果分析器框架不支持这种时机控制,可以先把 E 的 place 存到语义栈,等 S' 归约时再统一发射和回填。

4.5 临时变量命名冲突导致四元式语义错误

现象:嵌套表达式里生成的临时变量重名,后面的赋值覆盖了前面的值。

原因:new_temp计数器在递归调用中没有正确递增,或者多个生成器实例各自维护计数器。

解决:整个翻译程序只用一个全局计数器,或者把计数器挂在生成器实例上并确保所有语义动作共用同一个实例。临时变量名用t1, t2, ...递增,不要用固定名字。

5. 进阶技巧:用四元式解释器验证翻译结果,而不是靠肉眼盯

写完翻译程序之后,最怕的就是四元式序列看起来「差不多对」,但实际执行结果不对。我一般会再写一个几十行的四元式解释器,把生成的代码跑一遍,用实际输出验证翻译逻辑。这个习惯救过我很多次——肉眼检查跳转编号,嵌套一深就翻车。

解释器的核心是一个指令指针pc和一张变量表:

def run_quads(quads, variables=None): if variables is None: variables = {} pc = 0 steps = 0 while pc < len(quads) and steps < 1000: op, a1, a2, res = quads[pc] steps += 1 if op.startswith('j') and op != 'j': # 条件跳转,如 j>, j<, j== rel = op[1:] v1 = variables.get(a1, a1) v2 = variables.get(a2, a2) try: v1, v2 = float(v1), float(v2) except (ValueError, TypeError): pass cond = { '>': v1 > v2, '<': v1 < v2, '>=': v1 >= v2, '<=': v1 <= v2, '==': v1 == v2, '!=': v1 != v2 }.get(rel, False) pc = res if cond else pc + 1 elif op == 'j': pc = res elif op == ':=': variables[res] = variables.get(a1, a1) pc += 1 else: pc += 1 return variables # 验证前面的 if-else 翻译结果 gen = QuadGenerator() quads = gen.gen_if_else( ">", "a", "b", [(":=", "1", "_", "x")], [(":=", "2", "_", "x")] ) print(run_quads(quads, {'a': 5, 'b': 3})) # 期望 x=1 print(run_quads(quads, {'a': 1, 'b': 3})) # 期望 x=2

这个解释器支持条件跳转、无条件跳转和赋值三类指令,足够验证 IF-ELSE 翻译的正确性。steps上限是防止回填错误导致死循环——如果跑满 1000 步还没结束,基本可以断定跳转目标填错了。参数variables是初始变量表,a和b的值决定走哪个分支。

我自己的习惯是:每改一次回填逻辑,先跑三组用例——条件为真、条件为假、嵌套 if-else。三组都过,再去看生成的四元式列表。反过来先盯四元式,很容易被编号绕进去。这套 LL(1) + 四元式 + 回填的组合,写顺了之后,加 while、for、布尔短路运算都是同一套骨架往上叠。希望帮到你。

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

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

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

立即咨询