编译原理复习笔记

文法和语言的形式定义

终结符号 \(V_T\)、非终结符号 \(V_N\) 满足 \(V_T \cap V_N = \emptyset\)

产生式 $\alpha \rightarrow \beta $, \(\alpha \in (V_T \cup V_N)^+, \beta \in (V_T \cap V_N)^*\)

文法 \(G[S] = (V_N, V_T, P, S)\)\(P\) 是产生式的集合,\(S\) 是开始符号

  • 0 型文法(短语结构文法) $\alpha \rightarrow \beta $, \(\alpha \in (V_T \cup V_N)^+, \beta \in (V_T \cap V_N)^*\)

  • 1 型文法(上下文有关文法)$\alpha \rightarrow \beta $, \(\alpha \in (V_T \cup V_N)^+, \beta \in (V_T \cap V_N)^*, 1 \leq |\alpha| \leq |\beta|\)

  • 2 型文法(上下文无关文法)$\alpha \rightarrow \beta $, \(\alpha \in V_N, \beta \in (V_T \cap V_N)^*, 1 \leq |\alpha| \leq |\beta|\)

  • 3 型文法(正则文法)满足右线性(非终结符号只能出现在产生式的最右侧)或者左线性,即 \(A \rightarrow a | aB\) 或者 \(A \rightarrow a | Ba\)

一个只含有加法、乘法和括号的文法:

\(G[E] = (\{E, T, F\}, \{i, +, *, (, ) \}, P, E)\)

\(P = \{E \rightarrow E + T | T, T \rightarrow T * F | F, F \rightarrow (E) | i \}\)

推导的符号暂时定义为 \(\Rightarrow\)

句型:如果 \(G[E]\) 满足 \(E \Rightarrow^* u\),那么 \(u\) 就是文法 \(G[E]\) 的一个句型

句子:如果 \(G[E]\) 满足 \(E \Rightarrow^* u\) 并且 \(u \in V_T^*\),那么 \(u\) 就是文法 \(G[E]\) 的一个句子

浅显的理解,句型等于所有在推导中可能出现的串,句子等于所有推导最后一步的串

语言:文法 \(G[E]\) 的语言是所有句子的集合,在描述时可以写成 \(V_T^*\) 的形式

语法树:满足下面条件的树

  • 每个结点都用一个标记 \(l\) 表示,\(l \in V_N \cup V_T\)

  • 树根的标记是起始符号

  • 非叶子节点的标记必然是非终结符号

  • 父子关系必然和一条产生式规则相对应

最左(右)推导:如果在某个推导过程中的任何一步直接推导αβ中,都是对符号串α的最左(右)非终结符号进行替换,则称其为最左(右)推导

如果产生式满足 \(U \rightarrow xUy\),那么为规则递归(直接递归)

如果产生式满足 \(U \Rightarrow^* xUy\),那么为间接递归,同样地,可以定义左右递归

如果两个文法产生的语言是相同的,则称这两个文法是等价文法

结合性体现在文法是左递归还是右递归,优先级则体现在出现在语法树的位置

如果文法\(G\)的某个句子存在两棵(包括两棵)以上不同的语法树(即有两个不同的最左/最右推导),则称该文法是二义性文法

二义性问题是不可判定的

正向标记法可以推导出所有可达的非终结符号,反向标记法可以推导出所有可推导出终结符号串的非终结符号

有穷自动机

正则文法、正则表达式、有穷自动机相互等价

确定型有穷自动机 \(\text{DFA} = \{Q, \Sigma, t, q_0, F \}\), $t: Q \times \Sigma \rightarrow Q $

非确定型有穷自动机 \(\text{NFA} = \{Q, \Sigma, t, Q_0, F \}\), \(t: Q \times \Sigma^* \rightarrow 2^Q\)

被一个有穷自动机接受的符号串的集合称为这个自动机的语言 \(L(\text{DFA})\)

如果两个有穷自动机 \(A_1, A_2\) 满足 \(L(A_1) = L(A_2)\),那么它们是等价的

DFA 和 NFA 在表达能力上是等价的:

  1. \(\text{DFA} \Rightarrow \text{NFA}\):DFA 天然的是一种 NFA,显然成立

  2. \(\text{NFA} \Rightarrow \text{DFA}\)

TBD

从证明过程中容易得出 NFA 向 DFA 转换的方法:

定义 \(\epsilon\) 闭包:\(p' \in \epsilon -Closure(p)\) 当且仅当 \(p\) 通过若干条 \(\epsilon\) 的转移边后可以到达 \(p'\)

通过 \(\epsilon\) 闭包,可以把转移函数 \(t\) 改写成 \(t: 2^Q \times \Sigma \rightarrow 2^Q\),并且只在必要的时刻引入状态的集合

这样构造出的 DFA 可以化简。对于两个状态集合 \(P, Q\),如果对于任意的符号 \(k \in \Sigma\),都有 \(t(P, \Sigma) = t(Q, \Sigma) = R\),那么它们可以合并;换言之,如果两个状态到达了不同的子集中,那么它们就应该从一个状态集合中分离。

通过正则文法可以构造出一个DFA,使得二者能接受的符号串集合相同。同理,通过DFA可以构造出一个正则文法。

对于 \(U \rightarrow aW\),可以构造状态及其转移边 U --a--> W;对于 \(U \rightarrow Wa\),可以构造 W --a--> U,反之亦然。

通过正则表达式也可以构造出一个等价的DFA。正则表达式有递归定义:\((e), e_1e_2, e_1|e_2, e^*\)。通过正则表达式直接构造DFA比较麻烦,可以先构造出对应的GNFA在转换为等价DFA。

通过DFA构造出对应的正则表达式需要不断地删去状态。可以以一个最小状态机为例。TBD。

递归下降语法分析

构建一个语法分析器最方便的方法是递归下降。

自上而下语法分析

递归下降会遇到两个问题:

  1. 公共前缀:这需要预处理文法,提取不同的分支之间的公共前缀。

  2. 左递归导致的无穷递归:因为一般写出的递归下降方法是前序遍历的,我们会先处理左子树,如果有个左递归我们会不断进入左子树,不会接受任何的新字符。

