☰
北京交通大学编译原理期末试卷解析:从词法分析到LR分析表构造
2026/10/3 5:35:46 网站建设 项目流程

简介:本资源为北京交通大学2021—2022学年第二学期《编译原理》期末试卷(A卷)PDF文档,面向计算机专业本科生及考研复习者,用于检验和巩固编译原理核心知识。试卷覆盖文法分析、正则表达式与有限自动机、消除左递归与回溯、算符优先文法、LR(0)与SLR(1)分析、语法制导翻译及四元式序列优化等模块,题型包含语言求解、状态转换图构造、FIRSTVT/LASTVT集合计算、分析表构造、拉链-回填与DAG重构等,能帮助读者系统梳理编译器前端到优化的完整流程。资源包共1个PDF文件,大小约396KB,内容为原版试题,便于打印练习与对照复习。目前已有355人学习下载,适合需要真题演练、查漏补缺或备考冲刺的学习者使用。

1. 一份期末试卷能教你的,远不止考试重点

北京交通大学2021-2022学年第二学期《编译原理》期末试卷(A卷),这份PDF在期末季被反复检索,很多人第一反应是找答案、对题型、押考点。但如果你正在学编译原理,或者准备做编译原理实验、啃清华大学出版社第三版第二章答案,这份卷子的价值其实在另一个方向:它是一张被压缩过的知识地图,告诉你这门课到底在考什么、哪些环节是真正的硬骨头、哪些地方一考就翻车。

编译原理这门课,很多人学完的感觉是“每个字都认识,连起来不知道在说什么”。词法分析、语法分析、语义分析、中间代码、优化、目标代码生成,六大阶段听起来清清楚楚,一到做题就发现:正则表达式转NFA再转DFA,手写递归下降,算FIRST集和FOLLOW集,画LR分析表,每一样都能让人卡住。这份试卷能帮你把这些环节串起来,看清考试和实验之间的对应关系,也能让你在做编译原理实验时少走弯路。

这篇文章不打算给你一份“试卷答案回忆版”,而是顺着这份卷子涉及的知识点,把编译原理从理论到动手的路径拆开讲。适合正在学这门课、准备期末、或者想用Java写一个简单编译器练手的人。如果你只是想知道考了什么题,那可能帮不到你;但如果你想真正搞懂这些题背后的机制,并且能自己动手复现一遍,那往下看。

2. 从试卷题型反推:编译原理到底在考哪几层能力

2.1 期末卷子的典型结构:六类题对应六个知识块

北京交通大学这份A卷,从常见题型分布来看,基本覆盖了编译原理课程的核心模块。虽然我手里没有原卷的逐题内容,但根据同类院校的命题规律和这份卷子的标题信息,可以合理推断它包含以下几类题目:

第一类是概念辨析与简答,比如“编译程序和解释程序的区别”“词法分析和语法分析的分工”。这类题看似送分,实际上是在检验你有没有建立阶段划分的思维框架。第二类是正则表达式与有限自动机,通常要求你写出某个语言的正则定义,然后构造NFA、确定化为DFA、最小化。第三类是文法与语法分析,包括消除左递归、提取左公因子、求FIRST和FOLLOW集、判断LL(1)文法。第四类是LR分析,要求构造LR(0)或SLR(1)项目集规范族,填分析表。第五类是语义分析与中间代码,可能涉及语法制导翻译、三地址码生成。第六类是优化与目标代码,比如基本块划分、流图构造、常见优化技术。

这六类题不是孤立的,它们对应的是编译器流水线上的不同工位。你在卷子上做一道“正则转DFA”的题,实际上是在模拟词法分析器的核心算法;你做一道“构造SLR分析表”的题,实际上是在走语法分析器的自动生成流程。理解这一点,比死记硬背题型重要得多。

2.2 词法分析:正则表达式到DFA的手工推演

词法分析是编译器的第一道关口,任务是把字符流切分成有意义的单词符号。试卷里常见的考法是:给定一个语言描述,要求写出正则表达式,然后构造NFA,再确定化为DFA,最后最小化。这个过程在龙书和清华版教材里都有详细步骤,但手工做起来容易出错。

我一般会按这个顺序推进:先用自然语言把单词模式说清楚,比如“标识符是以字母开头、后跟字母或数字的串”;然后写正则表达式letter (letter | digit)*;接着用Thompson构造法把正则转成NFA;再用子集构造法做确定化;最后用Hopcroft算法或填表法最小化。每一步都有明确的规则,但手工画图时最容易在ε闭包和状态编号上翻车。

