编译原理

这是清华大学计算机系 2024 年秋季课程《编译原理》的期末复习笔记。

第一讲:课程概述

T 型图

T 型图用于表示一个编译程序,其中

  • S 是编译的源语言
  • T 是编译的目标语言
  • I 是用来实现编译程序的语言

T 型图

当 I 与 T 相同时,称为(M 机器上运行的)本地编译器;当 I 与 T 不同时,称为(M 机器上运行的)交叉编译器

多个 T 型图可以进行叠加,只需要保证相邻的 T-I 或者 I-S 相同即可。 L-(A)-B 加上 A-(M)-N 可以得到 L-(N)-B。

T 型图的叠加

例:将机器 A 上的语言 L 移植到机器 B。即,我们现在有 L-(A)-A,我们需要构造一个 L-(B)-B。

构造

在上图中,红框部分不是必要相邻的。

第二讲:词法分析

我认为很有意思的是以下题目:

已知 Java 中的“注释”以 /* 开始,以 */ 结束,在 /**/ 之间,除了 */ 序列外,可以出现任意字符。请构造一个正则表达式来匹配所有此类注释。

直接设计正则表达式是困难的;所以我先构造了一个 DFA,再转为正则表达式。

graph LR



style Start fill:none,stroke-width:0px



Start --> A(( ))

A --> |others| A

A --> |/*| B(( ))

B --> |$\Sigma$ - *| B

B --> |*| C(( ))

C --> |*| C

C --> |$\Sigma$ - * - /| B

C --> |/| D((( )))

可以写出正则表达式为: /((Σ{})+(Σ{,/}))/ /*\left(\left(\Sigma - \{*\}\right) + **^*\left(\Sigma - \{*, /\}\right)\right)^* **^*/

第三讲:自顶向下语法分析

带回溯的自顶向下语法分析过程中存在两类非确定性:

  1. 选择对哪一个非终结符进行展开;
  2. 如果选定的非终结符是多个产生式的左部,那么应该选择使用哪一个产生式。

对于第一类非确定性而言,我们可以考虑只允许最左推导或最右推导来避免;对于第二类非确定性而言,我们可以考虑使用自顶向下预测分析:向前查看确定树木的单词符号,然后确定应该选择哪一个产生式进行最左推导。

LL(1) 分析

LL(1) 是指,Left-to-right, Leftmost derivation with 1 token of look-ahead。

对于 CFG G={VN,VT,P,S}G = \{V_N, V_T, P, S\} 而言,我们一般想要研究 XG=VNVT{ε}{vAu,v is a suffix of u}. X_G = V_N \cup V_T \cup \{\varepsilon\} \cup \{v \mid A \to u, v \text{ is a suffix of } u\}.

First 集合

对于 CFG G={VN,VT,P,S}G = \{V_N, V_T, P, S\} 而言,α(VNVT)\alpha \in (V_N \cup V_T)^* 的 First 集合的定义为 First(α)={aα    aβ,aVT,β(VNVT),or a=ε when α    ε}. \text{First}(\alpha) = \{a \mid \alpha \overset{*}{\implies}a\beta, a \in V_T, \beta \in (V_N \cup V_T)^*, \text{or } a = \varepsilon \text{ when } \alpha \overset{*}{\implies} \varepsilon\}. 直观上来说,一个句型 α\alpha 若能够推导出另一个以终结符 aa 开头的句型,则 aFirst(α)a \in \text{First}(\alpha);若 α\alpha 可以推导出 ε\varepsilon,则 εFirst(α)\varepsilon \in \text{First}(\alpha)

步骤

  • 初始化:对于所有 xVT{ε}x \in V_T\cup\{\varepsilon\},都有 First(x)={x}\text{First}(x) = \{x\};对于其他 xx,都有 First(x)=\text{First}(x) = \varnothing
  • 迭代:重复以下步骤,直至所有 First 集合均不变。
    • 对于 y1y2yk{vAuP,v is a suffix of u}y_1y_2\cdots y_k \in \{v \mid A \to u \in P, v \text{ is a suffix of } u\} 而言,
      • 若能找到 i[1,k]i \in [1, k] 使得对于 j[1,i1]\forall j \in [1, i - 1] 都有 εFirst(yj)\varepsilon \in \text{First}(y_j),且 εFirst(yi)\varepsilon \notin \text{First}(y_i),则 First(y1y2yk)=(1jiFirst(yj)){ε}. \text{First}(y_1y_2\cdots y_k) = \left(\bigcup_{1 \le j \le i}\text{First}(y_j)\right) - \{\varepsilon\}.
      • 否则 First(y1y2yk)=1jkFirst(yj). \text{First}(y_1y_2\cdots y_k) = \bigcup_{1 \le j \le k}\text{First}(y_j).
    • 对所有 Ay1y2ykPA \to y_1y_2\cdots y_k \in P,置 First(A)First(A)First(y1y2yk). \text{First}(A) \gets \text{First}(A) \cup \text{First}(y_1y_2\cdots y_k).

关键在于找到第一个满足 εFirst(yi)\varepsilon \notin \text{First}(y_i)ii。它不能推导出 ε\varepsilon,说明从 y1y2yky_1y_2 \cdots y_k 推导出的句型的首字母:

  • 前面的 yjy_j,不全推导为 ε\varepsilon,得到 1ji1First(yj)\bigcup_{1 \le j \le i - 1}\text{First}(y_j)
  • 前面的 yjy_j 全推导为 ε\varepsilon,得到 First(yi)\text{First}(y_i)

yiy_i 的时候截断了——对于 yj,ji+1y_j, \forall j \ge i + 1 而言,它不会再对 y1y2yky_1y_2 \cdots y_k 的 First 集合产生贡献。

Follow 集合