公共前缀的处理比较简单,左递归难处理一些:

  1. 直接左递归:对于产生式 \(A \rightarrow A\alpha_1|A\alpha_2|...|\beta_1|\beta_2\),引入新的符号 \(A'\) 使得串变为 \(A \rightarrow \beta_1A'|\beta_2A'|...\) \(A' \rightarrow \alpha_1A' | ...\),容易证明它们等价

  2. 间接左递归:通过代换把间接左递归转换为直接左递归的形式

递归下降的其他问题是:

  1. 不断回溯效率太低

  2. 代码本身和文法深度耦合(虽然你可以构建一个通过文法自动构建解析程序的程序)

因此我们引入两个新的集合防止我们做出错误的抉择:

\(\text{FIRST}\) 集合:\(FIRST(A)\) 表示符号 \(A\) 可以推导出的所有串的首部终结符号的集合,这决定了通过某个符号我们进入哪一个解析程序

\(\text{FOLLOW}\) 集合:\(FOLLOW(A)\) 表示文法的所有句型中,可能紧跟着非终结符号 \(A\) 的所有终结符号的集合,这决定了我们是否结束当前的解析程序进入下一个解析程序

想法是好的,但是大家发现容易执行坏了:

  1. 如果一个产生式的多个右部的 \(FIRST\) 集合相交,且无法通过提取公共前缀的方式解决,那么我们还是会进入错误的解析程序

  2. 如果对于 \(U \rightarrow \alpha | \epsilon\)\(FOLLOW(U)\)\(FIRST(\alpha)\) 相交,那么我们就不知道是进入新的解析程序还是退出(大家给它起了个名字叫移入归约冲突)

因此,不满足上面两个条件的文法被称为 LL(1) 文法,可以使用这两个集合改进解析方法

下面需要给这两个“想法”下形式化定义:

后面又引入了一个集合 \(SELECT\) 用来统一判断某个文法是否是 LL(1) 的,此外 \(SELECT\) 的元素还是一种“程序接下来要做什么”的标识

下面需要把上面的形式化想法转化为实践,考虑利用它们构建一个 LL(1) 分析算法。我们引入一个分析表把集合转化成状态与对应的动作,再引入符号栈和状态栈保存状态(下推自动机):

自下而上语法分析

相比于自上而下的语法分析,自下而上的语法分析不发源于递归下降的想法,而是自下而上的构建语法树,不过一样面临“选择”问题。选择什么时候移入或归约。

首先给出几个定义:

  • 短语:对于文法 \(G[E]\)和它的一个句型 \(xuy\),如果有 \(E \rightarrow xUy\) 满足 \(U \Rightarrow^* u\),那么 \(u\) 就是一个短语。直观的说,短语代表了那些能被规约的东西。

  • 直接短语:对于文法 \(G[E]\)和它的一个句型 \(xuy\),如果有 \(E \rightarrow xUy\) 满足 \(U \Rightarrow u\),那么 \(u\) 就是一个短语。直观的说,短语代表了那些直接一步规约的东西。

  • 句柄:句型的最左直接短语称为句柄

从定义不难看出,不断地对某个句型的句柄进行一次归约,那么最终能归约到开始符号

对于任意一个长度为 \(k\) 的句柄,它必须经历 \(k\) 次移入和 \(1\) 次归约,如果用一个状态机去描述的话,一共有 \(k + 1\) 个状态

又因为句柄是最左直接短语,不难看出某个句柄必然对应一个产生式的右部,产生式的数量和长度都是有限的,那么所有状态的数量也是有限的。我们用一种记号描述状态:

所有的这种被描述的状态称为 LR(0) 项(term)

显然,只利用这些状态以及状态之间的转移关系只能构建出一个 DFA,不能构建出一个 PDA,也无法进行语法分析。因此同样地需要引入栈。这里还有一个常考的定义:

活前缀:活前缀包含了句柄识别到当前时刻的历史,并逐步向后传递,最长的活前缀(可归约前缀)包含了句柄完全被识别的整个历史。

说人话,活前缀就是分析栈中可能出现的所有串

如果把每个 LR(0) 项都建成一个状态,不仅内存的开销很大,还因为有 \(\epsilon\) 边的存在导致这实际上是一个 NFA,因此使用一个 \(\epsilon -CLOSURE\) 来合并 NFA 中的一些结点。

哎写不动了

SLR 在 LR(0) 上做了一点小改进,避免了一些错误的归约状态(从而避免掉一些移入-归约冲突)

LR(1) 在 SLR 上做了更好的改进,通过在状态中引入一个符号,标识了在某些情况下的紧跟的终结符,增强了表达能力(可以理解成本来很泛泛的一个状态添加了一个细节),规避掉了更多的归约归约冲突

LALR(1) 只是对 LR(1) 在存储空间上的改进

语法制导翻译

从理论上也不是很漂亮,从实践上也不是很实用

可以为文法符号,引入一组属性 \(attr_1, attr_2, ...\),并定义程序的语义 \([[P]] = f(attr)\),在这个过程中,随着语法分析进行,每当使用一个产生式的时候就执行对应的语义动作

注释语法树:语法分析树的各个节点上标记了相应的属性值

语法制导定义 SDD:定义与文法符号相关联的属性集,定义与产生式关联的一组语义规则(程序片段),属性文法是没有副作用的 SDD

语法指导翻译方案:将程序片段附加到文法各个产生式右部的合适位置,后缀SDT指所有程序片段都在产生式的最后进行

属性本身可以被划分为综合属性和继承属性:

综合属性:从子结点的属性值计算得出的属性

继承属性:从父结点或兄弟结点继承的属性值

中间代码生成的核心在于处理文法、程序、中间代码三种不同的顺序关系。回填技术是处理这个问题的一个很好的算法。

怎么中间代码不讲 SSA 啊(

在涉及控制流语句的时候,我们会定义两个链表 falselisttruelist,用来保存所有涉及跳转的语句的真出口和假出口,这样在回填时就无需再次向下遍历整个语法树,通过链表中的位置就可以访问所有需要回填的位置

运行时刻环境

最近嵌套作用域原则:一个 Identifier 的作用域是个包含了这个 Identifier 的说明的最近的过程或函数

参数传递规则:

  1. 传地址:callee 接受到的是一个引用

  2. 传值:calle 接受到的是值本身

  3. 传结果:哎这个好抽象 被调用者的代码中使用形式参数时,访问的是形式参数的值单元,不修改实在参数;在结束调用返回时,修改实在参数

  4. 传名:常见在数组传递中 个人觉得和传地址区别不大 被调用者数据区中需要一个单元存放实参地址计算程序的入口地址

一个过程的数据区(也叫活动记录)是一个在执行中会使用的连续数据块,几乎所有数据区的大小都可以在编译期确定。这意味着在生成代码时,可以确定申请多少的栈区大小,从而组织整个存储空间。

我们假定栈向高地址增长。

写在考试前的总结

自动机理论与形式文法

  1. 注意辨别自己构建的是 NFA 还是 DFA

  2. 假设 DFA D 识别语言 L,如果要构造一个新的 DFA D' 识别语言 L 的补集,那么 D' 就是把 D 的接受状态和非接受状态翻转

  3. 在化简某个 DFA 时,先观察是否能划分为接受状态集和非接受状态集,如果能,那么不断地分割就可以得到最简 DFA,否则肉眼观察划分吧

  4. 题目中出现的 DFA 与正则表达式的对应,关注“连续的...”“以...为结尾”“以...开头”这样的特征

  5. 题目中可能出现不止一个左递归

  6. 对于 E -> E E | a 这种形式的文法,很容易构造其无二义性版本:E -> a E

  7. 一个包含加法和乘法的文法:

Expr -> Expr + Term | Term
Term -> Term * Factor | Factor
Factor -> (Expr) | id

可以通过消除左递归的方式得到它的另一个版本:

Expr -> Term Expr'
Expr' -> + Term Expr' | epsilon
Term -> Factor Term'
Term' -> * Factor Term' | epsilon
Factor -> (Expr) | id

这两个版本都很常用而且在龙书上出现过

自上而下的语法分析

  1. 自上而下的语法分析发源于递归下降法,First 集和 Follow 集的发明可以节省递归下降法在某些情况下的非必要搜索回溯步骤:

比如

E -> A B C
A -> a | epsilon
B -> b | epsilon
C -> c | epsilon

input: bc

对于输入串 bc,如果没有 First 集辅助选择,递归下降法就会错误的进入 A 对应的子程序,效率很低

为了更方便,大家干脆把所有的子程序进入的逻辑全都提取出来,搞成一张表,变成表格驱动的语法分析,用原输入串上的一个移动的指针代表分析进度

First 集合之间的冲突可以通过提取公共前缀解决,First 集合和 Follow 集合之间的冲突则需要谨慎文法设计

  1. LL(1) 分析表的构建:
  • 对于产生式 A -> a, 如果终结符 s 在 FIRST(a) 中,那么在表中的对应位置填入这个产生式

  • 对于产生式 A -> a, 如果 epsilon 在 FIRST(a) 中,那么对于 FOLLOW(a) 中的每个终结符,把产生式加入到它们对应的位置中

  1. LL(1) 算法的执行:

三个部分,符号栈、输入串、动作,符号栈的栈底放在右边更合适,动作包括“推导”和“匹配”

自下而上的语法分析

  1. 自下而上的语法分析来源于对句柄的认识,以及某个句柄必然出现在符号栈的顶端的这一推论

  2. LR 比 LL 更加强大

  3. 内核项:初始项以及点不在最左端的所有项

  4. SLR 分析表的构建:

  • 如果状态 A 通过一个终结符号 s 达到状态 B,那么在表中 ACTION 的对应位置填写移入 s,如果状态 A 通过一个非终结符号 E 到达状态 B,在么在表中 GOTO 的对应位置填写 B

  • 如果状态 A 里面有产生式 E -> e · 已经终结,那么对于 FOLLOW(E) 中的所有终结符号 s,在对应位置填写归约

  • 如果 S' -> S · 在状态中,那么 $ 置为 acc

  1. LR(1) 项目集的构建:
  • 从 S' -> S · , $ 开始,对于每个 A -> a · Bc, y,把 B -> · b, FIRST(cy) 加入状态中

  • 转移边不改变后面跟的符号

  1. LR(1) 分析表的构建:把 FOLLOW(E) 改编成后面跟的符号,后面跟的符号对移入动作没有贡献

  2. LALR 分析表的构建:在 LR(1) 项目集的基础上,合并掉那些具有相同核心的集合,在分析表中体现为合并掉两个状态行

  3. LR 语法分析算法的执行:

四个部分,状态栈,符号栈,输入串,动作,当执行一个归约动作时,符号栈和状态栈要弹出相同数量的元素,符号栈压入被归约的符号,状态栈则压入栈顶符号 GOTO 后的结果

语法制导翻译

posted @ 2025-05-26 21:44  sysss  阅读(44)  评论(0)    收藏  举报