浅谈编译原理——语法制导翻译篇

3505 字
18 分钟
浅谈编译原理——语法制导翻译篇
Note

尝试在抽象中建立一些理解。(起码对我来说很抽象)

概述#

语法制导翻译的功能#

对于编译器,一般不能只判断输入的语言是否符合语法规则。以算术表达式为例,还需要能确定最后的结果,即翻译目标为计算表达式的值。

语法制导就是如此,根据翻译目标,

  • 为上下文无关文法的每个符号设置语义属性(例如一个变量的属性有类型,层次,存储地址,一个表达式的属性有类型和值等);
  • 给每条产生式规则附加一个语义动作(本质上是一个代码片段,用于计算或输出语义属性的值),相当于对文法进行了拓广。

语法制导翻译过程就是根据语法分析过程中所使用的产生式,在适当的时机执行与之相应的语义规则,完成符号属性值的计算,从而完成翻译。

语法制导定义(SDD)#

语法制导定义:

  • 将文法符号和某些属性相关联
  • 通过语义规则描述如何计算属性的值

可以简单将语法制导定义理解为产生式 + 语义规则,例如:

语义规则的制定由翻译目标 →\rightarrow 决定产生式的含义 →\rightarrow 决定文法符号属性 →\rightarrow 决定产生式的语义规则。

需要注意的是,SDD 本身还无法给出语义规则的具体计算顺序。

对应题型:写出语法制导定义。

语法制导翻译(SDT)#

相较于 SDD,SDT 在上下文无关文法的产生式右部嵌入了程序片段,即语义动作,一个语义动作在产生式中的位置决定了这个动作的执行时间。

这样,编译器在语法分析过程中,在适当的时候执行这些语义动作,完成语义分析和检查。例如:

以第一条插入的语义动作为例,它表示当分析出T之后,就可以使用 T 的 type 属性的值来计算 L 的 in 属性值。

因此,SDT 可以视为对 SDD 的一种补充,是 SDD 的具体实施方案。

对应题型:设计翻译方案。

语法制导定义#

产生式与语义规则#

对于每一个文法产生式 A→X1X2⋯XnA\rightarrow X_1X_2\cdots X_n,都有与之相联系的语义规则:

b=f(c1,c2,⋯ ,ck)b=f(c_1,c_2,\cdots,c_k)

其中:

  • b,c1,c2,⋯ ,ckb,c_1,c_2,\cdots,c_k 都是某文法符号的属性;
  • ff 是函数,比如具体的计算操作。

文法属性#

属性文法扩展了上下文无关文法的定义,将文法符号与额外的属性(常见的包括值、类型等)建立了联系,具体又分为:

综合属性#

综合属性:通过子节点符号的属性或通过自身的属性计算得到。

  • 通常用于从下往上传递信息。
  • bb 是 AA 的一个综合属性,且 c1,c2,⋯ ,ckc_1,c_2,\cdots,c_k 为产生式右部符号的属性(即 AA 的子节点的属性),或 AA 的继承属性;
  • 向上看继承,向下看子节点

Warning

注意:你可能有以下疑惑,终结符是叶子节点,没有孩子了,可以有综合属性吗?答案是可以的,终结符可以有综合属性,词法分析程序提供的就是综合属性值,并且不能有继承属性。例如,digit.lexval,通常是一个常量。

还有一类比较特殊的属性,一般用于拓广文法的起始符号 S′S^\prime,其不依赖任何属性,仅用于在属性计算完成后输出某个值或完成一项功能,例如 Print(E.val),所以称其为虚拟综合属性。

继承属性#

继承属性:某节点的继承属性由它兄弟、父亲或者自己的属性计算得到。

  • 通常用于自上而下,或横向传递信息。
  • 如果 bb 是产生式右部某个符号 XiX_i 的一个继承属性,那么 c1,c2,⋯ ,ckc_1,c_2,\cdots,c_k 是 AA(它爹) 或任何产生式右部符号 XjX_j(兄弟或它自己)的属性;
  • 属性 bb 依赖于属性 c1,c2,⋯ ,ckc_1,c_2,\cdots,c_k;

注释分析树#

一般默认约定:继承属性写文法符号的左边,综合属性写在右边。

计算次序#

依赖图#

分析树中不同的节点间的属性存在依赖关系,可以用依赖图表示。