下面用Python演示一个最小化的DFA模拟,帮你验证手工结果:

# 一个简单的DFA模拟器,用于验证标识符识别 # 状态0:初始状态;状态1:已读入至少一个字母;状态2:死状态 dfa_transitions = { (0, 'letter'): 1, (0, 'digit'): 2, (1, 'letter'): 1, (1, 'digit'): 1, (1, 'other'): 2, (2, 'letter'): 2, (2, 'digit'): 2, (2, 'other'): 2, } def classify_char(ch): if ch.isalpha(): return 'letter' elif ch.isdigit(): return 'digit' else: return 'other' def run_dfa(input_string): state = 0 for ch in input_string: category = classify_char(ch) state = dfa_transitions.get((state, category), 2) if state == 2: return False # 进入死状态,不是合法标识符 return state == 1 # 必须以字母开头且至少一个字母 # 测试 test_cases = ["abc", "a1b2", "1abc", "abc!", ""] for s in test_cases: print(f"{s!r} -> {run_dfa(s)}")

这段代码的逻辑很直接:状态0是起点,读到字母进入状态1,读到数字进入死状态2;状态1下继续读字母或数字都留在状态1,读到其他字符进入死状态。参数说明:dfa_transitions是状态转移表,键是(当前状态,字符类别),值是下一状态;classify_char把字符映射到类别;run_dfa逐字符驱动状态机。运行结果会告诉你哪些串被接受。手工做题时,你可以用这个思路反向检查自己的DFA是否漏了转移或多了状态。

注意:考试时最小化DFA要求你写出划分过程,不能只给最终状态图。划分的依据是“是否接受状态”和“转移目标是否在同一组”,这两条要写清楚。

2.3 语法分析:FIRST/FOLLOW集与LL(1)判定的手算流程

语法分析是编译原理里最容易拉开差距的部分。LL(1)文法的判定需要算FIRST集、FOLLOW集和SELECT集,然后检查同一非终结符的各产生式SELECT集是否不相交。这个过程步骤固定,但手工算容易漏掉ε产生式的影响。

我通常按这个流程走:第一步,把文法写成产生式集合,标记出终结符和非终结符;第二步,对每个符号求FIRST集,遇到ε要特别处理;第三步,对每个非终结符求FOLLOW集,起始符号的FOLLOW集包含结束符;第四步,对每条产生式求SELECT集;第五步,检查冲突。下面用Python实现一个FIRST/FOLLOW计算器,你可以直接套用到试卷题目上:

# 计算FIRST集和FOLLOW集的简化实现 # 文法示例:E -> T E' | ε; E' -> + T E' | ε; T -> F T' | ε; T' -> * F T' | ε; F -> ( E ) | id grammar = { 'E': [['T', "E'"], ['ε']], "E'": [['+', 'T', "E'"], ['ε']], 'T': [['F', "T'"], ['ε']], "T'": [['*', 'F', "T'"], ['ε']], 'F': [['(', 'E', ')'], ['id']] } non_terminals = set(grammar.keys()) terminals = {'+', '*', '(', ')', 'id', '$'} start_symbol = 'E' def first_of_sequence(seq, first_sets): """计算一个符号串的FIRST集""" result = set() for sym in seq: if sym in terminals: result.add(sym) return result else: result |= (first_sets[sym] - {'ε'}) if 'ε' not in first_sets[sym]: return result result.add('ε') return result # 初始化FIRST集 first_sets = {nt: set() for nt in non_terminals} changed = True while changed: changed = False for nt, productions in grammar.items(): for prod in productions: before = len(first_sets[nt]) first_sets[nt] |= first_of_sequence(prod, first_sets) if len(first_sets[nt]) != before: changed = True # 初始化FOLLOW集 follow_sets = {nt: set() for nt in non_terminals} follow_sets[start_symbol].add('$') changed = True while changed: changed = False for nt, productions in grammar.items(): for prod in productions: for i, sym in enumerate(prod): if sym in non_terminals: rest = prod[i+1:] first_rest = first_of_sequence(rest, first_sets) if rest else {'ε'} before = len(follow_sets[sym]) follow_sets[sym] |= (first_rest - {'ε'}) if 'ε' in first_rest or not rest: follow_sets[sym] |= follow_sets[nt] if len(follow_sets[sym]) != before: changed = True print("FIRST sets:") for nt in sorted(non_terminals): print(f" FIRST({nt}) = {first_sets[nt]}") print("FOLLOW sets:") for nt in sorted(non_terminals): print(f" FOLLOW({nt}) = {follow_sets[nt]}")

