计算机技术学习札记

编译原理 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 分析表,在规约动作发生时,执行相应的语义动作。

本节笔记到此结束。