表示方法:

  • 分析树中,为符号的每个属性设置一个结点,一般画在符号旁边
  • 如果属性 b 依赖于 c,那么存在一条 c -> b 的有向边(被依赖者指向依赖者,因为只有 c 先算了才能算 b);

因此,属性的计算次序可以由依赖图的拓扑排序确定。

引入 2 种特殊的语法制导定义,这两种 SDD 的依赖图一定无环,因此可以通过拓扑排序计算出所有属性的属性值。

S属性定义#

即语法制导定义的所有属性都是综合属性,因此属性的计算过程就是自底向上的。因此可以配合自底向上的语法分析过程中实现。一般可以在进行归约时,按照语义规则计算归约得到的符号的属性值。

从依赖图来看,综合属性的边都是从下往上的。

L属性定义#

既有综合属性又有继承属性,但要求继承属性满足特定条件,即对于任意产生式 A→X1X2⋯XnA\rightarrow X_1X_2\cdots X_n 以及继承属性 Xi.aX_i.a,其只能依赖于:

  • AA 的继承属性(父亲的继承属性);
    • 这里之所以只能依赖父亲的继承属性,是因为如果依赖父亲的综合属性,但父亲的综合属性又依赖子节点的属性,就会产生环路;
  • X1,X2,⋯ ,Xi−1X_1,X_2,\cdots,X_{i-1} 的属性(左边兄弟的属性);

因此,从依赖图上来看,就是只允许:

  • 继承属性:从左向右,或从上到下的边;
  • 综合属性:同 S 属性定义,从下到上的边;

显然,每一个 S 属性定义都是 L 属性定义。

构造依赖图#

在已经画出分析树的基础上,假如有产生式 A→XYA\rightarrow XY,对应的语义规则为 A.a=f(X.x,Y.y) 和 X.i=g(A.a,Y.y)。

计算次序#

计算顺序是有向非循环图的拓扑排序。

即必须按依赖图中箭头指向的方向进行排序。

语法制导翻译#

翻译方案:把 SDD 的语义规则改写为计算属性值的程序片段,语义动作括在 {} 中,并插入到产生式右部某个合适的位置上。例如:

基本实现方法如下:

  1. 建立语法分析树;
  2. 将语义动作看作是虚拟的结点;
  3. 从左到右,深度优先地遍历分析树,在访问虚拟结点时执行相应语义动作;

翻译方案的设计#

S属性定义翻译#

  1. 为每一个语义规则建立一个包含赋值的动作;
  2. 把这个动作放在相应的产生式右边末尾;

这是比较显然的,因为都是综合属性,而父节点的综合属性依赖于子节点,因此需要等子节点先都分析完才能计算。例如,以产生式 T→T1∗FT\rightarrow T_1*F 和语义规则 T.val=T1.val∗F.valT.val=T_1.val*F.val 为例,将语义动作如下插入:

T→T1∗F{T.val=T1.val∗F.val}T\rightarrow T_1*F\{T.val=T_1.val*F.val\}

S 属性定义的基础文法是 LR 文法,其与 LR 分析过程是兼容的,当归约发生时,即可执行对应语义动作。

L属性定义翻译#

L 属性定义既有综合属性又有继承属性,因此需要遵守以下原则:

  • 一个动作不能引用这个动作右边的文法符号的综合属性;
    • 因为是处理过程是从左到右的,右边文法符号的综合属性还没有计算出来;
  • 对于产生式左部符号的综合属性:
    • 只有在它所引用的所有属性都计算出来之后才能计算;
    • 因此放在产生式右端末尾;(与 S 属性文法要求一致)
  • 对于产生式右部符号的继承属性:
    • 必须在这个符号以前的动作中计算出来;
    • 因此放在该文法符号前面;
    • 即对于右部符号 AA 的继承属性的动作,插入到右部紧靠 AA 之前的位置上(AA 的左边);

S属性定义的自底向上翻译#

语法树 vs. 分析树#

具体语法树 vs. 抽象语法树 vs. 分析树:

抽象语法树将节点按照以下操作抽象:

  1. 对于叶子节点:用一个附加域存储此叶子结点的词法值/符号表入口
    • makeleaf(id, entry):建立一个标识符节点,标号为 id,附加域指向该标识符在符号表中条目的入口;
    • makeleaf(num, val):建立一个数节点,标号为 num,附加域直接存词法值;
  2. 对于内部节点:附加字段数量等于结点的子结点数量,通过构造函数连接子节点
    • makenode(op, left, right):op 表示运算操作,left/right 分别指向左右孩子;

