编译原理 4:句法制导翻译
在完成语法分析后,我们得到了源代码对应的语法树,但这样的东西距离能用来运行的机器码显然还有不少的距离。具体地,我们即使有了语法树,我们还是不知道代码的语义——代码「要做什么」。句法制导翻译关注的就是读懂代码的「语义」,使用 CFG 来引导对语言的翻译。
基本思想
我们为 CFG 中的文法符号设置语义属性,用来表示语法成分对应的语义信息。文法符号的语义属性值,是用与文法符号所在产生式相关联的语义规则来计算的。
需要引入句法制导定义(SDD)和句法制导翻译方案(SDT)。
句法制导定义
SDD 是对 CFG 的推广,它将每个文法符号和一个语义属性集合相关联,将每个产生式和一组语义规则相关联。
如果 \(X\) 是一个文法符号,\(a\) 是 \(X\) 的一个属性,则用 \(X.a\) 表示属性 \(a\) 在某个符号为 \(X\) 的分析树结点上的值。语义规则则是在产生式中,对某一语法变元的某个属性进行计算的规则。例如,对于 C 语言变量类型 \(T\to\mathrm{int}\),它对应的语义规则就是 \(T.type=\mathrm{int}\)(将 \(T\) 的 \(type\) 字段设置成 \(\mathrm{int}\))。
句法制导翻译方案
将语义规则(语义动作)嵌入 CFG,我们就得到句法制导翻译方案 SDT。我们把语义动作放在花括号内,如:
\[\begin{aligned} & D\to T\,\textcolor{blue}{\{L.inh=T.type\}}\,L \\ & T\to\mathrm{int}\,\textcolor{blue}{\{T.type=\mathrm{int}\}} \\ & T\to\mathrm{real}\,\textcolor{blue}{\{T.type=\mathrm{real}\}} \\ & L\to\textcolor{blue}{\{L_1.inh=L.inh\}}\, L_1, \mathbf{id} \end{aligned}\]
语法符号的属性
综合属性
综合属性是指只能通过一个语法符号(非终结符或者终结符)自身或者(非终结符)它的子结点属性值来定义的属性。例如,对于产生式 \(E\to E+T\),对应语义规则 \(E.val = E.val + T.val\),这里的 \(val\) 就是综合属性。
继承属性
继承发生是指只能通过非终结符的父结点、兄弟结点或者它本身属性值来定义的属性。例如,对于产生式 \(D\to TL\),对应语义规则 \(L.inh=T.type\)(即 \(L\) 的类型是 \(T\) 给出的),\(inh\) 就是继承属性。
分析树和依赖图
在语法分析树中,如果将结点(语法符号)的属性和属性之间的依赖关系画在树上,其中综合属性画在结点的左边,继承属性画在右边,我们就可以得到一个有向图,称为「依赖图」。如果属性 \(X.a\) 的值依赖于 \(Y.b\) 的值,则依赖图中有一条从 \(Y.b\) 到 \(X.a\) 的边。如:
S-SDD
仅仅使用综合属性的 SDD 称为 S-属性的 SDD,或 S-属性定义、S-SDD。由于综合属性只能通过节点自己或者子节点计算,对于这样的 SDD,可以按照分析树结点自底向上计算各个属性值。例如:
S-SDD 定义的制导方案(SDT),只要将对应的语义规则放置在对应的产生式末尾,例如:
如果这个文法可以使用 LR 分析,那么它的 SDT 可以在 LR 句法分析过程中实现:在规约发生时,执行相应的语义动作。在原有的状态栈和符号栈之上,我们增加一个「综合属性」栈,用来存放对应符号的综合属性值,即可实现 SDT 的 LR 分析。
具体地,我们还要将语义规则中的 \(\{A.a=f(X.x, Y.y, Z.z)\}\) 这样的抽象定义,更改为具体可执行的栈操作。例如:
stack[top - 2].symb = A
stack[top - 2].val = f(stack[top - 2].val, stack[top - 1].val, stack[top].val)
top = top - 2
当规约动作发生时,按这些栈操作进行即能实现 S-SDT 的语义分析。
L-SDD
使用了部分继承属性的 SDD 称为 L-SDD,其中 L 的意思是「从左到右」,即这种 SDD 依赖图的边可以从左到右,但不能从右到左。L-SDD 的属性要么是综合属性,要么是这样的继承属性:假设存在产生式 \(A\to X_1X_2\cdots X_n\),其右部符号 \(X_i\) 的继承属性仅依赖于 (a) \(A\) 的继承属性,或 (b) \(X_1X_2\cdots X_{n-1}\) 的属性,或 (c) \(X_i\) 本身的属性,且不构成环路。
例如:
对于 L-SDD 的 SDT,需要:
将计算某个非终结符号 \(A\) 的继承属性的动作插入到产生式右部紧靠 A 的本次出现之前的位置。
将计算左部综合属性的动作放在最后。
例如:
得到:
在非递归的预测分析中进行翻译
「非递归的预测分析」即 LL(1) 的分析法。在 编译原理 2:自顶向下的语法分析 中,我们使用「语法分析栈」记录已经分析出来的语法变元。现在,我们需要对这个栈中的元素进行扩充,将原先的单个文法符号 \(A\) 变成下面的结构:
以上面的乘法文法为例。首先,将所有的语义动作用指针指代,得到下面的 SDT:
尝试分析式子 3 * 5
。首先,按照 LL(1) 分析表的构造文法,先写出这个文法的 LL(1) 分析表。
栈顶⬇️ 读入➡️ | \(*\) | \(\mathbf{digit}\) | \(\#\) |
---|---|---|---|
\(T\) | \(T\to FT'\) | ||
\(T'\) | \(T'\to*FT_1'\) | \(T'\to\varepsilon\) | |
\(F\) | \(F\to\mathbf{digit}\) |
然后开始分析。
启动时,分析栈为初始符号 \(T\) 的综合属性和继承属性。
T Tsyn # (val)
读入
3
,使用 \(T\to FT'\),将 \(T'\) 和 \(F\) 先后入栈。F Fsyn {a1} T' T'syn {a2} Tsyn # (val) (inh) (syn) ===> (val)
使用 \(F\to\mathbf{digit}\),将 \(\mathbf{digit}\) 入栈,消去
3
。digit {a6} Fsyn {a1} T' T'syn {a2} Tsyn # (3) ===> (val=3) ====> (inh=3) (syn) ===> (val)
读入
*
,使用 \(T'\to *FT_1'\),将 \(T_1'\)、\(F\) 和 \(*\) 先后入栈,消去*
。* F Fsyn {a3} T1' T1'syn {a4} T'syn {a2} Tsyn # (val) (inh) (syn) ===> (syn) ===> (val) (T'inh=3)
读入
5
,使用 \(F\to\mathbf{digit}\),消去5
。digit {a6} Fsyn {a3} T1' T1'syn {a4} T'syn {a2} Tsyn # (5) ====> (val=5) 3*5=> (inh=15) => ...
L-SDD 的 LR 翻译
给定一个 LL 文法为基础的 L-SDD,可以修改这个文法来对它进行 LR 分析中的句法制导翻译。我们将每个产生式右部中间的语义动作移出,改为用产生 \(\varepsilon\) 的标记非终结符替代。这样在修改后的 SDT 中,所有语义动作都在产生式的末尾。如:
\[\begin{aligned} T&\to FMT'\ \{T.val=T.syn\} \\ M&\to\varepsilon\ \{M.inh=F.val, M.syn=M.inh\} \\ T'&\to *FNT_1'\ \{T'.syn=T_1'.syn\} \\ N&\to\varepsilon\ \{N.i1=T'.inh, N.i2=F.val, N.syn=N.i1\times N.i2\} \\ T'&\to \varepsilon\ \{T'.syn=T'.inh\}\\ F&\to \mathbf{digit}\ \{F.val=\mathbf{digit}.lexval\} \end{aligned}\]
然后,重新设计 LR 分析表,在规约动作发生时,执行相应的语义动作。
本节笔记到此结束。