浅谈编译原理——语法制导翻译篇
尝试在抽象中建立一些理解。(起码对我来说很抽象)
概述
语法制导翻译的功能
对于编译器,一般不能只判断输入的语言是否符合语法规则。以算术表达式为例,还需要能确定最后的结果,即翻译目标为计算表达式的值。
语法制导就是如此,根据翻译目标,
- 为上下文无关文法的每个符号设置语义属性(例如一个变量的属性有类型,层次,存储地址,一个表达式的属性有类型和值等);
- 给每条产生式规则附加一个语义动作(本质上是一个代码片段,用于计算或输出语义属性的值),相当于对文法进行了拓广。
语法制导翻译过程就是根据语法分析过程中所使用的产生式,在适当的时机执行与之相应的语义规则,完成符号属性值的计算,从而完成翻译。
语法制导定义(SDD)
语法制导定义:
- 将文法符号和某些属性相关联
- 通过语义规则描述如何计算属性的值
可以简单将语法制导定义理解为产生式 + 语义规则,例如:

语义规则的制定由翻译目标 决定产生式的含义 决定文法符号属性 决定产生式的语义规则。
需要注意的是,SDD 本身还无法给出语义规则的具体计算顺序。
对应题型:写出语法制导定义。
语法制导翻译(SDT)
相较于 SDD,SDT 在上下文无关文法的产生式右部嵌入了程序片段,即语义动作,一个语义动作在产生式中的位置决定了这个动作的执行时间。
这样,编译器在语法分析过程中,在适当的时候执行这些语义动作,完成语义分析和检查。例如:

以第一条插入的语义动作为例,它表示当分析出T之后,就可以使用 T 的 type 属性的值来计算 L 的 in 属性值。
因此,SDT 可以视为对 SDD 的一种补充,是 SDD 的具体实施方案。
对应题型:设计翻译方案。
语法制导定义
产生式与语义规则
对于每一个文法产生式 ,都有与之相联系的语义规则:
其中:
- 都是某文法符号的属性;
- 是函数,比如具体的计算操作。
文法属性
属性文法扩展了上下文无关文法的定义,将文法符号与额外的属性(常见的包括值、类型等)建立了联系,具体又分为:
综合属性
综合属性:通过子节点符号的属性或通过自身的属性计算得到。
- 通常用于从下往上传递信息。
- 是 的一个综合属性,且 为产生式右部符号的属性(即 的子节点的属性),或 的继承属性;
- 向上看继承,向下看子节点

注意:你可能有以下疑惑,终结符是叶子节点,没有孩子了,可以有综合属性吗?答案是可以的,终结符可以有综合属性,词法分析程序提供的就是综合属性值,并且不能有继承属性。例如,digit.lexval,通常是一个常量。
还有一类比较特殊的属性,一般用于拓广文法的起始符号 ,其不依赖任何属性,仅用于在属性计算完成后输出某个值或完成一项功能,例如 Print(E.val),所以称其为虚拟综合属性。
继承属性
继承属性:某节点的继承属性由它兄弟、父亲或者自己的属性计算得到。
- 通常用于自上而下,或横向传递信息。
- 如果 是产生式右部某个符号 的一个继承属性,那么 是 (它爹) 或任何产生式右部符号 (兄弟或它自己)的属性;
- 属性 依赖于属性 ;

注释分析树

一般默认约定:继承属性写文法符号的左边,综合属性写在右边。
计算次序
依赖图
分析树中不同的节点间的属性存在依赖关系,可以用依赖图表示。
表示方法:
- 分析树中,为符号的每个属性设置一个结点,一般画在符号旁边
- 如果属性 b 依赖于 c,那么存在一条 c -> b 的有向边(被依赖者指向依赖者,因为只有 c 先算了才能算 b);
因此,属性的计算次序可以由依赖图的拓扑排序确定。
引入 2 种特殊的语法制导定义,这两种 SDD 的依赖图一定无环,因此可以通过拓扑排序计算出所有属性的属性值。
S属性定义
即语法制导定义的所有属性都是综合属性,因此属性的计算过程就是自底向上的。因此可以配合自底向上的语法分析过程中实现。一般可以在进行归约时,按照语义规则计算归约得到的符号的属性值。
从依赖图来看,综合属性的边都是从下往上的。
L属性定义
既有综合属性又有继承属性,但要求继承属性满足特定条件,即对于任意产生式 以及继承属性 ,其只能依赖于:
- 的继承属性(父亲的继承属性);
- 这里之所以只能依赖父亲的继承属性,是因为如果依赖父亲的综合属性,但父亲的综合属性又依赖子节点的属性,就会产生环路;
- 的属性(左边兄弟的属性);
因此,从依赖图上来看,就是只允许:
- 继承属性:从左向右,或从上到下的边;
- 综合属性:同 S 属性定义,从下到上的边;
显然,每一个 S 属性定义都是 L 属性定义。
构造依赖图
在已经画出分析树的基础上,假如有产生式 ,对应的语义规则为 A.a=f(X.x,Y.y) 和 X.i=g(A.a,Y.y)。

计算次序
计算顺序是有向非循环图的拓扑排序。
即必须按依赖图中箭头指向的方向进行排序。
语法制导翻译
翻译方案:把 SDD 的语义规则改写为计算属性值的程序片段,语义动作括在 {} 中,并插入到产生式右部某个合适的位置上。例如:

基本实现方法如下:
- 建立语法分析树;
- 将语义动作看作是虚拟的结点;
- 从左到右,深度优先地遍历分析树,在访问虚拟结点时执行相应语义动作;
翻译方案的设计
S属性定义翻译
- 为每一个语义规则建立一个包含赋值的动作;
- 把这个动作放在相应的产生式右边末尾;
这是比较显然的,因为都是综合属性,而父节点的综合属性依赖于子节点,因此需要等子节点先都分析完才能计算。例如,以产生式 和语义规则 为例,将语义动作如下插入:
S 属性定义的基础文法是 LR 文法,其与 LR 分析过程是兼容的,当归约发生时,即可执行对应语义动作。
L属性定义翻译
L 属性定义既有综合属性又有继承属性,因此需要遵守以下原则:
- 一个动作不能引用这个动作右边的文法符号的综合属性;
- 因为是处理过程是从左到右的,右边文法符号的综合属性还没有计算出来;
- 对于产生式左部符号的综合属性:
- 只有在它所引用的所有属性都计算出来之后才能计算;
- 因此放在产生式右端末尾;(与 S 属性文法要求一致)
- 对于产生式右部符号的继承属性:
- 必须在这个符号以前的动作中计算出来;
- 因此放在该文法符号前面;
- 即对于右部符号 的继承属性的动作,插入到右部紧靠 之前的位置上( 的左边);
S属性定义的自底向上翻译
语法树 vs. 分析树
具体语法树 vs. 抽象语法树 vs. 分析树:


抽象语法树将节点按照以下操作抽象:
- 对于叶子节点:用一个附加域存储此叶子结点的词法值/符号表入口
makeleaf(id, entry):建立一个标识符节点,标号为 id,附加域指向该标识符在符号表中条目的入口;makeleaf(num, val):建立一个数节点,标号为 num,附加域直接存词法值;
- 对于内部节点:附加字段数量等于结点的子结点数量,通过构造函数连接子节点
makenode(op, left, right):op表示运算操作,left/right分别指向左右孩子;
构造 AST 及其语法制导定义
对于 AST,产生式的语义应该是创建与产生式左部符号代表的子表达式对应的子树,即创建子树的根结点。
因此,文法符号的属性应包括 nptr 表示指向创建的子树的根节点,例如,对于产生式 ,对应的语义规则为:

分析和翻译过程形如:

有向非循环图dag
简单来说, 在语法树的基础上,公共表达式可以有多个父亲节点,节省了存储开销。

自底向上的翻译过程实现(LR)
修改分析栈:使其能保存综合属性。

改写语义规则:使之变成具体可执行的栈操作。

简单来说,当要执行归约操作 前,执行对应代码,先改写对应的符号栈,然后计算对应的综合属性栈。然后在执行归约操作,将 入栈, 出栈,栈顶变化为 。因此,综合属性刚好在每次归约前计算。
对于终结符,其在移进时,综合属性值(由词法分析程序提供)随状态一起入栈。
L属性的自底向上翻译
主要介绍以下 4 种方法:
1. 移走翻译方案中嵌入的语义规则
只要把嵌入(除产生式右部最右侧)在产生式右部之中的语义动作想办法移动到最右边,那么之后的分析思路与 S属性的翻译过程就类似了。
因此,针对嵌入在产生式右部的语义规则,例如 :
- 引入一个新的非终结符 ;
- 以及对应产生式
2. 直接使用分析栈中的继承属性
使用该方法的前提是:
- 继承属性在栈中
- 位置已知
3. 变换继承属性的计算规则
前一种方法中,虽然我们知道 LR 分析过程中,属性值都放在分析栈 val 中,但是无法知道继承属性的值在栈中的位置。
例如,如果同时包含产生式 和 ,对应的语义动作均为 ,即继承属性 依赖之前的 ,当问题是,在到达 并使用 进行归约时,无法知道 该取 val[top-1] 还是 val[top-2],即无法知道 离 有多远。
个人理解,解决的思路是想办法区分语义动作相同的产生式,例如如果产生式 以及语义动作均不变,我们在希望使用它时, 需要的就是 val[top-1]。而对于产生式 ,我们引入一个新的符号,并中转属性的继承关系:
- 在 前面添加一个非终结符 及产生式 ,使之紧挨着 ;
- 产生式 对应的语义规则修改为 ;
- 产生式 对应的语义规则为 ;
因此,如果我们希望 继承自 val[top-2],进行上述修改后,分析过程变成:当分析到 之前,让 去接收本来要继承给 的 ,分析到 时,计算 ,接下来分析到 ,此时 就位于 val[top-1], 能确定继承 。
上面只是针对“复制规则”,即直接等于,进一步可以考虑对所有继承属性的“非复制规则“进行改造,将其改造为复制规则,考虑:
- (非复制规则)
同样,引入非终结符 和对应产生式 ,将以上文法改写为:
和复制规则基本一致,只是 中执行的计算改变。
在归约 前,栈中状态形如:
a X(top)a.lex X.s当归约 时,N 入栈,且 来自当前栈顶 对应的综合属性值处,因此 N.s: val[top+1]=f(val[top])。栈中状态变为:
a X N
a.lex X.s N.s入栈:
a X N(top-1) y(top)
a.lex X.s N.s y.lex随后归约 , 出栈而 入栈, 来自于 ,因此对应的执行过程为 Y.s: val[top]=g(val[top-1])。栈中状态变为:
a X N Y
a.lex X.s N.s Y.i事实上按照上述方法构造,基本可以得出以下归约时执行动作的结论:
| 归约 | 栈顶状态 | 语义本质 | 赋值位置 |
|---|---|---|---|
| N → ε | top 是 X.s | N.s = f(X.s) | val[top+1] = f(val[top]) |
| Y → y | top 是 y,top-1 是 N.s | Y.s = g(N.s) | val[top] = g(val[top-1]) |
4. 改写语法制导定义为S属性定义
- 改写文法
- 让信息传递方向与 LR 归约方向一致
😄依旧似懂非懂。
文章分享
如果这篇文章对你有帮助,欢迎分享给更多人!