这段代码的核心是迭代到不动点。first_of_sequence处理符号串的FIRST集,遇到终结符直接加入并返回,遇到非终结符先加入其FIRST集去掉ε的部分,如果该非终结符不能推出ε就停止,否则继续看下一个符号。FOLLOW集的计算依赖FIRST集,对每条产生式扫描每个非终结符,看它后面跟着的符号串的FIRST集,如果后面能推出ε或者为空,就把左部非终结符的FOLLOW集加进来。参数说明:grammar是文法字典,键是非终结符,值是产生式列表,每个产生式是符号列表;terminals包含所有终结符和结束符$。运行后对照试卷题目,检查你的手算结果是否一致。

提示:SELECT集等于FIRST(α)减去ε,如果α能推出ε,还要加上FOLLOW(A)。判定LL(1)时,同一非终结符的不同产生式SELECT集不能有交集。

3. LR分析表构造:从项目集到分析表的完整走一遍

3.1 LR(0)项目集规范族的构造逻辑

LR分析是自底向上语法分析的核心,也是试卷里分值高、容易丢分的部分。构造LR(0)项目集规范族,本质上是在追踪“当前可能匹配到产生式的哪个位置”。一个项目就是产生式加一个点,点表示已经识别了多少。比如产生式E -> E + T,项目E -> E · + T表示已经识别了左部的E,期待看到加号。

构造过程从增广文法开始,先加一条S' -> S,然后求闭包、求转移。闭包操作是:如果项目A -> α · B β中的点后面是非终结符B,就把B的所有产生式加进来,点在最左边。转移操作是:对某个项目集,看所有点后面的符号,对每个符号求GOTO,即把点移过该符号后求闭包。重复直到没有新项目集产生。

手工做的时候,最容易出错的地方是闭包求不全,或者GOTO时漏了项目。我一般会先把所有产生式编号,然后画一个表格,行是项目集编号,列是各个符号的GOTO目标,这样不容易乱。下面用Python演示一个LR(0)项目集构造的骨架:

# LR(0)项目集规范族构造的简化演示 # 文法:S' -> E; E -> E + T | T; T -> T * F | F; F -> ( E ) | id grammar = { "S'": [['E']], 'E': [['E', '+', 'T'], ['T']], 'T': [['T', '*', 'F'], ['F']], 'F': [['(', 'E', ')'], ['id']] } non_terminals = set(grammar.keys()) terminals = {'+', '*', '(', ')', 'id', '$'} def closure(items): """求项目集的闭包,item是(产生式左部, 产生式右部元组, 点的位置)""" result = set(items) changed = True while changed: changed = False for lhs, rhs, dot in list(result): if dot < len(rhs) and rhs[dot] in non_terminals: for prod in grammar[rhs[dot]]: new_item = (rhs[dot], tuple(prod), 0) if new_item not in result: result.add(new_item) changed = True return frozenset(result) def goto(items, symbol): """求GOTO函数""" moved = set() for lhs, rhs, dot in items: if dot < len(rhs) and rhs[dot] == symbol: moved.add((lhs, rhs, dot + 1)) if not moved: return None return closure(moved) # 初始项目集 start_item = ("S'", ('E',), 0) I0 = closure({start_item}) states = [I0] transitions = {} queue = [I0] while queue: current = queue.pop(0) for sym in terminals | non_terminals: target = goto(current, sym) if target and target not in states: states.append(target) queue.append(target) if target: transitions[(states.index(current), sym)] = states.index(target) print(f"共构造 {len(states)} 个项目集") for i, state in enumerate(states): print(f"I{i}:") for item in sorted(state): lhs, rhs, dot = item rhs_str = ' '.join(rhs[:dot]) + ' · ' + ' '.join(rhs[dot:]) print(f" {lhs} -> {rhs_str}")

这段代码的逻辑是:closure不断把点后面是非终结符的项目展开,直到不再新增;goto把点移过指定符号后求闭包;主循环用队列遍历所有可达项目集,记录状态编号和转移。参数说明:grammar是增广后的文法,start_item是增广产生式的初始项目。运行后你会得到所有项目集和它们之间的转移关系,这就是画DFA和填分析表的基础。

3.2 SLR(1)分析表的填写与冲突处理