构造 AST 及其语法制导定义#

对于 AST,产生式的语义应该是创建与产生式左部符号代表的子表达式对应的子树,即创建子树的根结点。

因此,文法符号的属性应包括 nptr 表示指向创建的子树的根节点,例如,对于产生式 E→E1+TE\rightarrow E_1+T,对应的语义规则为:

E.nptr=makenode(′+′,E1.nptr,T.nptr)E.nptr = makenode('+',E_1.nptr, T.nptr)

分析和翻译过程形如:

有向非循环图dag#

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

自底向上的翻译过程实现(LR)#

修改分析栈:使其能保存综合属性。

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

简单来说,当要执行归约操作 A→XYZ⋅A\rightarrow XYZ\cdot前,执行对应代码,先改写对应的符号栈,然后计算对应的综合属性栈。然后在执行归约操作,将 AA 入栈,X,Y,ZX,Y,Z 出栈,栈顶变化为 top→top−2top\rightarrow top - 2。因此,综合属性刚好在每次归约前计算。

对于终结符,其在移进时,综合属性值(由词法分析程序提供)随状态一起入栈。

L属性的自底向上翻译#

主要介绍以下 4 种方法:

1. 移走翻译方案中嵌入的语义规则#

只要把嵌入(除产生式右部最右侧)在产生式右部之中的语义动作想办法移动到最右边,那么之后的分析思路与 S属性的翻译过程就类似了。

因此,针对嵌入在产生式右部的语义规则,例如 R→+T{print(′+′)}RR\rightarrow +T\{print('+')\}R:

  • 引入一个新的非终结符 MM;
  • 以及对应产生式 M→ε{print(′+′)}M\rightarrow\varepsilon\{print('+')\}

2. 直接使用分析栈中的继承属性#

使用该方法的前提是:

  • 继承属性在栈中
  • 位置已知

3. 变换继承属性的计算规则#

前一种方法中,虽然我们知道 LR 分析过程中,属性值都放在分析栈 val 中,但是无法知道继承属性的值在栈中的位置。

例如,如果同时包含产生式 A→aXZA\rightarrow aXZ 和 A→bXYZA\rightarrow bXYZ,对应的语义动作均为 Z.i=X.sZ.i = X.s,即继承属性 Z.iZ.i 依赖之前的 XX,当问题是,在到达 ZZ 并使用 Z→zZ\rightarrow z 进行归约时,无法知道 Z.iZ.i 该取 val[top-1] 还是 val[top-2],即无法知道 ZZ 离 XX 有多远。

个人理解,解决的思路是想办法区分语义动作相同的产生式,例如如果产生式 A→aXZA\rightarrow aXZ 以及语义动作均不变,我们在希望使用它时,Z.iZ.i 需要的就是 val[top-1]。而对于产生式 A→bXYZA\rightarrow bXYZ,我们引入一个新的符号,并中转属性的继承关系:

  1. 在 ZZ 前面添加一个非终结符 MM 及产生式 M→εM\rightarrow\varepsilon,使之紧挨着 ZZ;
  2. 产生式 A→bXYMZA\rightarrow bXYMZ 对应的语义规则修改为 M.i=X.s,Z.i=M.sM.i=X.s,Z.i=M.s;
  3. 产生式 M→εM\rightarrow\varepsilon 对应的语义规则为 M.s=M.iM.s = M.i;

因此,如果我们希望 Z.iZ.i 继承自 val[top-2],进行上述修改后,分析过程变成:当分析到 MM 之前,让 M.iM.i 去接收本来要继承给 ZZ 的 X.sX.s,分析到 MM 时,计算 M.s=M.iM.s = M.i,接下来分析到 ZZ,此时 M.sM.s 就位于 val[top-1],Z.iZ.i 能确定继承 M.sM.s。

上面只是针对“复制规则”,即直接等于,进一步可以考虑对所有继承属性的“非复制规则“进行改造,将其改造为复制规则,考虑:

  • A→aXY,Y.i=f(X.s)A \rightarrow a X Y,Y.i = f(X.s)(非复制规则)
  • Y→y,Y.s=g(Y.i)Y → y,Y.s = g(Y.i)

