前言
本提纲涵盖所有考点,包含多套真题,复习绝对高效,持续更新。由于md格式文件解析问题,CSDN上显示的内容可能与pdf文件有所出入,点赞自取pdf版本。
本提纲每半年更新一次,包括最近一次考试的回忆、删除无用不考的知识、新增易考的考点、优化文章排版布局、对一些知识点进行更详细的解释等。目前已有多个版本,最近一次更新于2023年12月12日。
实验地址为:https://github.com/roomdestroyer/PL0Compiler,python编写,写了非常详细的README和代码注释,帮助你搞懂这个实验。
有任何修改建议请发送邮件到 1761806916@qq.com
把本文涉及到的知识点全部搞清楚,应付考试可游刃有余。后续同学们考完试,可以把回忆版私发给我,我在此文中持续更新。
山东大学编译原理复习提纲
编译原理总结
一、简答与计算
1.1 必考
1. 编译过程
- 画图表示编译过程的各阶段,并简要说明各阶段的功能:
- 词法分析器:输入源程序,进行词法分析,输出单词符号;
- 语法分析器:根据文法构建分析表,对单词符号进行语法分析,检查程序是否符合语法规则;
- 语义分析与中间代码生成器:按照文法翻译规则对语法分析器归约出的语法单位进行语义分析,并把它们翻译成一定形式的中间代码;
- 优化器:对中间代码进行优化处理;
- 目标代码生成器:把中间代码翻译成目标代码。
2. 消除左递归
-
对于左递归文法 P → P α ∣ β P\to P \alpha \,| \, \beta P→Pα∣β(其中 $$ β \beta β 的第一个符号不是 P P P),可以直接利用下列规则将其转为右递归文法:
$$
P \to \beta , P{‘} \
P’ \to \alpha , P’ , | , \epsilon$$
-
同样地,如果 P P P 对应多个产生式,例如: P → P α 1 ∣ P α 2 ∣ . . . ∣ P α n ∣ β 1 ∣ β 2 ∣ . . . ∣ β m P \to P \alpha_1 \, | \, P \alpha_2 \, | \, ... \, | \, P\alpha_n \, | \, \beta_1 \, | \, \beta_2 \, | \, ... \, | \, \beta_m P→Pα1∣Pα2∣...∣Pαn∣β1∣β2∣...∣βm(其中 β i \beta_i βi 的第一个符号不是 P P P),可以首先利用结合律将其转为一个产生式:
P → P ( α 1 ∣ . . . ∣ α n ) ∣ ( β 1 ∣ . . . ∣ β m ) P \to P( \alpha_1\, |\, ... \, |\,\alpha_n)\,|\,(\beta_1 \,|\, ... \,|\,\beta_m) P→P(α1∣...∣αn)∣(β1∣...∣βm)
然后利用上面的规则将其转为右递归文法:
P → ( β 1 ∣ . . . ∣ β m ) P ′ P ′ → ( α 1 ∣ . . . ∣ α n ) P ′ ∣ ϵ P \to (\beta_1 \,|\, ... \,|\,\beta_m)P' \\ P' \to ( \alpha_1\, |\, ... \, |\,\alpha_n) P' \, | \, \epsilon P→(β1∣...∣βm)P′P′→(α1∣...∣αn)P′∣ϵ
-
有些文法会有隐式的左递归,例如:
S → Q c ∣ c Q → R b ∣ b R → S a ∣ a S \to Qc \, | \, c \\ Q \to Rb \, | \, b \\ R \to Sa \, | \, a S→Qc∣cQ→Rb∣bR→Sa∣a
其中一种隐式的左递归为:
S → Q c → R b c → S a b c S \to Qc \to Rbc \to Sabc S→Qc→Rbc→Sabc
消除隐式左递归的步骤为:
-
将非终结符进行排序(不同的顺序会有不同的结果):S, Q, R
-
根据顺序重构产生式,确保每个非终结符所推出的产生式体中,不能包含其排序之前的非终结符(例如 R 推出的产生式不能包含 S 和 Q,若有则按顺序逐步推导,直到不包含 S 和 Q):
S → Q c ∣ c Q → R b ∣ b R → S a ∣ a → Q c a ∣ c a ∣ a → R b c a ∣ b c a ∣ c a ∣ a S \to Qc \, | \, c \\ Q \to Rb \, | \, b \\ R \to Sa \, | \, a \to Qca \, | \, ca \, | \, a \to Rbca \, | \, bca \, | \, ca \, | \, a S→Qc∣cQ→Rb∣bR→Sa∣a→Qca∣ca∣a→Rbca∣bca∣ca∣a
-
对重构后的产生式应用上面的规则,最终的产生式为:
S → Q c ∣ c Q → R b ∣ b R → b c a R ′ ∣ c a R ′ ∣ a R ′ R ′ → b c a R ′ ∣ ϵ S \to Qc \, | \, c \\ Q \to Rb \, | \, b \\ R \to bcaR' \, | \, caR' \, | \, aR' \\ R' \to bcaR' \, | \, \epsilon S→Qc∣cQ→Rb∣bR→bcaR′∣caR′∣aR′R′→bcaR′∣ϵ
-
3. 消除回溯
在构造文法时,消除回溯可以提高编译器在解析代码时的效率和准确性,避免在遇到解析歧义时重复尝试多个解析路径,从而减少资源消耗和提高错误定位的准确性。
如下是一个简单的有回溯文法:
A → a b A → a c A \to ab \\ A \to ac A→abA→ac
在不消除回溯的解析器中,如果输入是 “ac”,解析器首先尝试使用产生式 1。它匹配了 ‘a’,但在尝试匹配 ‘b’ 时失败,因为下一个字符是 ‘c’。于是,解析器回溯到选择点,放弃已经做出的选择,然后尝试产生式 2。这次它成功匹配了 ‘a’ 和 ‘c’。
这个过程中,解析器不得不回到选择点并重新尝试,这在复杂的语法结构中会导致效率低下。如果我们通过重构产生式来消除回溯,比如改为:
A → a B B → b B → c A \to aB \\ B \to b \\ B \to c A→aBB→bB→c
这样,解析器首先匹配 ‘a’,然后根据接下来的字符是 ‘b’ 还是 ‘c’ 来决定是使用产生式 2 还是 3。这种方法避免了回溯,因为每个步骤的选择都是基于当前和后续的输入明确的,从而提高了解析效率。
一个无回溯文法 A → α 1 ∣ α 2 ∣ . . . ∣ α n A \to \alpha_1 \, | \, \alpha_2 \, | \, ... \, | \, \alpha_n \, A→α1∣α2∣...∣αn 的每对候选式应该具有以下条件:
F i r s t ( α i ) ∩ F i r s t ( α j ) = ϕ ( i ≠ j ) First(\alpha_i)\, \cap \, First(\alpha_j) \,=\, \phi \,\,(i \,\neq \,j) First(αi)∩First(αj)=ϕ(i=j)
解决回溯的办法是反复提左公因子,例如对于如下的有回溯文法:
P → α A 1 ∣ α A 2 ∣ . . . ∣ α A n ∣ β 1 ∣ β 2 ∣ . . . ∣ β m P \to \alpha A_1 \, | \, \alpha A_2 \, | \, ... \, | \, \alpha A_n \, | \, \beta_1 \, | \, \beta_2 \, | \, ... \, | \, \beta_m P→αA1∣αA2∣...∣αAn∣β1∣β2∣...∣βm
通过提左公因子 α \alpha α 可以得到下列消除回溯的文法:
P → α A ∣ β 1 ∣ β 2 ∣ . . . ∣ β m A → A 1 ∣ A 2 ∣ . . . ∣ A n P \to \alpha A \, | \, \beta_1 \, | \, \beta_2 \, | \, ... \, | \, \beta_m \\ A \to \, A_1 \, | \, A_2 \, | \, ... \, | \, A_n P→αA∣β1∣β2∣...∣βmA→A1∣A2∣...∣An
4. 后缀表达式
自然语言描述数学表达式使用的是中缀表达式,计算机执行数学运算需要使用后缀表达式,在编译器处理数学表达式时自然要考虑表达式的转换。此处有可能出现一个小题,给你一个中缀形式的数学表达式,要求你转换为后缀表达式。首先介绍一下这三种表达式:
- 中缀表达式:这是最常见的表达式形式,其操作符位于操作数之间,例如表达式 A + B A+B A+B。中缀表达式的主要特点是直观易懂,但在计算时可能需要括号来明确操作的优先级。
- 后缀表达式(逆波兰表示法):在这种表达式中,操作符位于操作数之后。例如,中缀表达式 A + B A+B A+B 在后缀表达式中可以表示为 A B + AB+ AB+。后缀表达式的优点是不需要括号来指定运算优先顺序,计算机执行起来更为直接和高效。
- 前缀表达式(波兰表示法):在这种表达式中,操作符位于操作数之前。例如,中缀表达式 A + B A+B A+B 在前缀表达式中表示为 + A B +AB +AB。前缀表达式也不需要括号来指定运算的优先级,便于计算机解析和计算。
举个例子,对于一个简单的表达式 3 × ( 3 + 5 ÷ ( 2 × 2 ) ) 3\times (3+5\div(2\times2)) 3×(3+5÷(2×2)),其三种不同的表达方式如下:
- 中缀表达式:原始表达式就是中缀表达式,即 3 × ( 3 + 5 ÷ ( 2 × 2 ) ) 3\times (3+5\div(2\times2)) 3×(3+5÷(2×2))。
- 后缀表达式:在后缀表达式中,操作符位于操作数之后。转换步骤如下:
- 2 × 2 2\times2 2×2 转换为 2 2 × 2\,2\,\times 22×
- 5 ÷ ( 2 × 2 ) 5\div(2\times2) 5÷(2×2) 转换为 5 2 2 × ÷ 5\,2\, 2\,\times \div 522×÷
- 3 + 5 ÷ ( 2 × 2 ) 3+5\div(2\times2) 3+5÷(2×2) 转换为 3 5 2 2 × ÷ + 3\,5\,2\,2\,\times\,\div\,+ 3522×÷+
- 最后整个表达式转换为 3 3 5 2 2 × ÷ + × 3\,3\,5\,2\,2\,\times\,\div\,+\,\times 33522×÷+×
- 前缀表达式:在前缀表达式中,操作符位于操作数之前。转换步骤如下:
- 2 × 2 2\times2 2×2 转换为 × 2 2 \times\,2\,2 ×22
- 5 ÷ ( 2 × 2 ) 5\div(2\times2) 5÷(2×2) 转换为 ÷ 5 × 2 2 \div\,5\times\,2\, 2 ÷5×22
- 3 + 5 ÷ ( 2 × 2 ) 3+5\div(2\times2) 3+5÷(2×2) 转换为 + 3 ÷ 5 × 2 2 +\,3\div\,5\times\,2\, 2 +3÷5×22
- 最后整个表达式转换为 × 3 + 3 ÷ 5 × 2 2 \times\,3\,+\,3\div\,5\times\,2\, 2 ×3+3÷5×22
中缀转后缀算法:
-
在算法开始时,你需要初始化一个符号栈,然后顺序读取中缀表达式,根据读取到的不同符号来执行相应的策略:
- ( ( ( 将其入栈;
- ) ) ) 依次弹出栈顶的元素并附加到结果字符串中,直到遇到下一个 ( ( ( 出现在栈顶,将该 ( ( ( 出栈但不附加到结果字符串中;
- + − +- +− 弹出栈顶的 + − ∗ / +-*/ +−∗/ 并附加到结果字符串中,然后自己进栈;
- ∗ / */ ∗/ 先让栈顶的 ∗ / */ ∗/ 出栈,附加到结果字符串中,然后自己进栈;
- 操作数 直接附加到结果字符串中;
最后,当输入串被扫面完毕,但符号中还有操作符时,将符号栈栈顶元素依次附加到结果字符串中。下面是一个 C++ 实现的函数:
// 中缀转后缀 vector<string> interToPost(vector<string> vec) { vector<string> res; stack<string> op_stack; for (int i = 0; i < vec.size(); i++) { if (vec[i] == "(") op_stack.push(vec[i]); else if (vec[i] == ")") { while (op_stack.top() != "(") { res.push_back(op_stack.top()); op_stack.pop(); } // 左括号出栈 op_stack.pop(); } // 遇到+-,先让栈中的+-*/出栈 else if (vec[i] == "+" || vec[i] == "-") { while (!op_stack.empty() && op_stack.top() != "(") { res.push_back(op_stack.top()); op_stack.pop(); } op_stack.push(vec[i]); } // 遇到*/,先让栈中的*/出栈 else if (vec[i] == "*" || vec[i] == "/") { while (!op_stack.empty() && (op_stack.top() == "*" || op_stack.top() == "/")) { res.push_back(op_stack.top()); op_stack.pop(); } op_stack.push(vec[i]); } else res.push_back(vec[i]); } while (!op_stack.empty()) { res.push_back(op_stack.top()); op_stack.pop(); } return res; }
计算后缀表达式:
-
计算后缀表达式的过程相对直观,主要是通过一个数据栈来实现。下面是计算后缀表达式的步骤:
- 创建一个空栈:这个栈将用于暂存操作数。
- 从左到右扫描后缀表达式:逐个检查后缀表达式中的元素。
- 处理遇到的元素:
- 当遇到一个操作数(数字)时,将其压入栈中。
- 当遇到一个操作符(比如 + − ∗ / +-*/ +−∗/ )时,从栈中弹出所需数量的操作数(对于大多数二元操作符,需要弹出两个操作数)。接着,使用这些操作数执行相应的运算(比如加法、减法、乘法、除法),需要注意要将栈上的第二个元素作为左操作符,栈顶元素作为右操作符。
- 将运算结果压回栈中:计算完操作符指定的运算后,将结果压入栈中。
- 重复以上步骤:继续处理表达式中的下一个元素,直到整个表达式被完全处理完毕。
- 得到最终结果:当表达式结束时,栈顶的元素就是整个后缀表达式的计算结果。
下面是一个 C++ 实现的函数:
// 计算后缀表达式 double solvePostPrefix(vector<string> post_prefix) { stack<double> dataStack; double num1, num2, result; for (auto x : post_prefix) { if (x == "+" || x == "-" || x == "*" || x == "/") { num1 = dataStack.top(); dataStack.pop(); num2 = dataStack.top(); dataStack.pop(); if (x == "+") result = num2 + num1; if (x == "-") result = num2 - num1; if (x == "*") result = num2 * num1; if (x == "/") result = num2 / num1; dataStack.push(result); } else { double num = strToDouble(x); dataStack.push(num); } } return dataStack.top(); }
1.2 选考
1. 编译、翻译和解释
- 翻译:把一种语言程序(源语言)转换成另一种语言程序(目标语言),例如将Python代码翻译为Java代码。这种转换使得原本只能在特定语言环境中运行的程序能够在其他环境或平台上运行。
- 编译:编译程序是翻译程序的一种特殊形式,它专门将高级编程语言(如C++或Java)编写的源代码转换为低级语言(如汇编语言或机器代码),从而可以在计算机硬件上直接运行。例如,GCC(GNU Compiler Collection)可以将C语言代码编译成适用于不同操作系统的机器代码。
- 解释:解释程序接受用源语言编写的代码作为输入,但不生成独立的目标程序,而是在程序运行时实时翻译和执行源代码。这种方式允许更快的开发迭代,但可能会牺牲运行速度。例如,Python解释器可以直接执行Python代码,而无需先将其编译成机器代码。
2. 中间代码
中间代码是一种清晰且易于操作的符号系统,通常与具体的硬件无关,但在一定程度上接近于指令格式,或可以较为轻松地转换成机器指令。它在编程语言的编译过程中扮演着桥梁的角色,平衡了源代码的高级特性和目标代码的低级细节。中间代码的常见形式包括:
- **三元式:**一种紧凑的表示方法,用于描述运算符和其操作数,例如表达式 a = b + c a = b+c a=b+c 的三元式表示为 ( + , b , c ) (+,b,c) (+,b,c)。
- **间接三元式:**类似于三元式,但增加了间接性,以支持更复杂的编译器优化技术。假设上述表达式在三元式序列中的位置是 4 4 4,那么其间接三元式表示为 ( 4 ) (4) (4)。
- **四元式:**提供了更详细的操作描述,每个元素代表一个操作码或操作数。同样,上述表达式的四元式表示为 ( + , b , c , a ) (+,b,c,a) (+,b,c,a),四元式在三元式的基础上增加了一个字段来明确指出结果的存储位置(此处为 a a a )。
- **逆波兰式:**一种无需括号即可表示运算顺序的方法,利于快速解析和执行。例如上述表达式的逆波兰式为 b c + b\,c\,+ bc+。在逆波兰表示法中,操作符跟在操作数之后,这种格式消除了对括号的需求,使得表达式的计算更加直接
- **树形表示:**以树的形式展示程序结构,便于进行结构化的分析和优化。上述表达式的树形表示为构建一个节点为 + + + 的树,其左右子节点分别是 b b b 和 c c c,这种表示法以树的形式体现了表达式的结构,使得对程序的结构化分析和优化变得更加直观。
3. 目标代码
将中间代码转换为特定机器上的低级语言代码,并生成能充分利用硬件性能的目标代码,是一个颇具挑战的任务。这一过程涉及到多种目标代码的形式,包括:
- **汇编指令代码:**这类代码需要通过汇编程序转换才能执行。它是一种更接近机器语言的代码形式,提供了对硬件的细致控制,但同时要求更多的手动管理。
- **绝对指令代码:**这种代码可以直接在机器上执行,不需要任何额外的转换步骤。绝对指令代码直接映射到机器的内存地址,因此效率很高,但它缺乏灵活性,因为代码一旦编写,其运行位置就固定了。
- **可重定位指令代码:**这类代码在运行前需要借助链接装配程序。链接装配程序的任务是将各个目标模块(包括系统提供的库模块)连接在一起,并确定程序在内存中的起始地址。这一过程使得各个模块能够形成一个完整的、可运行的绝对指令代码程序,提供了更高的灵活性和模块化能力。
4. 属性文法
属性文法是一种用于描述语言语法及其语义属性的强大工具,主要分为两种类型:
-
**S-属性文法:**这种文法仅包含综合属性。综合属性是那些从语法树的子节点计算并传递到父节点的属性,它们通常用于构建自底向上的解析过程。在S-属性文法中,每个语法结构的语义由其组成部分的语义直接决定,没有外部依赖。
假设我们有一个简单的算术表达式文法,用于处理加法和乘法,如 3 + 2 ∗ 4 3 + 2 * 4 3+2∗4。在S-属性文法中,我们可能有如下的规则:
T → T + T T → T ∗ T T → i n t T\to T + T \\ T\to T * T \\ T\to int T→T+TT→T∗TT→int
在这种情况下,每个表达式的值(综合属性)可以由其子表达式的值计算得出。例如 T → T + T T \to T + T T→T+T 的值是两个子表达式值的总和。
-
L**-属性文法:**这类文法既包含综合属性,也包含继承属性。不同于综合属性,继承属性是从父节点或相邻兄弟节点传递到当前节点的属性。在L-属性文法中,一个节点的继承属性可能依赖于:
- 产生式体中该符号左侧的属性:这些属性可以是其他节点的综合属性或继承属性。;
- 产生式头部的继承属性:这意味着子节点的属性可以受到父节点属性的影响;
考虑一个用于处理变量声明和赋值的语法。在这种情况下,变量的类型(继承属性)可能需要从声明传递到使用的地方。考虑一个用于标记表达式中每个数字的深度的例子。这里的“深度”是指数字在语法树中的层级,根节点的深度为0,每向下一层深度增加1。在这个例子中,我们只使用继承属性来传递深度信息,不计算表达式的值,也不使用综合属性。假设我们的文法如下:
E → E + T E → T T → T ∗ F T → F F → ( E ) F → n u m b e r E\to E+T\\E\to T\\ T\to T*F\\T\to F\\F\to (E)\\F\to number E→E+TE→TT→T∗FT→FF→(E)F→number
在这个文法中, E E E、 T T T 和 F F F 分别代表表达式、项和因子。我们定义一个继承属性 d e p t h depth depth 来表示当前节点的深度。处理过程如下:
- 在解析开始时,最顶层的 E E E(即整个表达式)的 d e p t h depth depth 设置为0。
- 对于每个规则,当我们向下移动到子节点时(如 E → E + T E\to E + T E→E+T 中的第二个 E E E 或 T T T), d e p t h depth depth 增加1。
- 当遇到数字时(即 F → n u m b e r F\to number F→number),我们记录该数字的 d e p t h depth depth。
例如,对于表达式 ( 3 + ( 4 ∗ 5 ) ) (3 + (4 * 5)) (3+(4∗5)):
- 最外层的表达式 ( 3 + ( 4 ∗ 5 ) ) (3 + (4 * 5)) (3+(4∗5)) 的 d e p t h depth depth 是 0。
- 数字 3 3 3 直接位于这个表达式中,因此它的 d e p t h depth depth 也是 0。
- 对于内层表达式 ( 4 ∗ 5 ) (4 * 5) (4∗5),其 d e p t h depth depth 为 1。
- 因此,数字 4 4 4 和 5 5 5 的 d e p t h depth depth 都是 2,因为它们位于括号内的子表达式中。
通过这种方式,我们可以使用继承属性来追踪每个数字在表达式中的深度,而不需要任何综合属性。
总得来说,L-属性文法提供了更大的灵活性,允许属性值在语法树中更广泛地传递,但同时也增加了设计和实现的复杂性。通过使用这两种属性文法,可以更精确地定义和解析程序语言的语法结构及其相关语义。
5. 高级语言的分类
-
**强制式语言:**这种语言注重底层细节和具体的操作指令。在强制式语言中,程序由一系列命令组成,每条命令具体指示计算机改变某些存储单元中的值。这种语言通常更关注如何执行操作,而不仅仅是要执行什么操作。
例如:C语言是一种典型的强制式语言。在C语言中,程序员编写一系列具体的命令来告诉计算机如何操作。比如,使用循环和分支语句来控制程序流程,直接对内存进行读写等。C语言允许程序员以非常细粒度的方式控制程序的每一个方面。
-
**应用式语言:**相比于强制式语言的操作细节,应用式语言更关注于程序的功能和目标。在这类语言中,每条语句都表达了一个较高层次的操作或结果,而不是具体的执行步骤。应用式语言的语句通常封装了更复杂的功能和逻辑。
例如:SQL专注于数据的查询和操作,而不是具体的操作步骤。当使用SQL时,程序员描述他们想要查询或修改什么数据,而不需要指定如何进行这些操作。例如,一个SQL查询可以非常简洁地表达对数据库的复杂查询请求。
-
**基于规则的语言:**这类语言基于一套特定的规则来执行程序。程序运行时会检查一定的条件,当这些条件满足特定值时,就会触发相应的动作或规则。基于规则的语言常用于专家系统和逻辑编程。
例如:Prolog是一种基于规则的逻辑编程语言,它使用事实和规则来表达逻辑。在Prolog中,程序是一系列的规则,形式上类似于“当满足这些条件时,则执行这些动作”。Prolog广泛用于人工智能和计算机语言理解。
-
**面向对象的语言:**面向对象的语言以对象为核心,其主要特点包括封装性、继承性和多态性。封装性允许隐藏内部状态和复杂性;继承性支持新对象基于现有对象的属性和行为构建;多态性允许以统一的方式处理不同类型的对象。
例如:Java是一种广泛使用的面向对象编程语言,它通过类和对象的概念来封装数据和操作。Java中的程序设计包括创建对象、通过继承机制共享行为以及利用接口实现多态性。这种语言风格便于构建模块化、可扩展和易于维护的代码。
6. 上下文无关文法
上下文无关文法(Context-Free Grammar,简称CFG)是一种用来描述形式语言的文法类型。在计算机科学和语言学中,它被广泛用于描述编程语言的语法和自然语言的结构。一个上下文无关文法 G G G 可以表示为一个四元组:
G = ( V N , V T P , S ) G=(V_N,\,V_T\,P,\,S) G=(VN,VTP,S)
- V N V_N VN 是一个非空有限集合,它的每个元素被称为非终结符号(non-terminal),非终结符是用来表示语法结构的符号,它们是产生式规则中的变量,代表了更大的构造块或模式;
- V T V_T VT 是一个非空有限集合,它的每个元素被称为终结符号(terminal),终结符是文法的基本符号,不能被进一步分解。在编程语言中,终结符通常是关键字、运算符、数字等最基本的元素,并有 V T ∩ V N = ϕ V_T\, \cap \,V_N = \phi VT∩VN=ϕ ;
- P P P 是一个有限集合,包含了一系列产生式,产生式规则定义了如何从非终结符生成字符串(可以包含非终结符和终结符),每个产生式的形式是 P → α P \to \alpha P→α,其中 P ∈ V N P \in V_N P∈VN 且 α ∈ ( V T ∪ V N ) \alpha \in (V_T \cup V_N) α∈(VT∪VN);
- S S S 是一个非终结符,称为开始符号,它是整个文法的起点。
7. 构造文法
8. 二义文法
如果一个文法的某个句子对应两棵不同的语法树,即其最左( 最右)推导不唯一,称该文法为二义文法。对于程序设计语言而言,通常需要其语法是无二义的。这是因为在编程中,每个语句的含义必须是清晰且明确的,以保证程序的一致性和可预测性。如果一个编程语言的文法是二义的,同一个语句可能会被编译器或解释器以不同的方式解释,导致程序行为的不确定性,这在实际应用中是不可接受的。
然而,证明一个文法是否是二义的通常是非常困难的。一个常见的方法是找到一个具体的句子,并展示它可以对应至少两棵不同的语法树。这种方法可以证明文法是二义的。但是,如果无法找到这样的句子,我们通常不能简单地断定该文法是无二义的。这是因为不存在一个通用的算法可以穷举所有可能的句子和它们的推导树,以证明一个文法的无二义性。
下面是一个二义文法的例子:
P → E E → E + E E → i d e n t E → i n t P\to E \\ E \to E+ E \\E \to ident \\ E \to int P→EE→E+EE→identE→int
由于找到句子 i d e n t + i n t + i n t ident+int+int ident+int+int 可以对应下面两种推导方式,因此可以证明这个文法是二义的:
要想消除这个文法的二义性,我们可以修改上述文法,使得非终结符 E E E 不能既推出表达式 E + E E+E E+E 又能推出终结符 i d e n t ident ident 和 i n t int int,可以做下列修改:
P → E E → E + T E → T T → i d e n t T → i n t P \to E \\ E \to E + T \\ E \to T \\ T \to ident \\ T \to int P→EE→E+TE→TT→identT→int
这样,通过保留左递归和消除右递归,上述文法的二义性就被消除了。现在假设我们想要为该文法增加更多的运算符,例如 ∗ * ∗,如果将该规则定义为 E → E ∗ E E \to E * E E→E∗E 的形式,二义文法还是会出现。然而,我们还是可以采用上述相同的方式(保留左递归消除右递归)来消除这种二义性,文法整体就变成了:
P → E E → E + T E → T T → T ∗ F T → F F → i d e n t F → i n t P \to E \\ E \to E+T \\ E \to T \\ T \to T * F \\ T \to F \\ F \to ident \\ F \to int P→EE→E+TE→TT→T∗FT→FF→identF→int
一般来说,优先级越低的运算符在更高层(例如 + + +、 − - −),而优先级越高的运算符在更底层(例如 ∗ * ∗、 ÷ \div ÷),这是因为计算数学表达式的时候是自底向上的,总是计算优先级高的表达式再执行优先级低的表达式。
悬空 e l s e else else 是一个更经典的二义文法的例子:
$$
P \to S \
S \to if ,, E ,, then ,, S \
S \to if ,, E ,, then ,, S ,, else ,, S \
S \to other
$$
句子 i f E t h e n i f E t h e n o t h e r e l s e o t h e r if \,\, E \,\, then \,\, if \,\, E \,\, then \,\, other \,\, else \,\, other ifEthenifEthenotherelseother 对应下面两种推导,因此文法是二义的:
if E then
if E then
other
else
other
if E then
if E then
other
else
other
句子末尾的 e l s e o t h e r else \,\, other elseother 可以属于两个 i f if if 中的任意一个,于是产生了二义性。为了消除这个文法的二义性,我们的核心目标是确保每个 e l s e else else 分支都能与一个明确的 i f if if 语句对应。通过下列改写可以消除这种二义性:
P → S S → i f E t h e n S S → i f E t h e n L e l s e S L → i f E t h e n L e l s e L S → o t h e r L → o t h e r P \to S \\ S \to if \,\, E \,\, then \,\, S \\ S \to if \,\, E \,\, then \,\, L \,\, else \,\, S \\ L \to if \,\, E \,\, then \,\, L \,\, else \,\, L \\ S \to other \\ L \to other P→SS→ifEthenSS→ifEthenLelseSL→ifEthenLelseLS→otherL→other
当一个 i f if if 语句包含一个嵌套的 i f − e l s e if-else if−else 结构时,它将产生一个 L L L 非终结符,每个 L L L 表示一个完整的 i f − e l s e if-else if−else 语句,并在需要时递归扩展。通过这种方式,文法确保了每个 e l s e else else 语句都紧随其最近的 i f if if 语句。举个例子,加入我们在句子中遇到一个 e l s e else else,现在有两种选择:
- 使用规则 L → i f E t h e n L e l s e L L \to if \,\, E \,\, then \,\, L \,\, else \,\, L L→ifEthenLelseL 是将其规约为 L L L;
- 使用规则 S → i f E t h e n L e l s e S S \to if \,\, E \,\, then \,\, L \,\, else \,\, S S→ifEthenLelseS 是将其规约为 S S S;
如果将其规约为 L L L,那么其一定是一个完整的 i f − e l s e if-else if−else 结构的前半部分;如果将其规约为 S S S,那么其一定是一个完整的 i f − e l s e if-else if−else 结构的后半部分,或 i f if if 结构的后半部分。通过这种办法,每个 e l s e else else 都会与它最近的 i f if if 成功匹配,从而避免了悬空 e l s e else else 问题。
9. 构造正规式
正则表达式(Regular Expression, RE),也称正规式,是一种强大的文本处理工具,用于在字符串中进行搜索、匹配和替换操作。它通过定义一个特定的模式(pattern),来描述一系列符合某个句法规则的字符串。正则表达式通常用于文本搜索、数据验证、数据提取等领域。
正则表达式中包含许多特殊符号,每个都有其独特的用途和含义。以下是一些常见的正则表达式符号及其用途:
.
(点):匹配任何单个字符(除了换行符);*
:表示前面的字符可以出现零次或多次;+
:表示前面的字符至少出现一次;?
:表示前面的字符最多出现一次(即该字符是可选的);{n}
:表示前面的字符恰好出现n次;{n,}
:表示前面的字符至少出现n次;{n,m}
:表示前面的字符至少出现n次,但不超过m次;[abc]
:表示匹配括号内的任意一个字符(在这个例子中是a
、b
或c
);[^abc]
:表示匹配不在括号内的任何字符;(abc)
:表示匹配括号内的精确序列abc
;|
:表示逻辑或(OR),匹配前后的表达式之一。
这些符号可以组合使用,创建复杂的匹配模式,以满足各种文本处理需求。
-
例1:令 ∑ = { 0 , 1 } \sum = \{0,\,1\} ∑={0,1},构造正规式,使其包含偶数个0和偶数个1的字。
分析:一个包含偶数个0和偶数个1的字由若干个已满足要求的短字组成,有以下三种情形:
- 00,这种以满足要求;
- 11,这种也已满足要求;
- 10或01开头,则中间经历任意多个00或11,最后必须以10或01结尾,即:(10|01)(00|11)*(10|01)。
由以上三种任意组合,即可满足要求:
( ( 10 ∣ 01 ) ( 00 ∣ 11 ) ∗ ( 10 ∣ 01 ) ∣ 00 ∣ 11 ) ∗ ((10|01)(00|11)*(10|01)|00|11)* ((10∣01)(00∣11)∗(10∣01)∣00∣11)∗
10. 有限自动机(FA)
有限自动机(Finite Automata, FA)是一种抽象的状态图,它可以用来表示某些计算形式。从图形上来看,一个有限自动机由若干状态(用编号的圆圈表示)和这些状态之间的若干边(用标记的箭头表示)组成。每条边上标记有一个或多个来自字母表 ∑ \sum ∑ 的符号。
这台机器以一个起始状态 S 0 S0 S0 开始。对于每一个呈现给 FA 的输入符号,它会移动到与该输入符号标签相同的边所指示的状态。FA 的某些状态被称为接受状态,用双层圆圈表示。如果在所有输入被消耗后,FA 处于一个接受状态,那么我们说 FA 接受这个输入。如果FA以一个非接受状态结束,或者当前输入符号没有对应的边,我们则说 FA 拒绝这个输入字符串。
每一个正则表达式(RE)都可以写成一个 FA,反之亦然。对于一个简单的正则表达式,人们可以手动构造一个 FA。例如,以下是一个用于关键字 for 的 FA:
下面是用来表示正规式 [ a − z ] [ a − z 0 − 9 ] + [a-z][a-z0-9]+ [a−z][a−z0−9]+ 的 FA:
下面是用来表示正规式 ( [ 1 − 9 ] [ 0 − 9 ] ∗ ) ∣ 0 ([1-9][0-9]*)|0 ([1−9][0−9]∗)∣0 的 FA:
11. 确定有限自动机(DFA)
上述三个例子中的每一个都是一个确定性有限自动机(Deterministic Finite Automata, DFA)。DFA 是 FA 的一个特殊情况,其中每个状态对于给定的符号最多只有一个出边。换句话说,DFA没有歧义:对于每一种状态和输入符号的组合,都有且仅有一种选择来决定下一步该怎么做。
由于这个特性,DFA 在软件或硬件中非常容易实现。只需要一个整数 c c c 来跟踪当前状态。状态之间的转换由一个矩阵 M [ s , i ] M[s, \,i] M[s,i] 表示,该矩阵编码了给定当前状态和输入符号的下一个状态。(如果不允许转换,我们用 E E E 来标记,表示错误)对于每一个符号,我们计算 c = M [ s , i ] c=M[s, \,i] c=M[s,i],直到所有输入被消耗完毕,或达到错误状态。
-
例1:设计一个 DFA,使其识别包含偶数个0和偶数个1的句子(包含空句子)。
分析:DFA 可以包含下面4种状态,输入一个字符后从一个状态转换到另一个状态:
- 0:偶数个0偶数个1;
- 1:偶数个0奇数个1;
- 2:奇数个0偶数个1;
- 3:奇数个0奇数个1。
于是 DFA 可以设计为:
-
例2:设计一个 DFA,使其接受 ∑ = { 0 , 1 } \sum = \{0,\,1\} ∑={0,1} 上能被4整除的大于1的二进制数。
分析:1. 任意二进制数除以四,只有余数为0、1、2、3四种情况,因此需要四个状态;2. 当一个二进制数的后面增加一个0,该二进制数变为原来的2倍,如果后面增加一个1,则变为原来的2倍加1;3. 大于1的第一个二进制数是10,其被4整除之后是2,因此将起始状态设置为2。通过以上分析,设计出来的 DFA 如下所示:
-
例3:设计一个 DFA,使其接受 ∑ = { 0 , 1 , . . . , 9 } \sum=\{0,\,1\,,...,\,9\} ∑={0,1,...,9} 上能被3整除的十进制数。
分析:1. 任意十进制数除以3,只有余数为0、1、2三种情况,因此需要三个状态;2. 一个十进制数 n n n 后面加 i i i,变为 10 n + i 10n+i 10n+i;3. 初态只能为0,但可以接受空字。通过以上分析,设计出来的 DFA 如下所示:
12. 非确定有限自动机(NFA)
DFA 的替代选择是非确定有限自动机(Nondeterministic Finite Automata, NFA)。NFA是一种有效的有限自动机,但其内在的不确定性使得它在处理上相对更为复杂。
以正则表达式 [a-z]*ing
为例,该表达式代表所有以 ing
结尾的小写单词。这可以用以下自动机表示:
现在考虑这个自动机如何处理单词 sing
。它可以有两种不同的处理方式:
- 在
s
上转移到状态0,i
上转移到状态1,n
上转移到状态2,g
上转移到状态3; - 整个过程中都停留在状态0,将每个字母与
[a-z]
转换匹配。
这两种方式都遵守转换规则,但一种导致接受,另一种导致拒绝。这里的问题在于状态0在符号 i
上允许两种不同的转换。一种是留在状态0,匹配 [a-z]
,另一种是转移到状态1,匹配 i
。
此外,没有简单的规则来选择其中一条路径。如果输入是 sing
,正确的解决方案是在 i
上立即从状态0转移到状态1。但如果输入是 singing
,那么我们应该在第一个 ing
时留在状态0,然后在第二个 ing
时转移到状态1。
为了解决这个问题,我们可以在状态转换图中引入空字 ϵ \epsilon ϵ,使得所有可能的路径都会被同时考虑。如果任何一条路径能够成功地处理整个输入字符串并且到达接受状态,那么状态转换图就会接受该字符串。这种构造方法叫做 NFA。
例如,为正规式 a*(ab|ac)
我们可以构造下列的 NFA:
这样,当输入句子为 aab
的时候,它会同时考虑下面的匹配规则:
- 在
a
上转移到状态0,下一个a
上再次转移到状态0,b
上没有可行的转移规则; - 通过
ϵ
\epsilon
ϵ 转移到状态1,
a
上转移到状态2,下一个a
上没有可行的转移规则; - 在
a
上转移到状态0,通过 ϵ \epsilon ϵ 转移到状态1,下一个a
上转移到状态2,b
上转移到状态3。
NFA 会对输入的句子执行所有可行的尝试,直到有一种转移方法到达接受状态为止。
正则表达式和有限自动机在能力上是等价的。对于每一个正则表达式(RE),都存在一个相应的有限自动机(FA),反之亦然。然而,在三者中,确定性有限自动机(DFA)是最直接实现于软件中的,但非确定性有限自动机(NFA)是最便于编程者构造的。一般而言,在接受了一个 RE 后,我们会将其转换为 NFA,然后再确定化为 DFA。
13. 构造 NFA
假设要将正规式 (a|b)*(aa|bb)(a|b)*
转换为 NFA,第一步是构造初态和终态两个状态,将它们之间用箭头连接起来,并将正规式放在箭头上面:
接下来,将正规式扩展为 (a|b)、(aa|bb)、(a|b) 三个部分:
最后,进一步扩展深层的表达式:
14. 句型、句子、短语
**句型:**句型是由文法的开始符号通过一系列的推导规则所生成的符号串。这些符号串可能包含非终结符号和终结符号。例如,在一个简单的算术表达式文法中,句型可能是 E + T E+T E+T 或者 n u m ∗ ( E ) num * (E) num∗(E) 这样的符号串,其中 E E E 和 T T T 是非终结符号, n u m num num 是终结符号;
**句子:**句子是一种特殊的句型,它完全由终结符号组成。换句话说,句子是从开始符号仅通过终结符号推导出来的符号串。在上述算术表达式的例子中,一个句子可能是 3 + 5 3+5 3+5 或者 4 ∗ ( 2 + 3 ) 4 * (2 + 3) 4∗(2+3)。这些都是不再含有任何非终结符号的表达式,代表了该文法所能描述的具体语言实例;
**短语:**短语是句型中相对于某个非终结符号的一部分。如果在某个句型的推导过程中,非终结符号 A A A 被替换为了一系列符号(可以是终结符号、非终结符号或二者的混合),那么这一系列符号就构成了一个短语。短语反映了语法树中非终结符号所代表的子树的结构。例如,在句型 E + T E+T E+T 中,如果 E E E 被推导为 n u m num num,那么 n u m num num 就是这个句型相对于非终结符号 E E E 的短语。
-
对一个抽象语法树来说,子树的边缘是相对于子该子树根节点的短语。
例如如下的语法树,对于所有以 S S S 为根的子树,其边缘自底向上可以是: ( T ) (T) (T)、 b b b、 ( S d ( T ) d b ) (Sd(T)db) (Sd(T)db);对于所有以 T T T 为根的子树,其边缘自底向上可以是: S S S、 S d ( T ) Sd(T) Sd(T)、 S d ( T ) d b Sd(T)db Sd(T)db。
**直接短语:**对文法 G G G,如果存在一个推导 S = > α A δ S => \alpha A\delta S=>αAδ 且 A = > β A => \beta A=>β,则称 β \beta β 是句型 α A δ \alpha A\delta αAδ 相对于非终结符号 A A A 的直接短语。直接短语对应于语法树中从某个非终结符号直接推导出来的所有符号,它们构成了语法树中高度为2的子树,即二层子树的边缘。换句话说,直接短语是所有二层子树的边缘。在上图中有三个二层子树:
- S S S 是 T T T 的直接短语;
- ( T ) (T) (T) 是 S S S 的直接短语;
- b b b 是 S S S 的直接短语。
**句柄:**在一个句型中,最左边的直接短语被称为句柄。句柄是语法分析中特别重要的概念,因为它代表了最左边的二层子树的边缘。在某些语法分析算法中,如移进-归约分析法,句柄的识别是进行归约操作的关键。最左直接短语。句柄是最左二层子树的边缘,例如上图中的句柄是 S S S;
-
例1:证明 E + T ∗ F E+T*F E+T∗F 是下列文法的一个句型,并指出这个句型的所有短语、直接短语和句柄。
E → E + T ∣ T T → T ∗ F ∣ F F → ( E ) ∣ i E\to E+T \, | \, T \\ T \to T *F \, | \, F \\ F \to (E) \, | \, i E→E+T∣TT→T∗F∣FF→(E)∣i
证明:上述句型可以由 E → E + T → E + T ∗ F E \to E+T \to E+T*F E→E+T→E+T∗F 得出,因此该句型属于此文法。同时,画出该句型的抽象语法树可以得到其直接短语(所有二层子树的边缘)是 T ∗ F T*F T∗F,句柄(最左二层子树的边缘)是 T ∗ F T*F T∗F。
**活前缀:**活前缀是指在语法分析过程中,那些能够被进一步扩展成为合法句型的前缀部分,简单来说就是一个句型的前缀。
-
例1:对于下列文法,写出 a b c abc abc 和 a b A abA abA 的活前缀:
S → E E → a A A → b A A → c S \to E \\ E \to aA \\ A \to bA \\ A \to c S→EE→aAA→bAA→c
a b c abc abc 和 a b A abA abA 都是合法的句型,因此它们的活前缀分别是:
a b c abc abc 的活前缀:
ϵ a a b a b c \epsilon \\ a \\ ab \\ abc ϵaababc
a b A abA abA 的活前缀:
ϵ a a b a b A \epsilon \\ a \\ ab \\ abA ϵaababA
-
素短语和最左素短语的定义非常不明确,目前翻遍了中文互联网没找到能说清楚的帖子,实际写编译器的时候也没什么用,深究可能会造成混淆。有兴趣可以查看一篇英文文章:Bottom-Up Parsing (Compiler Writing) Part 2 (what-when-how.com)
15. 规范规约
规范规约是最右推导的逆过程,因此也称为最左规约。
16. 根据 FA 写正规式
是根据正规式构造 FA 的逆过程。
17. 符号表
用于登记源程序的各类信息,如变量名、常量名、过程名等,以及编译各阶段的进展状况。当扫描器识别出一个标识符后,把该名字填入符号表,在语义分析阶段回填类型,在目标代码生成阶段回填地址。
符号表的作用和地位:(重点)
- 收集符号属性;
- 语义合法性检查的依据,如重复变量定义;
- 作为目标代码生成阶段地址分配的依据。
符号表的主要属性:
- 符号名:变量、过程、类的名称;
- 符号数据类型:整型、实型、布尔型;
- 符号声明类别:
static
、const
等; - 符号存储方式:堆区存储还是栈区存储等;
- 符号作用域:全局变量与局部变量;
符号表的组织方式:
- 构造多个符号表,具有相同属性种类的符号组织在一起;
- 把所有符号项都组织在一张大的符号表中;
- 折中了上述两种方案,根据符号属性的相似程度分类成若干张表;
- 使用对象组织,需要编译器的支持,但非常方便管理;
符号表项的排列:
- 数组:线性组织;
- 链表:
Hash
表,跳表; - 树形:二叉树,平衡二叉树。
18. 运行时空间组织
运行时存储器的划分:
- 代码区:编译生成的目标代码;
- 静态区:编译时就可以完全确定的数据;
- 栈区:栈式内存分配;
- 堆区:堆式内存分配;
存储分配策略:
- **静态分配策略:**在编译时对所有数据对象分配固定的存储单元,且在运行时始终保持不变;
- **栈式动态分配策略:**在运行时把存储器作为一个栈进行管理,运行时,每当调用一个过程,它所需要的存储空间就动态地分配于栈顶,一旦退出,它所占空间就予以释放;
- **堆式动态分配策略:**在运行时把存储器组织成堆结构,以便用户关于存储空间的申请与归还(回收),凡申请者从堆中分给一块,凡释放者退回给堆;
活动记录:
为了管理过程在一次执行中所需要的信息,使用一个连续的存储块,这样的一个连续存储块称为活动记录, 一般包括:
- 局部变量:源代码中在过程体中声明的变量;
- 临时单元:为了满足表达式计算要求而不得不预留的临时变量;
- 内情向量:内情向量是静态数组的特征信息,用来描述数组属性信息的一些常量,包括数组类型、维数、各维的上下界及数组首地址;
- 返回地址:调用位置的地址;
- 动态链:
SP
指向当前过程的动态链地址(也是帧起始地址),它又指向调用该过程的上一过程的帧起始地址,用于过程结束后回收分配的帧;它和函数的嵌套定义关系无关,只与调用顺序有关; - 静态链:指向当前过程的直接父过程的帧起始地址,用于访问非局部数据,它只与函数嵌套定义关系有关。
19. 优化手段
- 源代码级别:选择适当的算法,例如快排优于插排;
- 语义动作级别:生成高效的中间代码,例如在词法分析阶段加入错误检查;
- 中间代码级别:安排专门的优化阶段,例如
DAG
优化;- 局部优化:例如
DAG
优化; - 循环优化:包含代码外提、强度削弱、删除归纳变量、复写传播等;
- 局部优化:例如
- 目标代码级别:考虑如何有效地利用寄存器,例如窥孔优化。
20. 待用/活跃信息
当翻译 A = B op C
时:
- 待用信息:变量在哪些中间代码中还会被引用;
- 活跃信息:
A
、B
、C
是否还会在基本块内被引用;
21. LL(1)分析
对于一个不含回溯和左递归的文法,LL(1) 方法从左到右扫描输入串,维护一个状态栈和一个符号栈,每一步只向右查看一个符号,根据状态栈顶、符号栈顶和分析表的内容来确定下一步的动作,最终分析出整个句子。
22. LR(1)分析
LR(1) 分析适合大多数上下文无关文法,它从左到右扫描符号串,能记住移进和规约出的整个符号串,即 “记住历史”,还可以根据所用的产生式推测未来可能碰到的输入符号,即 “展望未来”,根据 “历史”、“展望” 和分析表的内容来确定下一步的动作,最终分析出整个句子。
23. display 表
为提高访问非局部变量的速度,引入指针数组指向本过程的所有外层,成为嵌套层次显示表,display 表是一个栈,自顶向下依次指向当前层、直接外层、直接外层的直接外层,直到最外层。
24. 语法制导翻译法
对单词符号串进行语法分析,构造语法分析树,然后根据需要遍历语法树并在语法树的各结点处按语义规则进行计算。这种有源程序的语法结构驱动的处理办法就是语法制导翻译法。
二、综合题
2.1 词法分析
给定正规式: ①构造NFA ②确定化 ③最小化
例题:
① + ②
③
2.2 LL(1) 分析
给出文法:①构造First集合 ②构造Follow集合 ③构造LL(1)分析表 ④识别句子
例题:
首选判断LL(1) 适用条件:
①求 First(X)
A -> Bc
B -> ε
则 First(A) = { c };
若 A -> B
B -> ε
则 First(A) = { ε };
②求 Follow(X)
③构造分析表
④分析过程
2.3 LR(1) 分析
给出文法:①构造拓广文法 ②求First集合 ③构造LR(1)项目集规范族 ④构造LR(1)分析表并消除二义性 ⑤识别句子
构造带向前搜索符的DFA,无归约-归约冲突则是LR(1)文法。LL和LR的共同点是他们都从左到右读入输入字符串,但LL从语法树自顶向下分析,LR从语法树自底向上分析。
例题1
构造文法G[S]的LR(1)项目集规范族:
S -> aCaCb
S -> aDb
C -> a
D -> a
①构造拓广文法:
S' -> S
S -> aCaCb
S -> aDb
C -> a
D -> a
②求 First
集合:
③构造 LR(1)
项目集规范族:
④构造 LR(1)
分析表并消除二义性:
消除二义性需要认为定义运算优先级,发生移入-规约冲突时选择正确的项填入。
⑤识别句子 aaaab
和 aab
:
2.4 中间代码生成
给出翻译模式和高级语言程序,翻译句子,一般涉及多种类型句子的综合,也可能涉及声明语句填写符号表。
1. 过程中的说明语句
2.算术表达式的翻译
3. 布尔表达式的翻译
重点:回填。
4. 控制流语句的翻译
重点:if 和 while 。
2.5 目标代码生成
给出基本块代码:①构造DAG ②写出优化后的中间代码 ③写出DAG目标优化后的中间代码 ④根据变量活跃性和寄存器信息,写出目标代码。
例题1
给出基本块代码为:
①构造DAG
②写出优化后的中间代码
③写出DAG目标优化后的中间代码
(1) T6 = R - r
(2) T2 = R * r
(3) T4 = T2
(4) T1 = 6.28
(5) T3 = 6.28
(6) A = 6.28 * T2
(7) T5 = A
(8) B = A * T6
(9) T0 = 3.14
④根据变量活跃性和寄存器信息,写出目标代码
假定 B
是基本块出口之后活跃的,有寄存器 R0
和 R1
可用,目标代码为:
DAG 优化中,不活跃变量,目标代码依然要生成计算其值的代码,只是不生成存储到主存的代码。计算代码被优化是后续优化完成的,不是 DAG 完成的。
LD R0, R
SUB R0, r R0: T6 R1: \
LD R1, R
MUL R1, r R0: T6 R1: T2
ST R0, T6
LD R0, 6.28 R0: 6.28 R1: T2
MUL R0, R1 R0: A R1: T2
MUL R0, T6 R0: B R1: T2
ST R0, B
三、历年考试回忆
2004-2005
2015-2016
一、简答(30 分)
1.给了一个文法(具体忘了),让证明二义性。
2.写出文法,表示{0、1}集上的所有正规式
3.解释 S-属性文法
4.说出传地址和传质这两种参数传递方式的异同
5.结合具体例子谈一谈回填思想
二、(babbb**)* 画 NFA 转 DFA ,最小化(15 分)
三、(1)一个文法,判断是否为 LL(1)文法(10 分)
(2)上题中的文法是否为 SLR 文法,给出证明(15 分)
(与课本上例子不同之处在于有 B ——〉ε)
四、利用语法指导思想写四元式(15 分)
(比较简单,就是一个 while-do 语句)
五、给出一段中间代码,画 DAG 图;只有 R 在代码块外是活跃的,做优化;如
果有寄存器 R0 和 R1,写出目标代码。(15 分)
2016-2017
一、简答题
1.编译流程图及各部分的作用。⒉举例说明文法二义性。
3.什么是L属性文法?
4.写出允许过程嵌套的C活动记录结构。5.中间代码优化有哪几种?
6.符号表作用。
二、词法分析
RE NFA DFA最小化DFA整个流程走一遍
三、自上而下文法分析
给了一个文法,证明文法是LL1的,画表。
四、自下而上文法分析
给了一个文法,证明文法是LR1的,画表。
五、中间代码生成
—段代码,四元式翻译。涉及循环和判断的嵌套。
六、代码优化和目标代码生成。
给了一个代码段,
先画DAG图,然后优化,最后输出汇编代码。
2017-2018
2019-2020
一、简答题(25分)
1.判断一个文法是否二义
2.编译的前端,后端,什么是一遍扫描
3.什么是S属性
4.什么是语法制导翻译
5.在语法制导翻译中,空返产生式的作用(M->e)
二、计算题(75分)
1.一个单词表由a,b组成,请写出代表偶数个a的正规式,NFA,并确定化、最小化
2.判断一个文法是不是LL(1)的,如果是就写出预测分析表,不是就说明原因(15分)
3.判断一个文法是不是SLR(1)的,如果是就写出预测分析表,不是就说明原因(15分)
4.中间代码生成程序(15分)
while a<c and b<d do if c==1 then c:=c+1 else c:=c+2;
5.代码优化(15分)
DAG优化,最后写出四元式的形式(这个是一个坑,四元式是目标代码,也就是此时要做目标代码生成),同时目标代码生成要列表(Rvalue 寄存器描述,Avalue地址描述)。
2020-2021
2021-2022第二学期
一、简答题
- 画编译流程图;
2.判断一个文法是不是二义文法;
3.给一个句型,找它的句柄;
4.中缀表达式转后缀,比较长,最好使用算法一步步推导;
5.消除循环左递归;
6.给一个 DFA,把它转换为正规式
二、计算题
1.词法分析:给定正规式 ①构造NFA ②确定化 ③最小化
2.LL(1)分析,给出文法 ①构造First集合 ②构造Follow集合 ③构造LL(1)分析表(可能涉及消除二义文法冲突)④识别句子
3.LR(1)分析,给出文法 ①构造拓广文法 ②构造拓广文法的LR(1)项目集规范族 ③构造LR(1)分析表(及消除二义文法冲突) ④识别句子
4.给出基本块代码 ①构造DAG ②写出优化后的中间代码 ③写出DAG目标优化后的中间代码 ④根据变量活跃性和寄存器信息,写出目标代码
5.给出翻译模式和高级语言程序,翻译句子 while a < b do if c > d then x = y * z
,会给出翻译规则。
2022-2023第一学期
一、概念题 5分×5
1.画编译流程图
2.给出有穷自动机的概念,说明 NFA DFA的区别
3.简述推导和归约的概念
4.说明法制导定义的概念。S-SDD、L-SDD的概念
5.基本块划分方法
二、a((b(a|b)*)|空)ab 画NFA转DFA并最小化
三、说明下列文法是LL(1)的,给出语法分析表,并分析ccccd
S -> CC
C -> cC
C -> d
四、说明下列文法是LR(0)的,给出分析表并分析accd
S -> aA|bB
A -> cA|d
B -> cB|
五、说明语法制导翻译的思想(7分)
六、代码优化列举四种并说明(8分)
2022-2023第二学期
1、考试时间:2023/5/26 14:00-16:00
2、考试科目:编译原理(老师:LiuHong)
3、考后感悟:本次考试题目近80%都是2023年初开学考试的题目,真的一模一样,符号都不带变的
。提醒一下最好带个尺子、铅笔和橡皮,这样更方便画图和表格。
一、简答题(5*5’=25’)
1.画出编译原理的程序框图。
2.什么是文法的二义性?为什么要消除二义性?如何消除二义性?
1、二义性:给定文法,若存在某个句子,有多个最左/右推导,即可以生成多棵解析树,则这个文法就是二义的。 2、通常要求程序设计语言的文法的无二义性的,否则会导致一个程序有多个“正确”的解释。即使文法允许二义性,但仍需要在文法之外加以说明,来剔除不要的语法分析树。总之,必须保证文法消除了二义性使得最后的语法解析树只有一棵。 3、①改写原文法 ②引入消除二义性的规则。
3.简述推导和归约的概念。
推导:将终结符替换为它的某个产生式的体。归约:将一个与某个产生式的体相匹配的特定子串替换为该产生式的头。
4.简述递归下降语法分析技术的基本思想。
对于LL(1)文法,不必实际构建解析树,而且可以借助系统栈来实现预测分析,这就是递归下降算法。
5.简述划分基本块的算法。
①确定首指令:第一个三地址指令;任意一个转移指令的目标指令;转移指令后的一个指令。②确定基本块:从一个首指令开始到下一个首指令之间的部分为一个基本块。
二、词法分析(20’)
根据正则式:a( (b(a|b)*) |
ε
\varepsilon
ε)ba,写出NFA ,确定化,最小化。
①NFA:
②确定化:
DFA:
③最小化: 初步划分为{0,1,2,4,5}、{3}; 根据a将{0,1,2,4,5}划分为{0,1,5}、{2,4}; 根据a将{0,1,5}划分为{0},{1},{5}; {2,4}不用划分,归为一个节点; 最终节点划分为{0}、{1}、{2,4}、{3}、{5}。
三、语法分析(20’)
G(S)文法如下:
S→CC
C→cC
C→d
3.1 G(S)对应的First和Follow是什么?证明是G(S)是LL(1)。
First(S) = {c,d}
First(C) = {c,d}
Follow(S) = {$}
Follow(C) = {c,d,$}
证明:① G(S)不含左递归。
② 对于C——>cC,C——>d中,First(cC) ⋂ First(d) =
φ
③ 对于A,C,它们的首终结符都不含
ℇ
所以G(S)是LL(1)
3.2写出G(S)的预测分析表。
Start(S——>CC) = {c,d}Start(C——>cC) = {c}Start(C——>d) = {d}
c | d | |
---|---|---|
S | S——>CC | S——>CC |
C | C——>cC | C——>d |
3.3根据预测分析表,写出cdccccd的TOP-DOWN的推导过程。
S$ | cdccccd$ |
---|---|
CC$ | cdccccd$ |
cCC$ | cdccccd$ |
CC$ | dccccd$ |
dC$ | dccccd$ |
C$ | ccccd$ |
cC$ | ccccd$ |
C$ | cccd$ |
cC$ | cccd$ |
C$ | ccd$ |
cC$ | ccd$ |
C$ | cd$ |
cC$ | cd$ |
C$ | d$ |
d$ | d$ |
$ | $ |
四、语义分析(20’)
给出以下文法:
E→aA|bB
A→cA|d
B→cB|d
4.1证明是LR(0)。
证明:对文法进行拓广:(1)E'——>E(2)E——>aB(3)E——>bB(4)A——>cA(5)A——>d(6)B——>cB(7)B——>d画图如下:
从上图可以看出该文法没有 移入-归约冲突,也没有 归约-归约冲突,所以是LR(0)文法。
4.2 写出预测分析表。
注意归约E'——>E
五、语法制导翻译(7’)
写出语法制导翻译的基本思想。并说明抽象语法树在语法制导翻译中的角色。
1、基本思想:对字符串进行语法分析,构建语法分析树,然后根据需要遍历语法树并在语法书的各结点处按语义规则进行计算。这种有源程序的语法结构驱动的处理方法就是语法制导翻译。2、抽象语法树中,每个结点代表一个语法结构,比如对应某个运算符;结点的每个子结点代表其子结构,比如对应运算分量,表示这些子结构按照特定的方式组成了较大的结构,可以忽略掉一些标点符号等非本质的东西。抽象语法树是将源代码转换为目标代码的中间表示形式,可以帮助我们更好地理解源代码的结构和语义。在语法制导翻译中,我们可以通过遍历抽象语法树来执行语义动作,生成目标代码。
六、代码优化(8’)
说明局部优化和全局优化的不同。写出至少四个优化方法,并简述其算法。
1、局部优化是指单个基本块范围内的优化;全局优化是指面向多个基本块的优化。2、优化方法:①删除公共子表达式:如果表达式 x op y 先前已被计算过,并且从先前的计算到现在,x op y 中变量的值没有改变。那么可以删除公共子表达式。②删除无用代码:在复制语句x = y的后面尽可能地用y代替x③常量合并:如果在编译时刻推导出一个表达式的值是常量,就可以 使用该常量来替代这个表达式④代码移动:对于那些不管循环执行多少次都得到相同结果的表达式,在进入循环之前就对它们求值。⑤强度削弱:用较快的操作代替较慢的操作。
2023-2024第一学期
一、简答题
- 画图表示编译过程的各阶段
- 给一个句子判断是不是二义文法
- 给一个句子,写出短语和句柄
- 提左公因子
- 中缀转后缀表达式(只有加法和括号和乘法)
给一个FA,给出对应的正则表达式
二、词法分析:给定正规式,类似作业题那个00|11闭包那个题,然后构造NFA、确定化和最小化。
三、LL(1)分析,给出文法,消除左递归、构造First、Follow集合、构造LL(1)分析表(可能涉及消除二义文法冲突)、识别句子。
四、LR分析,给出文法,构造拓广文法,构造拓广文法的LR(1)项目集规范族(这里好像设计了同心集合并,因为他给了10种状态,正常推肯定大于10种),构造LR(1)分析表,识别句子(一共规约9~10行,很快,我是强行ACC了最后)。
五、给出翻译模式和高级语言程序,翻译句子。翻译模式很长,给了一个while+if+then的句子最后好像是then a;b(a和b都是句子)
然后让填符号表,让填10行的中间代码。
六、给出基本块代码(和最后的作业题很像)(比较简单),构造DAG,写出优化后的中间代码,写出DAG目标优化后的中间代码,根据变量活跃性和寄存器信息,写出目标代码,整个题量很大,很多人都没做完,题也比较难,主要是LR1分析大部分人都画时间很多。只有看到一个题,一眼就知道咋做,然后立刻写,中间不停顿才差不多做完。