对于 CFG G={VN,VT,P,S}G = \{V_N, V_T, P, S\} 而言,AVNA \in V_N 的 Follow 集合的定义为 Follow(A)={aS#    αAβ#,aFirst(β#),α,β(VNVT)}. \text{Follow}(A) = \{a \mid S\# \overset{*}{\implies} \alpha A \beta\#, a \in \text{First}(\beta\#), \alpha, \beta \in (V_N \cup V_T)^*\}.

直观上来说,若 GG 中存在一个包含子串 AaAa 的句型,则 aFollow(A)a \in \text{Follow}(A);若 GG 中存在一个以 AA 结尾的句型,则 #Follow(A)\# \in \text{Follow}(A)

显然,一定有 #Follow(S)\# \in \text{Follow}(S)

步骤

  • 初始化:Follow(S)={#}\text{Follow}(S) = \{\#\};对于其他的 AVNA \in V_N,都有 Follow(A)=\text{Follow}(A) = \varnothing
  • 迭代:重复以下步骤,直至所有 Follow 集合均不变。 若有 AαBβP,α,β(VNVT),BVNA \to \alpha B \beta \in P, \alpha, \beta \in (V_N \cup V_T)^*, B \in V_N,则
    • Follow(B)Follow(B)(First(β){ε})\text{Follow}(B) \gets \text{Follow}(B) \cup \left(\text{First}(\beta) - \{\varepsilon\}\right)
    • εFirst(β)\varepsilon \in \text{First}(\beta),则 Follow(B)Follow(B)Follow(A)\text{Follow}(B) \gets \text{Follow}(B) \cup \text{Follow}(A)

也就是说,对于所有产生式的右部的非终结符 BB 而言,设 BB 后面接着的是 β\beta,那么 Follow(B)\text{Follow}(B) 肯定要包含 First(β){ε}\text{First}(\beta) - \{\varepsilon\}。另外,如果 εFirst(β)\varepsilon \in \text{First}(\beta),这说明 AA 可以推导出以 BB 为后缀的句型(此时让 β    ε\beta \overset{*}{\implies}\varepsilon)。那么,AA 后面能立即跟着的终结符,也能跟在 BB 的后面,所以 Follow(B)\text{Follow}(B) 还需要包含 Follow(A)\text{Follow}(A)

预测集合 PS

对于 CFG G={VN,VT,P,S}G = \{V_N, V_T, P, S\} 而言,AαPA \to \alpha \in P 的预测集合的定义为:

  • εFirst(α)\varepsilon \notin \text{First}(\alpha),则 PS(Aα)=First(α)\text{PS}(A \to \alpha) = \text{First}(\alpha)
  • εFirst(α)\varepsilon \in \text{First}(\alpha),则 PS(Aα)=(First(α){ε})Follow(A)\text{PS}(A \to \alpha) = (\text{First}(\alpha) - \{\varepsilon\}) \cup \text{Follow}(A)

直观上来说,PS(Aα)\text{PS}(A \to \alpha) 中的元素表示的是 LL(1) 分析过程中 AA 能和哪些终结符匹配:要么是匹配不含 ε\varepsilonFirst(α)\text{First}(\alpha),要么是 α    ε\alpha \overset{*}{\implies}\varepsilon 后匹配 Follow(A)\text{Follow}(A)

GG 是 LL(1) 的,当且仅当对于 GG 中任意两个有相同左部的产生式 AαA \to \alphaAβA \to \beta,都满足 PS(Aα)PS(Aβ)=\text{PS}(A \to \alpha) \cap \text{PS}(A \to \beta) = \varnothing

预测分析表

如果 AαPA \to \alpha \in P,则在 SSPS(Aα)\text{PS}(A \to \alpha) 列的单元中写下 α\alpha

非终结符\终结符aabbccdd#\#
SSSAaSS \to AaSSBbSS \to BbSSBbSS \to BbSSdS \to d
AAAaA \to a
BBBεB \to \varepsilonBcB \to c

表驱动 LL(1) 分析程序

  • 初始时栈中仅含 #\#。然后将文法初始符号 SS 入栈。
  • 迭代:重复以下步骤,直至栈顶为 #\# 且串当前字符也为 #\#
    • 若栈顶为非终结符,则弹出该终结符,依据串当前字符,在预测分析表中找到相应的产生式,从上到下将产生式右部放置于栈中。
    • 若栈顶为终结符,则检查是否与串当前字符匹配。

消除左递归

消除直接左递归

对于文法 PPα1Pα2Pαmβ1β2βn P \to P\alpha_1 \mid P\alpha_2 \mid \cdots \mid P\alpha_m \mid \beta_1 \mid \beta_2 \mid \cdots \mid \beta_n 而言,可以消除直接左递归,将产生式改写为: Pβ1Qβ2QβnQQα1Qα2QαmQε \begin{align*} P & \to \beta_1Q \mid \beta_2Q \mid \cdots \mid \beta_n Q \\ Q & \to \alpha_1Q \mid \alpha_2Q \mid \cdots \mid \alpha_mQ \mid \varepsilon \end{align*}

一定不要忘记最后的 ε\varepsilon

消除间接左递归

假设所有的非终结符有排序:A1,A2,,AkA_1, A_2, \cdots, A_k,按照以下步骤消除所有的间接左递归:

  • 对于每个 AiA_i,考虑位于它前面的 Aj(1j<i)A_j(1 \le j < i)。用 Aiα1rα2rαtrA_i \to \alpha_1r \mid \alpha_2r \mid \cdots \mid \alpha_tr 来反复替代 AiAjrA_i \to A_j r 的产生式,其中 Ajα1α2αtA_j \to \alpha_1 \mid \alpha_2 \mid \cdots \mid \alpha_t
  • 消除关于 AiA_i 的直接左递归。

例:设文法 G[S]G[S]SPQaPQSbQSPc \begin{align*} S & \to PQ \mid a \\ P & \to QS \mid b \\ Q & \to SP \mid c \end{align*} 试变换该文法,得到一个等价的不含左递归的文法。假设非终结符排序为 S,P,QS, P, Q

顺序为:PPSS 替换,QQSS 替换,QQPP 替换。

第一轮对 SS 进行操作后(实际上未发生变化)为: SPQaPQSbQSPc \begin{align*} S & \to PQ \mid a \\ P & \to QS \mid b \\ Q & \to SP \mid c \end{align*}

第二轮对 PP 进行操作后(实际上未发生变化)为: SPQaPQSbQSPc \begin{align*} S & \to PQ \mid a \\ P & \to QS \mid b \\ Q & \to SP \mid c \end{align*}

第三轮对 QQ 进行操作后(先替换 SS,再替换 PP)为:

  • 先替换 SS SPQaPQSbQPQPaPc \begin{align*} S & \to PQ \mid a \\ P & \to QS \mid b \\ Q & \to PQP \mid aP \mid c \end{align*}
  • 再替换 PP SPQaPQSbQQSQPbQPaPc \begin{align*} S & \to PQ \mid a \\ P & \to QS \mid b \\ Q & \to QSQP \mid bQP \mid aP \mid c \end{align*}
  • 再消除直接左递归 SPQaPQSbQbQPRaPRcRRSQPRε \begin{align*} S & \to PQ \mid a \\ P & \to QS \mid b \\ Q & \to bQPR \mid aPR \mid cR \\ R & \to SQPR \mid \varepsilon \end{align*}

提取左公因子

对于文法 Pαβ1αβ2αβmγ1γ2γn P \to \alpha\beta_1 \mid \alpha\beta_2 \mid \cdots \mid \alpha\beta_m \mid \gamma_1 \mid \gamma_2 \mid \cdots \mid \gamma_n 而言,可以提取左公因子,将产生式改写为: PαQγ1γ2γnQβ1β2βm \begin{align*} P & \to \alpha Q \mid \gamma_1 \mid \gamma_2 \mid \cdots \mid \gamma_n \\ Q & \to \beta_1 \mid \beta_2 \mid \cdots \mid \beta_m \end{align*}

第四讲:符号表

开作用域和闭作用域

  • 该点作用域为当前作用域
  • 当前作用域与包含它的程序单元所构成的作用域称为开作用域
  • 不属于开作用域的作用域称为闭作用域

单符号表与多符号表

pascal
 1const a = 25;
 2
 3var x, y; // (1)
 4
 5procedure p;
 6
 7    var z;
 8
 9    begin
10
11        ...
12
13    end;
14
15procedure r;
16
17    var x, s; // (2)
18
19    procedure t;
20
21        var v, x, y; // (3)
22
23        begin
24
25            ...
26
27            end;
28
29    begin
30
31        ... // here
32
33        end;
34
35begin
36
37    ...
38
39end;
40

对于单符号表而言,所有的嵌套的定义域共用一个全局符号表。此时符号表中有 a、(2) 处的 x、(1) 处的 yprxst

对于多符号表而言,每个作用域都有各自的符号表,用一个栈来维护。此时符号表的组织为:

栈中位置作用域符号集合开/闭
栈底全局作用域axypr开作用域
栈顶过程 r 的作用域xst开作用域

不在符号表中的作用域有:

栈中位置作用域符号集合开/闭
(不在栈中)过程 p 的作用域z闭作用域
(不在栈中)过程 t 的作用域 3vxy闭作用域

第五讲:自底向上语法分析

短语、直接短语、句柄

直观上来说,

  • 短语是指分析树中非叶节点对应的果实;
  • 直接短语是指高度(注意:高度是指到子树中最远叶节点的距离,而非深度)为 11 的节点对应的果实;
  • 句柄是指分析树中最左侧的直接短语。

例:已知文法 G[S]G[S]SABAaAεBbbB \begin{align*} S & \to AB \\ A & \to aA \mid \varepsilon \\ B & \to b \mid bB \end{align*} 试指出句型 aaAbaaAb 的全部短语、直接短语、句柄。

画出 aaAbaaAb 的分析树如下:

graph TD



S[S] --- A1[A]

S --- B[B]

A1 --- a1[a]

A1 --- A2[A]

B --- b[b]

A2 --- a2[a]

A2 --- A3[A]



classDef yellow fill:#FF0;

class A2,B yellow

每个非叶节点对应果实对应一个短语,所以所有的短语如下: aaA,aA,b,aaAb aaA, aA, b, aaAb 高度为 11 的为非叶节点(图中高亮的节点)对应直接短语: aA,b aA, b 最左侧的直接短语为句柄: aA aA 如果分析树不唯一,则短语、直接短语和句柄是所有分析树对应结果的并集

移进-归约分析

移进-归约分析借助一个栈(称为下推栈或分析栈)来实现,拥有两种动作:移进(Shift)和归约(Reduce)。移进是指从输入序列中将一个单词符号移入分析栈,归约是指按照确定的方式对位于分析栈栈顶的短语进行归约。

移进-归约分析中的冲突

  • 移进-归约冲突:不能确定下一步应该移进,还是应该归约。
  • 归约-归约冲突:不能确定下一步应该对栈顶的哪一个短语进行归约。

LR 分析方法

LR 是指,Left-to-right, Rightmost derivation。在 LR 分析中,归约是对栈顶句柄进行归约。在设计 LR 分析程序时,通常是做法是在分析栈中存放分析引擎的当前状态,这样的分析栈,我们称之为状态栈

ACTION 表和 GOTO 表

  • ACTION[k,a]=si\text{ACTION}[k, a] = \text{s}iShift:将状态 ii 移进栈顶,且输入串指针指向下一输入符号。
  • ACTION[k,a]=rj\text{ACTION}[k, a] = \text{r}jReduce:按第 jj 条产生式归约。
  • GOTO[i,A]=j\text{GOTO}[i, A] = j:若按照 AαA \to \alpha 进行归约,则需要将栈顶的 α|\alpha| 个状态弹出,此时栈顶状态为 ii不将其弹出,再将状态 jj 移进栈顶。

如果同时含有状态栈和符号栈,则需要

  • ACTION[k,a]=si\text{ACTION}[k, a] = \text{s}iShift:将输入符号也移进符号栈栈顶。
  • ACTION[k,a]=rj\text{ACTION}[k, a] = \text{r}jReduce:按第 jj 条产生式修改符号栈栈顶的符号。
  • GOTO[i,A]=j\text{GOTO}[i, A] = j:此时将符号 AA 伴随着状态 jj 移进栈顶。

LR(0) 分析

LL(1) 是指,Left-to-right, Leftmost derivation with 0 token of look-ahead。

增广文法

对于 G[S]G[S] 而言,构造 G[S]G[S'],添加 SSS'\to S 使得开始符号不会出现在任何产生式的右部。可以证明,G[S]=G[S]G[S'] = G[S]

活前缀

对于 CFG G={VN,VT,P,S}G = \{V_N, V_T, P, S\} 而言,若 S    rmαAwS \overset{*}{\underset{\text{rm}}{\implies}} \alpha A wA    βA \implies \beta,其中 α,β(VNVT),wVT\alpha, \beta \in (V_N \cup V_T)^*, w \in V_T^*,即 β\beta 是右句型 αβw\alpha\beta w 的一个相对于非终结符 AA 的句柄,则 αβ\alpha\beta 的任何前缀 γ\gamma 都是文法 GG活前缀

直观上来说,γ(VNVT)\gamma \in (V_N \cup V_T)^* 是活前缀,当且仅当存在 wL(G)w \in L(G),使得 γ\gamma 恰好是针对 ww 的分析过程中,某个时刻分析栈上的符号串#\# 除外)。活前缀是某个右句型的前缀,且这个前缀的右侧不超过该句型的某个句柄。它们的关系如下:

  • 活前缀已经含有该句柄的全部符号:表明该句柄对应的产生式 AαA \to \alpha 的右部 α\alpha 已经出现在栈顶。
  • 活前缀只含该句柄的一部分符号:表明该句柄对应的产生式 Aα1α2A \to \alpha_1\alpha_2 的右部的子串 α1\alpha_1 已经出现在栈顶,期待从输入串中看到可以由 α2\alpha_2 推导出的字符串。
  • 活前缀不含有该句柄的任何符号:此时期待从输入串中看到该句柄对应的产生式 AαA \to \alpha 的右部所推导出的符号串。

LR(0) 有限状态机

项目

项目(item)是指由产生式 AxyzA \to xyz 得到的形如 Ax.yzA \to x.yz 的式子,圆点标志着已分析过的串与该产生式匹配的位置。若 AεA \to \varepsilon,则唯一的 LR(0) 项目记作 A.A \to .

根据圆点所在的位置和圆点后是终结符、非终结符或为空,把项目分为以下几种:

  • 移进项目:形如 Aα.aβA \to \alpha.a\beta,其中 aVT,α,β(VNVT)a \in V_T, \alpha, \beta \in (V_N \cup V_T)^*
  • 待约项目:形如 Aα.BβA \to \alpha.B\beta
  • 归约项目:形如 Aα.A \to \alpha.
  • 接受项目:形如 SSS' \to S

闭包

LR(0) 有限状态机的每个状态是某个 LR(0) 项目集合 II 的闭包 CLOSURE(I)\text{CLOSURE}(I)。构造闭包的算法如下:

  1. JIJ \gets I
  2. 迭代:重复以下步骤,直至 JJ 不再变化: 对于 JJ 中的每个项目 Aα.BβA \to \alpha.B\betaGG 中的每个产生式 BγB \to \gamma,进行 JJ{B.γ}J \gets J \cup\{B \to .\gamma\}
  3. CLOSURE(I)=J\text{CLOSURE}(I) = J

也就是对于 II 中的每个项目而言,把紧邻圆点右侧的非终结符对应的所有产生式(将右部放在圆点右侧)都加进 CLOSURE(I)\text{CLOSURE}(I)

初态

初态为 CLOSURE({SS})\text{CLOSURE}(\{S' \to S\})

转移函数

转移函数为 GO(I,X)=CLOSURE(J), \text{GO}(I, X) = \text{CLOSURE}(J), 其中 II 为某个由 LR(0) 有限状态机的状态,XVNVTX \in V_N \cup V_TJ={AαX.βAα.XβI}J = \{A \to \alpha X.\beta \mid A \to \alpha.X\beta \in I\}。也就是说,JJ 中的产生式,是 II 中的产生式圆点向右移动一个文法符号 XX 得到的。而这条转移边的边权,即为 XX

所有状态的集合

求出 LR(0) 有限状态机的所有状态的集合 CC 的算法如下:

  • 初始化 CCLOSURE({SS})C \gets \text{CLOSURE}(\{S' \to S\})
  • 迭代:重复以下步骤,直至 CC 不再变化: 对于 CC 中的每个项目集合 IIGG 的文法符号 XX,进行 CCGO(I,X)C \gets C \cup \text{GO}(I, X)

总结

使用该方法构造的 LR(0) 有限状态机可以看做是一个以 VNVTV_N \cup V_T 为字母表的(省略了死状态的)DFA。该 DFA 的语言是增广文法 G[S]G[S'] 的所有活前缀的集合。

LR(0) 分析表

对于 ACTION 表而言:

  • 若项目 Aα.aβA \to \alpha.a\beta 属于 IkI_kGO(Ik,a)=Ij\text{GO}(I_k, a) = I_j,则置 ACTION[k,a]=sj\text{ACTION}[k, a] = \text{s}j
  • 若项目 Aα.A \to \alpha. 属于 IkI_k,则对任意终结符 aa,置 ACTION[k,a]=rj\text{ACTION}[k, a] = \text{r}j,其中 AαA \to \alpha 是第 jj 个产生式(注意:SSS'\to S 是第 00 个产生式)。
  • 若项目 SS.S' \to S. 属于 IkI_k,则置 ACTION[k,#]=acc\text{ACTION}[k, \#] = \text{acc},表示接受。

对于 GOTO 表而言:

  • 如果 GO(Ik,A)=Ij\text{GO}(I_k, A) = I_j,则记 GOTO[k,A]=j\text{GOTO}[k, A] = j

终结符 aa 对应列为 ACTION[k,a]\text{ACTION}[k, a],非终结符 AA 对应列为 GOTO[k,A]\text{GOTO}[k, A]

状态dd++(())#\#EETT
0s3\text{s}3s4\text{s}412
1s6\text{s}6acc\text{acc}
2r2\text{r}2r2\text{r}2r2\text{r}2r2\text{r}2r2\text{r}2
3r4\text{r}4r4\text{r}4r4\text{r}4r4\text{r}4r4\text{r}4
4s3\text{s}3s4\text{s}452
5s6\text{s}6s7\text{s}7
6s3\text{s}3s4\text{s}48
7r3\text{r}3r3\text{r}3r3\text{r}3r3\text{r}3r3\text{r}3
8r1\text{r}1r1\text{r}1r1\text{r}1r1\text{r}1r1\text{r}1

值得注意的是,由于每次对 rj\text{r}j 的赋值都是“对任意终结符”而言的,所以,如果出现 rj\text{r}j,则是一行全部为 rj\text{r}j

LR(0) 文法

如果 LR(0) 分析表中各个表项均无多重定义,则称该文法为一个 LR(0) 文法。或者,从 LR(0) 有限自动机的角度来看,每个状态(项目闭包)均需要满足:

  • 不同时含有移进项目和归约项目(无移进-归约冲突)
  • 不含有两个或两个以上归约项目(无归约-归约冲突)

需要额外注意的是,SS.S'\to S.接受项目,并非归约项目,它不会和其他归约项目或移进项目产生冲突。

SLR(1) 分析

SLR(1) 是指,Simple Left-to-right, Rightmost derivation with 1 token of look-ahead。

向前查看一个符号,利用 Follow 集合,可解决部分冲突:

  • 如果每个状态的所有归约项中要归约的非终结符的 Follow 集合互不相交,则可以解决归约-归约冲突。
  • 如果每个状态的所有归约项的非终结符的 Follow 集合与该状态的所有移进项目要移进的符号集互不相交,则可以解决移进-归约冲突。

SLR(1) 分析表

只需要修改构造 SLR(1) 分析表中对于归约项目的构造部分:

  • 若项目 Aα.A \to \alpha. 属于 IkI_k,则对 aFollow(A)a \in \text{Follow}(A),置 ACTION[k,a]=rj\text{ACTION}[k, a] = \text{r}j,其中 AαA \to \alpha 是第 jj 个产生式(注意:SSS'\to S 是第 00 个产生式)。

此时就不一定会出现整行全为 rj\text{r}j 的情况了——rj\text{r}j 只会出现在 Follow(A)\text{Follow}(A) 对应的终结符列,而且一行中可以既有 rj\text{r}j 又有 si\text{s}i

SLR(1) 文法

如果 SLR(1) 分析表中各个表项均无多重定义,则称该文法为一个 SLR(1) 文法。或者,从 LR(0) 有限自动机的角度来看,每个状态(项目闭包)均需要满足:

  • 对该状态的任何项目 Aα.aβA \to \alpha.a\beta,不存在 Bγ.B \to \gamma.,使得 aFollow(B)a \in \text{Follow}(B),即不存在移进-归约冲突。
  • 对该状态的任意两个项目 Aα.A \to \alpha.Bβ.B \to \beta.,满足 Follow(A)Follow(B)=\text{Follow}(A) \cap \text{Follow}(B) = \varnothing,即不存在归约-归约冲突。

直观上来说,前者是说,对于一个终结符,要么归约它,要么依据这个终结符进行移进;后者是说,对于一个终结符,不存在两种对于它的规约。其实移进-归约和归约-归约并不一定冲突——它们只会在对于相同终结符有不同解释的时候才会冲突。

LR(1) 分析

LR(1) 是指,Left-to-right, Rightmost derivation with 1 token of look-ahead。

Follow 集合缩小了归约范围,但它仍然不精确:一个输入符号属于所归约非终结符的 Follow 集合,未必就是句柄后可以跟的符号。

LR(1) 有限状态机

项目

LR(1) 项目是在 LR(0) 项目的基础上增加一个终结符(称为向前搜索符)得到的,形如 Aα.β,a A \to \alpha.\beta, a 其中 Aα.βA \to \alpha.\beta 是一个 LR(0) 项目,aVT{#}a \in V_T \cup \{\#\}。向前搜索符表示产生式右端完全匹配后所允许在余留符号串中的首个终结符

形如 Aα.,aA \to \alpha., a 的 LR(1) 项目相当于 LR(0) 中的归约项目,但是只有当余留符号串的首个终结符是 aa 的时候才可以进行归约。

对于 Aα.β,a1Aα.β,a2Aα.β,am \begin{align*} A & \to \alpha.\beta, a_1 \\ A & \to \alpha.\beta, a_2 \\ \vdots \\ A & \to \alpha.\beta, a_m \\ \end{align*} 而言,可以简记为 Aα.β,a1/a2//amA \to \alpha.\beta, a_1/a_2/\cdots/a_m

闭包

LR(0) 有限状态机的每个状态是某个 LR(0) 项目集合 II 的闭包 CLOSURE(I)\text{CLOSURE}(I)。构造闭包的算法如下:

  1. JIJ \gets I
  2. 迭代:重复以下步骤,直至 JJ 不再变化: 对于 JJ 中的每个项目 [Aα.Bβ,a][A \to \alpha.B\beta, a]GG 中的每个产生式 BγB \to \gamma,进行 JJ{[B.γ,b]}J \gets J \cup\{[B \to .\gamma, b]\},这里 bFirst(βa)b \in \text{First}(\beta a)
  3. CLOSURE(I)=J\text{CLOSURE}(I) = J

也就是对于 II 中的每个项目而言,把紧邻圆点右侧的非终结符对应的所有产生式(将右部放在圆点右侧)都加进 CLOSURE(I)\text{CLOSURE}(I),这些产生式的向前搜索符是非终结符后的句型 β\beta 和向前搜索符 aa 构成的句型 βa\beta a 的 First 集合

初态

初态为 CLOSURE({[SS,#]})\text{CLOSURE}(\{[S' \to S, \#]\})

转移函数

转移函数为 GO(I,X)=CLOSURE(J), \text{GO}(I, X) = \text{CLOSURE}(J), 其中 II 为某个由 LR(1) 有限状态机的状态,XVNVTX \in V_N \cup V_TJ={[AαX.β,a][Aα.Xβ,a]I}J = \{[A \to \alpha X.\beta, a] \mid [A \to \alpha.X\beta, a] \in I\}。也就是说,JJ 中的产生式,是 II 中的产生式圆点向右移动一个文法符号 XX 得到的。而这条转移边的边权,即为 XX

所有状态的集合

求出 LR(1) 有限状态机的所有状态的集合 CC 的算法如下:

  • 初始化 CCLOSURE({[SS,#]})C \gets \text{CLOSURE}(\{[S' \to S, \#]\})
  • 迭代:重复以下步骤,直至 CC 不再变化: 对于 CC 中的每个项目集合 IIGG 的文法符号 XX,进行 CCGO(I,X)C \gets C \cup \text{GO}(I, X)

LR(1) 分析表

对于 ACTION 表而言:

  • 若项目 [Aα.aβ,b][A \to \alpha.a\beta, b] 属于 IkI_kGO(Ik,a)=Ij\text{GO}(I_k, a) = I_j,则置 ACTION[k,a]=sj\text{ACTION}[k, a] = \text{s}j
  • 若项目 [Aα.,b][A \to \alpha., b] 属于 IkI_k,则置 ACTION[k,b]=rj\text{ACTION}[k, b] = \text{r}j,其中 AαA \to \alpha 是第 jj 个产生式(注意:SSS'\to S 是第 00 个产生式)。
  • 若项目 [SS.,#][S' \to S., \#] 属于 IkI_k,则置 ACTION[k,#]=acc\text{ACTION}[k, \#] = \text{acc},表示接受。

对于 GOTO 表而言:

  • 如果 GO(Ik,A)=Ij\text{GO}(I_k, A) = I_j,则记 GOTO[k,A]=j\text{GOTO}[k, A] = j

LR(1) 文法

如果 LR(1) 分析表中各个表项均无多重定义,则称该文法为一个 LR(1) 文法。或者,从 LR(1) 有限自动机的角度来看,每个状态(项目闭包)均需要满足:

  • 对于每个状态,不能同时含有项目 [Aα.aβ,b][A \to \alpha.a\beta, b] 和项目 [Bγ.,a][B \to \gamma., a],即不存在移进-归约冲突;
  • 对于每个状态,不能同时含有项目 [Aα.,a][A \to \alpha., a] 和项目 [Bβ.,a][B \to \beta., a],即不存在归约-归约冲突。

LALR(1) 分析

LALR(1) 是指,Look-Ahead, Left-to-right, Rightmost derivation with 1 token of look-ahead。

芯和同芯状态

对于 LR(1) 项目 [Aα.aβ,b][A \to \alpha.a\beta, b] 而言,把 Aα.aβA \to \alpha.a\beta 称为它的。如果对于两个状态,它们各自所有项目的芯分别构成的集合是相同的,那么就称这两个状态为同芯状态

LALR(1) 有限状态机

对于 LR(1) 有限状态机中的两个同芯状态,由 GO 函数得到的针对任何符号的状态仍然是同芯的,因此,我们可以对 GO 函数进行修改。

合并 LR(1) 有限状态机中的同芯状态,结合修改后的 GO 函数,即得 LALR(1) 有限状态机

构造 LALR(1) 有限状态机并不需要先构造出 LR(1) 有限状态机,因为可以边构造边合并状态。

LALR(1) 分析表

LALR(1) 分析表的构造过程与 LR(1) 分析表的构造过程一样。

LALR(1) 文法

一个 LR(1) 文法,如果将其 LR(1) 有限状态机的同芯状态合并后所得到的 LALR(1) 有限状态机中的全部状态无归约-归约冲突,则该文法是一个 LALR(1) 文法

只需要检查归约-归约冲突的原因是,合并同芯状态不会产生新的移进-归约冲突(因为移进项目和归约项目一定不同芯——它们圆点的位置都不同),但会产生新的归约-归约冲突。

二义文法的 LR 分析

可以手动规定一些符号的优先级和结合性,使得文法分析可以进行下去。具体而言,可以手动规定某些移进-归约冲突时应该是移进还是归约,手动规定某些归约-归约冲突时应该按照哪个产生式进行归约。

LR 分析中的错误处理

将分析表中的每个空白项赋值为 ej\text{e}j,表示报第 jj 种错误,运行第 jj 种错误处理程序。

第六讲:语法制导的语义计算基础

属性文法

在文法 G[S]G[S] 的基础上,可以为文法符号关联有特定意义的属性,并为产生式关联对应的语义动作,这样的拓展后的文法称为属性文法SABC{if (A.num=B.num) and (B.num=C.num)then print(“Accepted!”)else print(“Refused!”)}AA1a{A.num:=A1.num+1}Aa{A.num:=1}BB1b{B.num:=B1.num+1}Bb{B.num:=1}CC1c{C.num:=C1.num+1}Cc{C.num:=1} \begin{align*} S & \to ABC & \{\text{if } (A.\text{num} = B.\text{num}) \text{ and } (B.\text{num} = C.\text{num}) \\ & & \text{then } \text{print}(\text{``Accepted!''}) \\ & & \text{else } \text{print}(\text{``Refused!''})\} \\ A & \to A_1 a & \{A.\text{num} := A_1.\text{num} + 1\} \\ A & \to a & \{A.\text{num} := 1\} \\ B & \to B_1b & \{B.\text{num} := B_1.\text{num} + 1\} \\ B & \to b & \{B.\text{num} := 1\} \\ C & \to C_1 c & \{C.\text{num} := C_1.\text{num} + 1\} \\ C & \to c & \{C.\text{num} := 1\} \\ \end{align*} 形如 b:=f(c1,c2,,ck)b := f(c_1, c_2, \cdots, c_k) 的称为语义动作,其中 ff语义函数。语义动作也可以只含一个函数,例如 f(c1,c2,,ck)f(c_1, c_2, \cdots, c_k)

形如 X.a:=Y.bX.a := Y.b 的语义动作称为复写规则

综合属性和继承属性

对于关联于产生式 AαBβA \to \alpha B\beta 的语义动作 X.v:=f()X.v := f(\cdots) 而言:

  • XXAA,则称 A.vA.v综合属性
  • XXBB,则称 B.vB.v继承属性

从分析树的角度来看,综合属性由子节点向父节点转移;继承属性由父节点向子节点转移。

在遍历分析树进行语义计算的时候,通常可以构造一个依赖图(依赖于别人结果的属性,指向被依赖的属性),如果这个图是一个 DAG(Directed Acyclic Graph,有向无圈图),则可以按照任意一个拓扑排序的顺序进行计算。

S-属性文法

仅包含综合属性的属性文法称为 S-属性文法

基于 S-属性文法的语义计算

基于 S-属性文法的语义计算是简单的,只需要每次对符号进行归约的时候,同时计算综合属性的值即可。

L-属性文法

一个属性文法称为 L-属性文法,当且仅当其中每一个产生式 AX1X2XnA \to X_1X_2 \cdots X_n,其每个语义动作所涉及的属性或者是综合属性,或者是某个 Xi(1in)X_i(1 \le i \le n) 的继承属性,而这个继承属性只能依赖于:

  1. XiX_i 左边的符号 X1,X2,,Xi1X_1, X_2, \cdots, X_{i-1} 的属性;
  2. AA 的继承属性。

可以看出,S-属性文法是一种特殊的 L-属性文法。直观上来说,属性信息按照以下方式进行传递:

L-属性文法

基于 L-属性文法的语义计算

c++
 1void visit(Node u) {
 2
 3    for (Node v : children(u)) { // 从左到右遍历 u 的每个子节点
 4
 5        v.calculate_INHERITED_attribute(); // 计算 v 的继承属性
 6
 7        visit(v);
 8
 9    }
10
11    u.calculate_SYNTHESIZED_attribute(); // 计算 u 的综合属性
12
13}
14

经过一遍深度优先搜索即可得到所有的综合属性和继承属性的值。

翻译模式

翻译模式在形式上类似于属性文法,但是它允许 {}\{\cdots\} 括起来的语义动作出现在产生式右端的任何位置,以此显式地表达属性计算的次序。

S-翻译模式

S-翻译模式仅涉及综合属性,通常将语义动作集合置于相应产生式的末尾。

基于 S-翻译模式的语义计算

假设语义栈由向量 vv 表示,归约前栈顶位置为 top\text{top},栈上第 ii 个位置所对应符号的综合属性值 xxv[i].xv[i].x 表示。为了明确表达语义栈上的操作,将 AXYZ{A.a:=f(X.x,Y.y,Z.z)} A \to XYZ \quad \{A.a:= f(X.x, Y.y, Z.z)\} 变换为 AXYZ{v[top2].a:=f(v[top2].x,v[top1].y,v[top].z)} A \to XYZ \quad \{v[\text{top} - 2].a := f(v[\text{top} - 2].x, v[\text{top} - 1].y, v[\text{top}].z)\}

注意:归约时,产生式右端从左到右在栈中是按照自顶到底的顺序排列的。

L-翻译模式

L-翻译模式既可以包含综合属性,又可以包含继承属性,但需要满足:

  1. 产生式右部某个符号的继承属性的计算必须位于该符号之前,且语义动作不访问位于该语义动作右侧的符号的属性,仅依赖于该语义动作左侧的符号的属性(如果依赖于产生式左部的符号,则只能依赖于它的继承属性)。
  2. 产生式左部非终结符的综合属性的计算只能在所用到的属性都已计算出来之后进行,通常将相应的语义动作置于产生式的末尾。

直观上来说,这是为了保证 DFS 遍历语法分析树的正确性。

递归下降语义计算程序

ParseX( ) 修改一下,以 XX 的继承属性作为形式参数,综合属性作为返回值(不妨设 XX 仅有一个综合属性)。

假设 B.val,S.valB.\text{val}, S.\text{val} 是综合属性,B.f,S.fB.f, S.f 是继承属性。则 S{B.f:=S.f}B{S1.f:=S.f+1}S1{S.val:=B.val+S1.val}Sε \begin{align*} S & \to \{B.f := S.f\} \quad B \quad \{S_1.f := S.f + 1\} \quad S_1 \quad \{S.\text{val} := B.\text{val} + S_1.\text{val}\}\\ S & \to \varepsilon \end{align*} 对应的语义计算子函数可以设计为:

cpp
 1float ParseS(int f) {
 2
 3    Sv;
 4
 5    if (lookahead == '0' || lookahead == '1') {
 6
 7        Bf = f;
 8
 9        Bv = ParseB(Bf);
10
11        S1f = f + 1;
12
13        S1v = ParseS(S1f);
14
15        Sv = Bv + S1v;
16
17    } else if (lookahead == '#') {
18
19        Sv = 0;
20
21    } else {
22
23        printf("Syntex error!");
24
25    }
26
27    return Sv;
28
29}
30

消除基础文法左递归

假设有如下翻译模式: AA1Y{A.a:=g(A1.a,Y.y)}AX{A.a:=f(X.x)} \begin{align*} A & \to A_1 Y & \{A.a := g(A_1.a, Y.y)\} \\ A & \to X & \{A.a := f(X.x)\} \end{align*} 消去关于 AA 的左递归,得: AXRRYRε \begin{align*} A & \to XR \\ R & \to YR \mid \varepsilon \end{align*} 再考虑语义动作,可以给 RR 一个综合属性 R.sR.s 和一个继承属性 R.iR.i,翻译模式可以变换为 AX{R.i:=f(X.x)}R{A.a:=R.s}RY{R1.i:=g(R.i,Y.y)}R1{R.s:=R1.s}Rε{R.s:=R.i} \begin{align*} A & \to X \quad \{R.i := f(X.x)\} \quad R \quad \{A.a := R.s\} \\ R & \to Y \quad \{R_1.i := g(R.i, Y.y)\} \quad R_1 \quad \{R.s := R_1.s\} \\ R & \to \varepsilon \quad \{R.s := R.i\} \end{align*} 消除基础文法左递归

基于 L-翻译模式的自底向上语义计算

如果语义动作集中仅包含综合属性的访问或赋值,则只需要处理好嵌入在产生式中间的语义动作。对此,一种处理方法是:引入新的非终结符 NN 和产生式 NεN \to \varepsilon;把嵌入在产生式中间的语义动作集用 NN 代替,并把该动作集放在产生式 NεN \to \varepsilon 后面。

例如,对于下列翻译模式: ETRR+T{print(“+”)}R1RT{print(“-”)}R1RεTnum{print(num.val)} \begin{align*} E & \to TR \\ R & \to +T \quad \{\text{print}(\text{``+''})\} \quad R_1 \\ R & \to -T \quad \{\text{print}(\text{``-''})\} \quad R_1 \\ R & \to \varepsilon \\ T & \to \underline{\text{num}} \quad \{\text{print}(\underline{\text{num}}.\text{val})\}\\ \end{align*} 可以变换为 ETRR+TMR1RTNR1RεTnum{print(num.val)}Mε{print(“+”)}Nε{print(“-”)} \begin{align*} E & \to TR \\ R & \to +TMR_1 \\ R & \to -TNR_1 \\ R & \to \varepsilon \\ T & \to \underline{\text{num}} \quad \{\text{print}(\underline{\text{num}}.\text{val})\}\\ M & \to \varepsilon \quad \{\text{print}(\text{``+''})\} \\ N & \to \varepsilon \quad \{\text{print}(\text{``-''})\} \end{align*}

然而,如果语义动作集中有继承属性的访问或赋值,情况则会复杂一些。由于在自底向上的语义计算过程中,只有综合属性会出现在语义栈上,所以继承属性的访问最终要落实到对某个综合属性值的访问。

例如,对于下列翻译模式: SaA{C.i:=f(A.s)}C S \to aA \quad \{C.i := f(A.s)\} \quad C 其中 A.sA.sAA 的综合属性,C.iC.iCC 的继承属性。此时 A.sA.s 固然在语义栈中——但是 f(A.s)f(A.s) 并不在。所以需要引入新的非终结符 MM,将以上翻译模式改写为: SaA{M.i:=A.s}M{C.i:=M.s}CMε{M.s=f(M.i)} \begin{align*} S & \to aA \quad \{M.i: = A.s\} \quad M \quad \{C.i := M.s\} \quad C \\ M & \to \varepsilon \quad \{M.s = f(M.i)\} \end{align*} 可以把 MM 看做成一个函数,它负责计算 M.sf(M.i)M.s \gets f(M.i),将一个继承属性转换为了综合属性,并存放在语义栈上。

但是,此时还需要再解决一个问题:避免不一致访问。考虑以下翻译模式: SaA{C.i:=A.s}CSbAB{C.i:=A.s}CCc{C.s:=g(C.i)} \begin{align*} S & \to aA \quad \{C.i := A.s\} \quad C \\ S & \to bAB \quad \{C.i := A.s\} \quad C \\ C & \to c \quad \{C.s := g(C.i)\} \end{align*}

这里出现的问题是,当使用 CcC \to c 进行归约的时候:

  • C.iC.i 要么位于次栈顶,这是从第一个产生式得来的,CC 位于 top\text{top}AA 位于 top1\text{top} - 1。此时 A.sA.sv[top1].sv[\text{top} - 1].s,位于次栈顶
  • C.iC.i 要么位于次次栈顶,这是从第二个产生式得来的,CC 位于 top\text{top}BB 位于 top1\text{top} - 1AA 位于 top2\text{top} - 2。此时 A.sA.sv[top2].sv[\text{top} - 2].s,位于次次栈顶

这样就不知道在显式写出语义栈访问的时候,最后一句产生式对应的语义动作究竟是 v[top].s:=g(v[top1].s)v[\text{top}].s := g(v[\text{top} - 1].s) 还是 v[top].s:=g(v[top2].s)v[\text{top}].s := g(v[\text{top} - 2].s) 了。

注意:这里赋值式的左式为 v[top].sv[\text{top}].s,是因为第三个产生式中归约前的 cc 位于 top\text{top},则 CC 位于 top\text{top}。如果是 CεC \to \varepsilon,则归约后 CC 应位于 top+1\text{top} + 1,需要写成 v[top+1].s:=v[\text{top} + 1].s := \cdots

一种可行的做法是引入新的非终结符 MM,将以上翻译模式改写为 SaA{C.i:=A.s}CSbAB{M.i:=A.s}M{C.i:=M.s}CCc{C.s:=g(C.i)}Mε{M.s:=M.i} \begin{align*} S & \to aA \quad \{C.i := A.s\} \quad C \\ S & \to bAB \quad \{M.i := A.s\} \quad M \quad \{C.i := M.s\} \quad C \\ C & \to c \quad \{C.s := g(C.i)\} \\ M & \to \varepsilon \quad \{M.s := M.i\} \end{align*}

这样就将 C.iC.i 的位置统一于 top1\text{top} - 1 处。

第七讲:静态语义分析与中间代码生成

静态语义分析

静态语义分析最基本的工作是检查程序结构的一致性或完整性,例如:

  • 控制流检查(例如 break 语句必须在循环中等)
  • 唯一性检查(例如标识符、枚举类型的元素是唯一的等)
  • 名字的上下文相关性检查(例如变量在使用前必须经过声明、在外部不能访问私有变量等)
  • 类型检查(例如 void f(int *a); 的函数在调用时不允许 f(10); 等)

类型检查

{} 需要加引号以区别于元符号: S‘{’{S1.inloop:=S.inloop}S1‘}’{S.type:=S1.type} S \to \text{`\{'} \quad \{S_1.\text{inloop}:=S.\text{inloop}\} \quad S_1 \quad \text{`\}'} \quad \{S.\text{type} := S_1.\text{type}\}

中间代码生成

综合属性 D.widthD.\text{width}P.widthP.\text{width} 表示所申明变量所占的字节数。继承属性 P.offsetP.\text{offset}D.offsetD.\text{offset} 表示相应程序单元起始位置在全局空间中的偏移量。

综合属性 id.place\underline{\text{id}}.\text{place} 表示相应的名字对应的存储位置,这更像是“引用”,而不是指针或地址。综合属性 E.codeE.\text{code} 表示对 EE 进行求值的 TAC 语句序列。

语义函数 gen\text{gen} 的结果是生成一条 TAC 语句。语义函数 newtemp\text{newtemp} 的作用是在符号表中新建一个从未使用过的名字,并返回该位置的存储位置。

\mid\mid 是 TAC 语句序列之间的链接运算。

ER{E.place:=newtemp;E.code:=R.codegen(E.place  :=  R.base  [  R.offs  ])} E \to R \quad \{E.\text{place} := \text{newtemp}; E.\text{code}:=R.\text{code} \mid\mid \text{gen}(E.\text{place} \;\text{`}:=\text{'}\;R.\text{base}\;\text{`}[\text{'}\;R.\text{offs}\;\text{`}]\text{'})\}

继承属性 E.trueE.\text{true}E.falseE.\text{false} 分别代表 EE 为真核假时控制要转移到的程序位置,即标号。

拉链

至此,已经有 L-翻译模式可以将布尔表达式和控制语句翻译为 TAC 语句序列;现在,介绍一种可以处理同样问题的 S-翻译模式。这种翻译模式用到以下树型和语义函数:

  • 综合属性 E.truelistE.\text{truelist}(真链):链表中的元素表示一系列跳转语句的地址,这些跳转语句的目标语句标号是体现 EE 为真的标号。
  • 综合属性 E.falselistE.\text{falselist}(假链):链表中的元素表示一系列跳转语句的地址,这些跳转语句的目标语句标号是体现 EE 为假的标号。
  • 综合属性 S.nextlistS.\text{nextlist}(next 链):链表中的元素表示一系列跳转语句的地址,这些跳转语句的目标语句标号是在执行序列中紧跟在 SS 之后的下一条 TAC 语句的地址。
  • 综合属性 N.nextlistN.\text{nextlist}:仅含一个语句地址的链表,对应于处理到 NN 时的跳转语句。
  • 综合属性 S.breaklistS.\text{breaklist}(break 链):链表中的元素表示一系列跳转语句的地址,这些跳转语句的目标语句标号是跳出直接包围 SS 的 while 语句后的下一条 TAC 语句的地址。
  • 综合属性 M.gotostmM.\text{gotostm} 中记录处理到 MM 时下一条待生成语句的标号。
  • 语义函数 makelist(i)\text{makelist}(i),创建只有一个节点 ii 的表,对应于一条跳转语句的地址。
  • 语义函数 merge(p1,p2)\text{merge}(p_1, p_2):链接两个链表 p1,p2p_1, p_2,返回结果链表。
  • 语义函数 backpatch(p,i)\text{backpatch}(p, i):将链表 pp 中每个元素所指向的跳转语句的标号置为 ii
  • 语义函数 nextstm\text{nextstm}:返回下一条 TAC 语句的地址。
  • 语义函数 emit()\text{emit}(\cdots):输出一条 TAC 语句,并使 nextstm\text{nextstm}11

拉链的目的是确定哪些行应被填上同样的出口语句行——它们未来将被填上同一个数。

例如: EE1ME2{backpatch(E1.falselist,M.gotostm);E.truelist:=merge(E1.truelist,E2.truelist);E.falselist:=E2.falselist} \begin{align*} E \to E_1 \lor M E_2 \quad & \{ \text{backpatch}(E_1.\text{falselist}, M.\text{gotostm}); \\ & E.\text{truelist}:= \text{merge}(E_1.\text{truelist}, E_2.\text{truelist}); \\ & E.\text{falselist} := E_2.\text{falselist}\} \end{align*} 由于这里是逻辑“或”运算,故 E1E_1E2E_2 为真,都能够体现 EE 为真,所以 E.truelistE.\text{truelist}E1.truelistE_1.\text{truelist}E2.truelistE_2.\text{truelist} 链接后的结果。类似地。E1E_1 为假并不能说明什么;E2E_2 为假能够体现EE 为假,所以 E.falselistE.\text{falselist}E2.falselistE_2.\text{falselist}

代码回填

对于布尔表达式 E=a<bc<de<fE=a<b\lor c<d\land e<f 为输入。

遍历到 a<ba<b 并归约的时候,得到 E.truelist={0}E.\text{truelist}=\{0\}E.falselist={1}E.\text{falselist}=\{1\},此时 nextstm=2\text{nextstm}=2。写下代码

cpp
1(0) if a < b goto _
2
3(1) goto _
4

然后遍历 \lorMM,得到 M.gotostm={nextstm}={2}M.\text{gotostm}=\{\text{nextstm}\} = \{2\},这个语义动作不对 nextstm\text{nextstm}11

遍历到 c<dc<d 并归约的时候,得到 E.truelist={2}E.\text{truelist}=\{2\}E.falselist={3}E.\text{falselist}=\{3\},此时 nextstm=4\text{nextstm} = 4

cpp
1(0) if a < b goto _
2
3(1) goto _
4
5(2) if c < d goto _
6
7(3) goto _
8

然后遍历 \landMM,得到 M.gotostm={nextstm}={4}M.\text{gotostm} = \{\text{nextstm}\} = \{4\}

遍历到 e<fe<f 并归约的时候,得到 E.truelist={4}E.\text{truelist}=\{4\}E.falselist={5}E.\text{falselist}=\{5\},此时 nextstm=6\text{nextstm} = 6。写下代码

cpp
 1(0) if a < b goto _
 2
 3(1) goto _
 4
 5(2) if c < d goto _
 6
 7(3) goto _
 8
 9(4) if e < f goto _
10
11(5) goto _
12

至此,拉链结束,开始准备代码回填。

归约 \land 的时候,E.truelist:=E2.truelist={4}E.\text{truelist} := E_2.\text{truelist} = \{4\}E.falselist=merge(E1.falselist,E2.falselist)={3,5}E.\text{falselist} = \text{merge}(E_1.\text{falselist}, E_2.\text{falselist}) = \{3, 5\}。然后将 M.gotostmM.\text{gotostm} 中的标号(此例为 44回填E1.truelistE_1.\text{truelist} 中的标号(此例为 22)对应行末填空中。代码为

cpp
 1(0) if a < b goto _
 2
 3(1) goto _
 4
 5(2) if c < d goto (4)
 6
 7(3) goto _
 8
 9(4) if e < f goto _
10
11(5) goto _
12

归约 \lor 的时候,E.falselist:=E2.falselist={3,5}E.\text{falselist} := E_2.\text{falselist} = \{3, 5\}E.truelist=merge(E1.truelist,E2.truelist)={0,4}E.\text{truelist} = \text{merge}(E_1.\text{truelist}, E_2.\text{truelist}) = \{0, 4\}。然后将 M.gotostmM.\text{gotostm} 中的标号(此例为 22回填E1.falselistE_1.\text{falselist} 中的标号(此例为 11)对应行末填空中。代码为

cpp
 1(0) if a < b goto _
 2
 3(1) goto (2)
 4
 5(2) if c < d goto (4)
 6
 7(3) goto _
 8
 9(4) if e < f goto _
10
11(5) goto _
12

拉链与代码回填

处理完整个表达式 EE 之后,语句 (0) (3) (4) (5) 的目标语句标号仍未确定,但这些语句已被记录在 EE 的真链和假链之中。如果以后 EE 被归约,则这些位置也将被回填。

第八讲:运行时存储组织

程序运行时,栈从从高地址低地址方向生长;堆与之相反。

嵌套函数定义中非局部量的访问

在 C 语言等不支持嵌套函数定义的程序设计语言中,非局部量只有全局变量,通常情况下可以分配在静态数据区。所以,这里以类 Pascal 语言为例,来讨论函数层面的非局部量的访问。

pascal
 1program Main(I, O);
 2
 3procedure P;
 4
 5    procedure Q;
 6
 7        procedure R;
 8
 9            begin
10
11                P;
12
13            end;
14
15        begin
16
17            R;
18
19        end;
20
21    begin
22
23        Q;
24
25    end;
26
27procedure S;
28
29    begin
30
31        P;
32
33    end;
34
35begin
36
37    S;
38
39end;
40

静态链

静态链展示的是代码层次上,函数嵌套定义的层次关系。以上述代码为例,函数嵌套定义树如下:

graph BT

R(R) --> Q(Q)

Q --> P

P(P) --> A(main)

S(S) --> A

所以,在任意一个函数 P 的调用中——即使可能的调用顺序是 main - S - P - Q - R - P(自函数调用栈栈底到栈顶)——栈顶的 P 的静态链属性(用 SL 表示)仍指向函数嵌套定义树中 P 的父节点 main 在函数调用栈中最近的那次调用。下图中展示了上述代码的静态链。

静态链

动态链

动态链展示的是函数调用栈中的层次关系。动态链相对于静态链而言理解起来就容易许多,因为它和函数调用栈是类似的。在函数的活动记录中,动态链属性(用 DL 表示)指向的是调用该函数的函数的栈帧的基地址(基地址是最高地址,因为栈是由高地址向低地址生长的,分配空间也是从高往低按顺序分配)。

动态链

Display 表

采用静态链实现非局部量访问,需要沿静态链进行多次访存操作。另一种方法是采用全局 Display 表。Display 表中存储的是静态链每一层中,最新活动记录的基地址

Display 表

callsSPQRP'Q'R'
D[3]___RRRR'
D[2]__QQQQ'Q'
D[1]SPPPP'P'P'
D[0]MainMainMainMainMainMainMain
saved_S__PQR

维护 Display 表的目的是得到 saved 数组。对于每个函数调用 calls 而言,SL 的维护是由 calls 指向 saved

静态作用域规则和动态作用域规则

若遵循静态作用域规则,则要沿着过程活动记录的静态链(或 Display 表)查找最近一个过程中所声明的同名变量。在访问非局部变量的时候,如果 saved 并非 _,则由 calls 沿着 saved 进行查找;否则沿着调用栈在上一层中查找。

若遵循动态作用域规则,则要沿着过程活动记录的动态链(可理解为,函数调用栈)查找最近一个过程中所声明的同名变量。

第九讲:目标代码生成和代码优化基础

基本块、流图、循环

基本块

基本块是指程序中一个顺序执行的语句序列,其中只有一个入口语句和一个出口语句。基本块的入口语句只有以下三种可能:

  1. 程序的第 1 条语句;
  2. 条件跳转语句或者无条件跳转语句的目标语句
  3. 条件跳转语句后面的相邻语句。

基本块就是从一个入口语句到下一个入口语句前的部分。

流图

由基本块构成的有向图。AABB 连一条有向边,当且仅当程序在基本块 AA 执行完之后有可能执行基本块 BB

循环

在流图中,如果从流图的首节点到 vv 的所有路径都必须经过 uu,则称 uuvv支配节点,记作 u DOM vu \text{ DOM } v。流图中节点 vv 的所有支配节点的集合,称为 vv支配节点集,记作 D(v)D(v)。设 n0n_0 是流图的首节点,则对于任意节点 vv,一定有 n0D(v)n_0 \in D(v)vD(v)v \in D(v)

写出 D(n)={,d,}D(n) = \{\cdots, d, \cdots\},若有 ndEn \to d \in E,则称 ndn \to dEE 的一条回边

如果 ndn \to d 是一条回边,则包含该回边的自然循环(简称循环)是由 nndd,以及有不经过 dd 到达 nn 的路径的所有节点构成。

数据流分析

正向数据流方程: out[S]=gen[S](in[S]kill[S]) \text{out}[S] = \text{gen}[S] \cup (\text{in}[S] - \text{kill}[S])

反向数据流方程: in[S]=gen[S](out[S]kill[S]) \text{in}[S] = \text{gen}[S] \cup (\text{out}[S] - \text{kill}[S])

到达-定值数据流

对变量 AA定值(definition)是指一条 TAC 语句赋值或可能赋值给 AA,该语句的位置称为 AA定值点

称变量 AA 的定值点 dd 到达某点 pp,是指有路径从紧跟 dd 的点到达 pp,且在这条路径上 dd 未被“杀死”,即该变量未被重新定值。直观上来说,是指流图中从 dd 有一条路径到达 pp,且路径上没有 AA 的其他定值。我们定义:

  • in[B]\text{in}[B]:到达基本块 BB 入口处(入口语句之前的位置)的各个变量的所有定值点的集合。
  • out[B]\text{out}[B]:到达 BB 出口处(紧接着出口语句之后的位置)的各个变量的所有定值点的集合。
  • gen[B]\text{gen}[B]BB 中定值的并能够到达 BB 出口出的所有定值点的集合。
  • kill[B]\text{kill}[B]:基本块 BB 外满足下列条件的定值点集合:这些定值点能够到达 BB 的入口处,但是所定值的变量在 BB 中被重新定值。

该数据流符合正向数据流方程: out[B]=gen[B](in[B]kill[B]) \text{out}[B] = \text{gen}[B] \cup (\text{in}[B] - \text{kill}[B])

同时,还满足合流方恒: in[B]=(out[b]),bP[B] \text{in}[B] = \cup (\text{out}[b]), \quad b \in \text{P}[B] 其中 P[B]\text{P}[B]BB 的前驱的集合。

设初值 in[B]=,B\text{in}[B] = \varnothing, \quad \forall B,不停地按照以上两个方程进行迭代,即可得到最终稳定的解。

活跃变量数据流

对程序中的某变量 AA 和某点 pp 而言,如果存在一条从 pp 开始的通路,其中引用了 AApp 点的值,则称 AA 在点 pp活跃的,否则称 AA 在 点 pp 是死亡的。我们定义:

  • LiveIn[B]\text{LiveIn}[B]BB 入口处活跃的变量的集合。
  • LiveOut[B]\text{LiveOut}[B]BB 出口处活跃的变量的集合。
  • Def[B]\text{Def}[B]BB 中定值的,且定值前未曾在 BB 中引用过的变量的集合。
  • LiveUse[B]\text{LiveUse}[B]BB 中被定值之前的要引用的变量的集合。

该数据流符合反向数据流方程: LiveIn[B]=LiveUse[B](LiveOut[B]Def[B]) \text{LiveIn}[B] = \text{LiveUse}[B] \cup (\text{LiveOut}[B] - \text{Def}[B])

同时,还满足合流方程: LiveOut[B]=(LiveIn[b]),bS[B] \text{LiveOut}[B] = \cup(\text{LiveIn}[b]), \quad b \in \text{S}[B] 其中 S[B]\text{S}[B]BB 的后继的集合。

设初值 LiveOut[B]=,B\text{LiveOut}[B] = \varnothing, \quad \forall B,不停地按照以上两个方程进行迭代,即可得到最终稳定的解。

UD 链

假设在程序某点 uu 引用了变量 AA 的值,则把能到达 uuAA 的所有定值点的全体,称为 AA 在引用点 uu引用-定值链(即 UD 链,Use-Definition Chain)。

  • 如果在基本块 BB 中,变量 AA 的引用点 uu 之前有 AA 的定值点 dd,并且 AAdd 的定值可以到达 uu,那么 AA 在点 uu 的 UD 链就是 {d}\{d\}
  • 如果在基本块中,变量 AA 的引用点 uu 之前没有 AA 的定值点,那么 in[B]\text{in}[B]AA 的所有定值点均到达 uu,它们就是 AA 在点 uu 的 UD 链。

一句话总结:UD 链给出的是某变量引用的所有可能的定值点

注意:定值是会覆盖的。只考虑“最近”的那个定值点。

DU 链

假设在程序中某点 pp 定义了变量 AA 的值,从 pp 存在一条到达 AA 的某个引用点 ss 的路径,且该路径上不存在 AA 的其他定值点,则把所有此类引用点 ss 的全体称为 AA 在定值点 pp定值-引用链(即 DU 链,Definition-Use Chain)。

一句话总结:DU 链给出的是某变量定值的所有可能的引用点

基本块内变量的待用信息

待用信息是用来跟踪变量在基本块内紧接着一次使用该变量的情况。

  1. 初始化所有的 nextuse()=\text{nextuse}(\cdot) = \varnothing
  2. 迭代:从出口语句到入口语句,假设当前语句(BB 中的第 ii 条)为 a:=b+ca := b + c按顺序进行以下操作:
    • aa 标上 nextuse(a)\text{nextuse}(a)
    • nextuse(a)0\text{nextuse}(a) \gets 0
    • bbcc 分别标上 nextuse(b)\text{nextuse}(b)nextuse(c)\text{nextuse}(c)
    • nextuse(b)i,nextuse(c)i\text{nextuse}(b) \gets i, \text{nextuse}(c) \gets i

基本块内变量的活跃信息

一个基本块中,如果某个变量 AA 在语句 ii 中被定值,在 ii 之后的语句 jj 中要引用 AA 的值,且 iijj 之间没有其他的对 AA 的定值点,则称 AA 在语句 iijj 之间是活跃的。

  1. 初始化出口语句的活跃变量集合 LLiveOut[B]L \gets \text{LiveOut}[B]
  2. 迭代:从出口语句到入口语句,假设当前语句为 a:=b+ca := b + c按顺序进行以下操作:
    • LL{a}L \gets L - \{a\}
    • LL{b,c}L \gets L \cup \{b, c\} 当前语句的活跃变量集合即为现在的 LL

一句话总结:从后往前,出口语句为 LiveOut[B]\text{LiveOut}[B],每次迭代先减去左端变量再加上右端变量

目标代码优化

窥孔优化

常量合并

直接计算出常量表达式的结果。

例如,将

cpp
1(1) r2 := 3 * 2
2

变为

cpp
1(1) r2 := 6
2

常量传播

所有引用常量变量的地方都显式地写出常量值。

例如,将

cpp
1(1) r2 := 6
2
3(2) r3 := r1 * r2
4

变为

cpp
1(1) r2 := 6
2
3(2) r3 := r1 * 6
4

强度削弱

将高强度运算符改为低强度运算符。运算符强度:除法 > 乘法 > 移位运算 > 加减法。

例如,将

cpp
1(1) x := 2.0 * f
2

变为

cpp
1(1) x := f + f
2

又例如,将

cpp
1(1) x := 32 * y
2

变为

cpp
1(1) x := y << 5
2

TAC 语句的 DAG

TAC 对应的 DAG子图

  1. 对于 x:=y  op  zx:=y \; \text{op}\;z 而言,如果 node(y)\text{node}(y)node(z)\text{node}(z) 不存在,则新建它们。
  2. 如果 yyzz 都是常数,则新建 node(x)\text{node}(x) 为计算结果的常数值。如果 node(y)\text{node}(y)node(z)\text{node}(z) 是为了计算 xx 而新建的,则删去 node(y)\text{node}(y)node(z)\text{node}(z)。这一步起到了常量合并的作用。
  3. 如果 yyzz 不是常数,则检查是否存在某个标记为 op\text{op} 的节点,使得左孩子是 node(y)\text{node}(y) 右孩子是 node(z)\text{node}(z)。若不存在,则新建该结点。这一步起到了删除公共子表达式的作用。
  4. 最后,把 xx 从原来的 node(x)\text{node}(x) 中删除,放到新的 node(x)\text{node}(x) 中。这一步起到了删除无用赋值的作用。

考虑以下代码:

cpp
 1(1) T0 := 3.14
 2
 3(2) T1 := 2 * T0
 4
 5(3) T2 := R + r
 6
 7(4) A := T1 * T2
 8
 9(5) B := A
10
11(6) T3 := 2 * T0
12
13(7) T4 := R + r
14
15(8) T5 := T3 * T4
16
17(9) T6 := R + r
18
19(10) B := T5 * T6
20

则 DAG 构造如下:

DAG

最后按照 n1,n2,,nkn_1, n_2, \cdots, n_k 的构造顺序,重新写成 TAC 语句即可。

代码外提

借助于 UD 链可以查找循环不变量,例如,对于循环内部的语句 x:=y+zx:=y+z,若 yyzz 的定值点都在循环外,则 x:=y+zx:=y+z 是循环不变量,可以将其外提。

具体而言,在流图中的循环的入口节点之前新建一个前置节点,用来放置所有被外提的代码。

注意:循环不变量被外提的的条件是,它是循环所有出口节点的必经节点。以下是一个无法进行代码外提的例子:

无法进行代码外提

这里的 i:=2i:=2 并不是循环中所有出口节点(例如,不是 B4B_4)的必经节点,所以无法外提。

删除归纳变量

设有循环不变量 CC,则称 I:=I±CI:=I\pm C 中的 II基础归纳变量J:=C1I±C2J:=C_1 * I \pm C_2 中的 JJ归纳变量,且称 JJII 同族

以下代码中 xx 是一个基本归纳变量,ii 是与 xx 同族的归纳变量。可以将代码进行如下改写:

删除归纳变量

Ershov 数

对于表达式树而言:

  • 叶节点标为 11
  • 对于每个非叶节点:
    • 如果只有 11 个子节点,则沿用该子节点的标记。
    • 如果两个子节点的标记不同,则取较大者
    • 如果两个子节点的标记相同,则取子节点标记 +1+1

对于节点 uu 而言,假设它的 Ershov 数为 kk,则它使用寄存器 R0,R1,,Rk1R_0, R_1, \cdots, R_{k-1} 进行运算,然后把结果存到 R0R_0 中。

寄存器相干图

对于流图中的每个基本块中的每个定值语句,将其定值变量和当前语句时刻的活跃集合中的所有变量之间连边。