有了LR(0)项目集规范族,接下来就是填ACTION表和GOTO表。SLR(1)在LR(0)的基础上,用FOLLOW集来解决归约-归约冲突和移进-归约冲突。具体规则是:如果项目A -> α ·在状态I中,且a在FOLLOW(A)中,那么ACTION[I, a]填归约A -> α;如果项目A -> α · a β在状态I中,且a是终结符,那么ACTION[I, a]填移进到GOTO(I, a);如果项目S' -> S ·在状态I中,ACTION[I, $]填接受。

冲突处理是考试的重点。移进-归约冲突时,SLR(1)看移进符号是否在归约项目的FOLLOW集中,如果在就冲突,不在就按移进处理。归约-归约冲突时,看两个归约项目的FOLLOW集是否有交集,有交集就冲突。如果SLR(1)解决不了,就要用LR(1)或LALR(1)。

下面用表格展示一个SLR(1)分析表的片段,方便你对照:

状态id+*()$ETF
0s5s4123
1s6acc
2r2s7r2r2
3r4r4r4r4
4s5s4823
5r6r6r6r6
6s5s493
7s5s410
8s6s11
9r1s7r1r1
10r3r3r3r3
11r5r5r5r5

表中s表示移进并转到对应状态,r表示按第几条产生式归约,acc表示接受。这个表是SLR(1)分析表的典型形式,你可以用它来模拟分析过程。比如输入串id + id * id,从状态0开始,读到id移进到状态5,然后按F->id归约到状态3,再按T->F归约到状态2,再按E->T归约到状态1,读到+移进到状态6,继续处理后面的部分。每一步都查表,直到接受或报错。

注意:填表时归约项目的FOLLOW集一定要算对,否则会把不该归约的符号填成归约,导致分析表冲突或分析错误。这是血泪经验,很多人在这里翻车。

4. 语义分析与中间代码:从语法树到三地址码

4.1 语法制导翻译的基本框架

语义分析阶段,编译器要检查程序的意义是否合法,并生成中间表示。试卷里常见的考法是:给一个简单的赋值语句或表达式文法,要求写出语法制导定义,然后给出对应的三地址码。语法制导翻译的核心思想是:为每个产生式关联语义规则,在语法分析过程中计算属性值。

属性分两种:综合属性从子节点传到父节点,继承属性从父节点或兄弟节点传到子节点。对于表达式求值,通常用综合属性就够了。比如产生式E -> E1 + T,语义规则可以是E.code = E1.code || T.code || gen('+', E1.place, T.place, E.place),其中||表示代码拼接,gen生成一条三地址指令。

我一般会先画出语法树,然后自底向上计算每个节点的place和code。place表示存放结果的临时变量或变量名,code是生成的指令序列。下面用Python演示一个简单的三地址码生成器:

# 简单表达式的三地址码生成 # 文法:E -> E + T | T; T -> T * F | F; F -> ( E ) | id class TACGenerator: def __init__(self): self.temp_count = 0 self.code = [] def new_temp(self): self.temp_count += 1 return f"t{self.temp_count}" def gen(self, op, arg1, arg2=None): result = self.new_temp() if arg2: self.code.append(f"{result} = {arg1} {op} {arg2}") else: self.code.append(f"{result} = {op} {arg1}") return result # 模拟解析过程,这里直接按表达式树后序遍历 def generate_tac(node, gen): """node是元组:(操作符, 左子节点, 右子节点) 或 标识符""" if isinstance(node, str): return node # 标识符直接返回名字 op, left, right = node left_place = generate_tac(left, gen) right_place = generate_tac(right, gen) return gen.gen(op, left_place, right_place) # 测试表达式:a + b * c expr_tree = ('+', 'a', ('*', 'b', 'c')) gen = TACGenerator() result = generate_tac(expr_tree, gen) print("三地址码:") for line in gen.code: print(f" {line}") print(f"最终结果存放在:{result}")

这段代码的逻辑是:对表达式树做后序遍历,先处理左右子树得到它们的place,然后为当前操作生成一条三地址指令并返回新的临时变量。参数说明:TACGenerator维护临时变量计数器和代码列表;gen方法根据操作符和参数生成指令;generate_tac递归处理树节点。运行结果会输出t1 = b * c和t2 = a + t1,最终结果在t2中。考试时,你需要根据语法制导定义写出类似的语义规则,并手动模拟生成过程。

4.2 控制流语句的翻译与回填技术

除了表达式,试卷还可能考控制流语句的翻译,比如if-else和while。这类语句的翻译难点在于跳转指令的目标地址在生成时还不确定,需要用回填技术。基本思路是:先生成跳转指令,但目标地址留空,把这些指令放入一个列表;等到目标地址确定后,再回填到指令中。