同样,引入非终结符 NN 和对应产生式 N→εN\rightarrow\varepsilon,将以上文法改写为:

  • A→aXNY,N.i=X.s,Y.i=N.sA → a X N Y,N.i = X.s,Y.i = N.s
  • N→ε,N.s=f(N.i)N\rightarrow\varepsilon,N.s = f(N.i)
  • Y→y,Y.s=g(Y.i)Y → y,Y.s = g(Y.i)

和复制规则基本一致,只是 N→εN\rightarrow\varepsilon 中执行的计算改变。

在归约 N→εN\rightarrow\varepsilon 前,栈中状态形如:

a X(top)
a.lex X.s

当归约 N→εN\rightarrow\varepsilon 时,N 入栈,且 N.iN.i 来自当前栈顶 XX 对应的综合属性值处,因此 N.s: val[top+1]=f(val[top])。栈中状态变为:

a   X   N
a.lex   X.s N.s

yy 入栈:

a   X   N(top-1) y(top)
a.lex   X.s N.s y.lex

随后归约 Y→yY\rightarrow y,yy 出栈而 YY 入栈,Y.iY.i 来自于 N.sN.s,因此对应的执行过程为 Y.s: val[top]=g(val[top-1])。栈中状态变为:

a   X   N Y
a.lex   X.s N.s Y.i

事实上按照上述方法构造,基本可以得出以下归约时执行动作的结论:

归约栈顶状态语义本质赋值位置
N → εtop 是 X.sN.s = f(X.s)val[top+1] = f(val[top])
Y → ytop 是 y,top-1 是 N.sY.s = g(N.s)val[top] = g(val[top-1])

4. 改写语法制导定义为S属性定义#

  • 改写文法
  • 让信息传递方向与 LR 归约方向一致

😄依旧似懂非懂。

文章分享

如果这篇文章对你有帮助,欢迎分享给更多人!

浅谈编译原理——语法制导翻译篇
https://blog.yokumi.cn/posts/notes-about-syntax-directed-translation/
作者
Yokumi
发布于
2025-12-06
许可协议
CC BY-NC-SA 4.0
相关文章智能推荐
1
浅谈编译原理——语法分析篇
专业学习词法分析程序接受字符流(即源程序),输出记号流,作为语法分析程序的输入(按照自左向右的顺序扫描输入的记号序列),语法分析程序会根据语法规则,判断输入的记号流是否合法,并输出分析树。 分析方法包括:自顶向下分析和自底向上分析。 由于上下文无关文法(CFG)中所有的产生式左边只有一个非终结符,所以我们在调用产生式规则的函…
2
2026.5 Live Repo
生活杂谈翻了翻相册才发现上一次已经是去年 9 月的羊文学了,哎gszm坏事做尽。 这回的出行方式是经典京沪高铁二等座,去年锅贴得意之作绿皮 D9 真给我坐麻了,这次果断 G 系列。不过被课表背刺,本来还可以买早一班(),结果就是快 0 点才到虹桥。好像是第一次这么晚到沪国。
3
BUPT 计网实践不完全指北
专业学习计算机网络实践这门课每个老师使用的仿真器并不相同,如果你有幸选了张海旸老师的课,那么恭喜你,你将穿越回 2007 年,和 BUPT 的历届计算机学子一起,使用原本在 Windows XP 系统上构建的仿真器 Dynamips 进行实验,由于年代久远,实验中发生不稳定现象也是常有的事。
4
漫游计算机网络之情景篇
专业学习PSTN ,即 Public Switched Telephone Network ,公用交换电话网。 交换局和交换局之间,使用光纤连接。 中继线上传输的是数字信号,其中用到了时分复用技术,包括: 用户家里的电话和电脑都通过电话线,其中,电脑的数字信号先经过 ADSL Modem 转换为模拟信号,然后和语音信号一起经…
5
漫游计算机网络之设备篇
专业学习简单来说,网关是一种能连接两个不同网络的设备,它在网络层或更高层起作用,负责协议转换、地址转换、路由转发等功能。 你可以简单的理解为,所有网关都是路由器,不过路由器并不一定是网关。 NAT ,即 Network Address Translation ,网络地址转换。

评论区

文章目录