以if E then S1 else S2为例,翻译过程是:生成E的代码,E的结果放在临时变量中;生成条件跳转指令,如果E为假跳到S2的开始;生成S1的代码;生成无条件跳转指令,跳到整个if语句的结束;确定S2的开始地址,回填前面的条件跳转;生成S2的代码;确定结束地址,回填无条件跳转。

回填技术的关键是维护两个列表:truelist和falselist,分别记录需要回填为真出口和假出口的跳转指令。在语法分析过程中,这些列表随着归约操作合并和传递。考试时,通常会要求你写出布尔表达式的翻译方案,并给出回填后的代码序列。

提示:回填的顺序很重要,先确定哪个地址就先回填哪个列表。如果顺序搞反,跳转目标会指向错误的指令。这是编译原理实验里最常见的bug之一。

5. 避坑与排查:编译原理学习和实验中的五个典型翻车现场

5.1 正则转DFA时ε闭包漏状态

现象:手工构造的DFA在模拟时,某些应该接受的串被拒绝,或者应该拒绝的串被接受。原因:在子集构造法中,ε闭包没有求完整,漏掉了通过ε转移可达的状态。解决:每次求闭包时,反复扫描直到没有新状态加入;可以用一个栈来管理待处理状态,确保每个状态都被展开。检查时,把每个项目集的状态编号列出来,对照NFA的ε转移逐条验证。

5.2 FIRST/FOLLOW集计算时忽略ε产生式

现象:LL(1)判定时,SELECT集算错,导致明明无冲突的文法被判为有冲突,或者有冲突的漏判。原因:FIRST集计算时,遇到非终结符能推出ε的情况没有继续看后面的符号;FOLLOW集计算时,没有把左部的FOLLOW集传给右部末尾的非终结符。解决:严格按照定义迭代到不动点,每次更新后检查是否有变化;对于含ε的产生式,单独标记并特殊处理。可以用前面给的Python代码交叉验证手算结果。

5.3 LR分析表填错归约项目的FOLLOW集

现象:SLR(1)分析表在某个状态对某个输入符号填了归约,但实际分析时应该移进,导致分析动作冲突或错误归约。原因:归约项目的FOLLOW集算错,把不该归约的符号包含了进来。解决:重新计算FOLLOW集,特别注意起始符号的FOLLOW集包含结束符$;对于每个归约项目,只在其左部非终结符的FOLLOW集对应的列填归约。填完后用几个典型输入串模拟一遍,看是否有冲突。

5.4 三地址码生成时临时变量命名冲突

现象:生成的中间代码中,两个不同的临时变量用了同一个名字,导致后续优化或目标代码生成时结果错误。原因:临时变量计数器没有正确递增,或者在递归生成时作用域混乱。解决:用一个全局计数器,每次生成新临时变量时递增;确保递归调用中传递的是同一个生成器实例。如果用手写方式,给临时变量加前缀区分不同表达式层级。

5.5 回填时跳转目标地址错位

现象:控制流语句翻译后,跳转指令的目标地址指向了错误的指令,程序执行流程混乱。原因:回填列表合并时顺序错误,或者目标地址确定后没有及时回填所有相关指令。解决:维护清晰的回填列表,每次确定一个地址就立即回填对应的列表;在合并列表时,保持顺序一致。调试时,把生成的代码打印出来,逐条检查跳转目标是否指向正确的标号。

6. 用Java写一个微型编译器:把试卷知识点串成可运行代码

如果你已经看完了前面的理论部分,现在想动手验证一遍,我建议用Java写一个微型编译器,处理简单的算术表达式和赋值语句。这个练习能把词法分析、语法分析、语义分析和中间代码生成串起来,比单独做每道题更有收获。

先定义词法分析器。输入是字符串,输出是Token序列。Token类型包括标识符、数字、运算符、括号和结束符。用有限自动机的手写方式实现,每个字符读入后根据当前状态决定下一步。核心代码如下:

// 简化的词法分析器 import java.util.*; public class Lexer { private String input; private int pos = 0; private List<Token> tokens = new ArrayList<>(); public Lexer(String input) { this.input = input; } public List<Token> tokenize() { while (pos < input.length()) { char ch = input.charAt(pos); if (Character.isWhitespace(ch)) { pos++; } else if (Character.isLetter(ch)) { StringBuilder sb = new StringBuilder(); while (pos < input.length() && (Character.isLetterOrDigit(input.charAt(pos)))) { sb.append(input.charAt(pos++)); } tokens.add(new Token(TokenType.ID, sb.toString())); } else if (Character.isDigit(ch)) { StringBuilder sb = new StringBuilder(); while (pos < input.length() && Character.isDigit(input.charAt(pos))) { sb.append(input.charAt(pos++)); } tokens.add(new Token(TokenType.NUM, sb.toString())); } else { switch (ch) { case '+': tokens.add(new Token(TokenType.PLUS, "+")); break; case '*': tokens.add(new Token(TokenType.MUL, "*")); break; case '(': tokens.add(new Token(TokenType.LPAREN, "(")); break; case ')': tokens.add(new Token(TokenType.RPAREN, ")")); break; case '=': tokens.add(new Token(TokenType.ASSIGN, "=")); break; default: throw new RuntimeException("非法字符: " + ch); } pos++; } } tokens.add(new Token(TokenType.EOF, "$")); return tokens; } }

这段代码的逻辑是:逐字符扫描,遇到空白跳过,遇到字母开始收集标识符,遇到数字收集数字,遇到运算符生成对应Token。参数说明:input是源字符串,pos是当前扫描位置,tokens是结果列表。Token类需要定义类型枚举和值。这个词法分析器对应试卷里正则转DFA的知识点,只是用代码实现了状态转移。

接下来是语法分析器,用递归下降法处理表达式。文法可以写成:E -> T E',E' -> + T E' | ε,T -> F T',T' -> * F T' | ε,F -> ( E ) | id | num。每个非终结符对应一个方法,方法内部根据当前Token决定走哪条产生式。递归下降的优点是直观,缺点是遇到左递归要改写文法。代码如下:

// 递归下降语法分析器,同时生成三地址码 public class Parser { private List<Token> tokens; private int pos = 0; private int tempCount = 0; private List<String> code = new ArrayList<>(); public Parser(List<Token> tokens) { this.tokens = tokens; } private Token peek() { return tokens.get(pos); } private Token consume() { return tokens.get(pos++); } private String newTemp() { return "t" + (++tempCount); } public String parseE() { String place = parseT(); while (peek().type == TokenType.PLUS) { consume(); String right = parseT(); String temp = newTemp(); code.add(temp + " = " + place + " + " + right); place = temp; } return place; } private String parseT() { String place = parseF(); while (peek().type == TokenType.MUL) { consume(); String right = parseF(); String temp = newTemp(); code.add(temp + " = " + place + " * " + right); place = temp; } return place; } private String parseF() { Token t = peek(); if (t.type == TokenType.LPAREN) { consume(); String place = parseE(); if (peek().type != TokenType.RPAREN) { throw new RuntimeException("缺少右括号"); } consume(); return place; } else if (t.type == TokenType.ID || t.type == TokenType.NUM) { consume(); return t.value; } else { throw new RuntimeException("意外的Token: " + t.value); } } public List<String> getCode() { return code; } }

这段代码的逻辑是:parseE处理加减,parseT处理乘除,parseF处理括号和原子。每遇到一个运算符,就生成一条三地址指令,用临时变量保存结果。参数说明:tokens是词法分析器的输出,pos是当前Token位置,tempCount是临时变量计数器,code是生成的指令列表。这个语法分析器对应试卷里LL(1)文法和递归下降的知识点,同时完成了语义分析和中间代码生成。

把词法分析器和语法分析器串起来,主程序如下:

public class MiniCompiler { public static void main(String[] args) { String source = "a + b * (c + d)"; Lexer lexer = new Lexer(source); List<Token> tokens = lexer.tokenize(); Parser parser = new Parser(tokens); String result = parser.parseE(); System.out.println("三地址码:"); for (String line : parser.getCode()) { System.out.println(" " + line); } System.out.println("最终结果:" + result); } }

运行这个程序,输入a + b * (c + d),你会得到类似t1 = c + d、t2 = b * t1、t3 = a + t2的输出。这个过程完整复现了从源程序到中间代码的编译流程,也把试卷里的词法、语法、语义知识点串了起来。

我自己的习惯是,每学完一个编译阶段,就用Java写一个最小实现,哪怕只能处理最简单的文法。写多了你会发现,那些考试题不再是抽象符号,而是代码里一个个具体的状态转移和递归调用。编译原理实验里常见的坑,比如ε闭包漏状态、FOLLOW集算错、回填地址错位,在动手写一遍之后都会变得具体。希望帮到你。

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

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

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

立即咨询