编译原理笔记05:语法制导翻译

Posted by LiYixian on Tuesday, November 21, 2023 | 阅读 | ,阅读约 5 分钟

Summary:

  • 语法制导的定义
  • 构造语法树
  • S-属性定义、L-属性定义
  • 自顶向下计算属性
  • 自底向上计算属性

语法制导定义

语法制导翻译是一种搭建在语法分析基础上的翻译技术。
思路:给每个符号(特别是非终结符)设置一系列属性 attribute,在语法翻译的时候对属性进行求值。

语法制导定义 SDD 是 CFG 的推广:

  • 语法符号 - 属性,定义时需要说明其意义和类型
    • 综合 synthesized,由子节点属性计算,命名为 val (value)
    • 继承 inherited,由 parent 和 sibling 属性计算,命名为 in (inherited)
      • 表达程序结构在上下文中的相互依赖关系
    • terminal 只有综合属性,由词法分析器提供;开始符号没有继承属性。
  • production - 语义规则,用于计算属性
    • 每个 production 关联一个或多个语义规则
    • 语义规则的执行反映属性之间的关系
  • 依赖图 dependency graph
  • 注释语法树 annotated parse tree,节点带有属性值的分析树

SDD 的格式是一个表,见下面两个例子。

e.g. 要对表达式求值,可以定义语义规则如下:

要定义变量,可以定义语义规则如下:

可见,翻译目标决定了语义规则

如果一条语义规则的作用和求值无关,如打印一个值或向符号表中添加记录,则成为 production 左侧 non-terminal 的虚拟综合属性,常见的有:

  • print (text) 打印 text
  • addtype (id.entry, type)
    • 在符号表中为符号 id 添加符号类型(变量类型)type
    • id. entry 指明符号 id 在符号表中的入口

依赖图

属性 b 依赖属性 c,则 b 应该在 c 之后计算;依赖图是表示这种依赖关系的有向图。
e.g. 变量定义的依赖图
符号左侧为继承属性,右侧为综合属性(包括虚拟综合属性)

如果依赖图无环,则存在一个拓扑排序,即为这些属性值的计算顺序。

语法制导翻译的一般步骤:

  1. 文法
  2. 注释语法树
  3. 依赖图
  4. 拓扑排序
  5. 语义规则计算顺序
  6. 输入串翻译结果

构造语法树

语法树是仅由终结符构成的分析树的压缩形式;
自身可以表示运算的优先级,所以所有括号都可以省略。
e.g.

用有向无环图 DAG 表示表达式:

  • 对语法树提取公共表达式
  • 可能出现一个节点有多个 parent 的情况
  • 构造节点前检查是否已经构造相同节点

e.g. i = i + 10

S-属性定义和 L-属性定义

翻译模式

又叫翻译方案,将语义规则嵌入 production 右部适当的位置,指明计算顺序;相当于在 SDD 的基础上添加了执行语义规则的时机。

e.g.

E -> T R
R -> op T {print(op.lexeme)} R | e
T -> num  {print(num.val)}

翻译模式的语法分析树,为每个语义规则构造节点,使用虚线与 production 头部相连:

S-属性定义

如果一个 SDD 仅使用了综合属性,则称其为 S-属性定义;因为属性自底向上计算,所以通常使用自底向上的方法求值。

S-属性定义可以和 LR 分析器结合:

  • 在栈中保存语法符号的属性值
  • 规约时,利用栈中语法符号(production 右部)属性值计算新的(左部符号的)综合属性值
  • 翻译模式:因为调用一个 production 时,右部所有符号的属性都已经计算完毕,所以 production 对应的语义规则都直接添加到末尾即可。
  • 注意仍然需要拓广文法

定义如下变量:

  • val 属性栈
  • top 规约前栈顶
  • ntop 规约后栈顶

那么可以用形如 val[top-?] 访问栈中内容,如 val[ntop] = val[top-2] + val[top]

e.g. 表达式计算语义规则的代码段

L-属性定义

如果一个 SDD 仅使用了综合属性,或所有继承属性只需要其左边符号的属性,则此 SDD 为 L-属性定义
所有 S-属性定义都是 L-属性定义

L-属性定义的属性计算顺序:DFS 遍历分析树,这样如果所有继承属性都只用到左 sibling 的属性,则继承属性可计算:

procedure dfsvisit(n:node)
{  
	for n 的每个孩子,由左至右 do
	{
		计算 m 的继承属性;(利用 n 的继承属性和 m 的左sibling的属性)
		dfsvisit(m);
	}
	计算 n 的综合属性;
}

翻译模式:

  • production 右部符号的继承属性必须在这个符号以前的语义规则中被算出来
  • 语义规则不能(直接或间接地)引用它右边的符号的属性
  • 左部 non-terminal 的综合属性,必须在其依赖的所有属性计算完毕后才能计算,一般放在右部末尾

自顶向下计算属性

基于递归下降的预测分析
要求消除左递归
-> 一般性方法:已知翻译模式

A -> A1Y {A.a = g(A1.a, Y.y)}
A -> X   {A.a = f(X.x)}

消除左递归:

A -> XR
R -> YR1
R -> e

把 A1 消除,增加了 R -> 如何通过 R 传递 A1 的属性?
为 R 设置:

  • 继承属性 R.i 保存中间结果
  • 综合属性 R.s 向上传递最终结果

把中间结果保存在 R.i 中一路向下计算,算出最终结果后,传递给 R.s,然后再一路向上传回根节点。

翻译模式:

A -> X  {R.i = f(X.x)} R {A.a = R.s}
R -> Y  {R1.i = g(R.i, Y.y)} R1 {R.s = R1.s}
R -> e  {R.s = R.i}

e.g.

消除左递归后:

语法树:

翻译模式:为每个 non-terminal 建立一个函数

  • 参数为此 non-terminal 的继承属性
  • 返回值为综合属性
  • production 中语法符号的属性 —— 函数局部变量
  • 函数体结构和递归调用预测分析类似,根据当前输入符号确定使用哪个 production

自底向上计算属性

思路:移走翻译模式中嵌入的语义规则,改写 SDD 为 S 属性定义

没有继承属性的情况

e.g. 已知如下 L-属性定义:

E -> TR
R -> + T {print('+')} R | - T {print('-')} R | e
T -> num {print(num.val)}

语义规则阻碍了此文法成为 S-属性定义
(在移入栈过程中根本不知道将来会形成哪个 production,无法执行嵌入的语义规则)
-> 加入空 production,取走语义规则

E -> T R
R -> + T M R | - T N R | e
T -> num { print(num.val)}
M -> e { print(‘+’) }
N -> e { print(‘-’) }

属性值的计算只在规约时进行

有继承属性的情况

用 C->XYZ 规约时如果需要 C 的继承属性,但 C 不在栈中,就需要寻找 C 在右部的 production(C 的继承属性依赖于其 parent 和左 sibling),但 parent 也不在栈中,进入一个循环
-> 复写规则

e.g.1. 继承属性位置可确定的情况

由于 L.in = T.type,而且 T 是 L 的左 sibling,在对关于 L 的 production 规约、需要 L 的继承属性 in 时,T 一定已经在栈中,可以用 T.type 代替 L.in:

e.g.2. 继承属性位置不可确定的情况

S -> aAC   {C.i = A.s}
S -> bABC  {C.i = A.s}
C -> c     {C.s = g(C.i)}

不确定 B 是否在栈中,就无法确定 A.s 在 top-1/top-2
-> 新建符号 M 作为跳板:

S -> aAC	{C.i = A.s}
S -> bABMC	{M.i = A.s; C.i = M.s;}
C -> c 		{C.s = g(C.i)}
M -> e		{M.s = M.i}

M 作为桥梁衔接 A 和 C,M 访问 top-2 来访问 A.s,这样 C 只要访问 top-1 就必然能够访问到 A.s。

e.g.3. 继承属性使用函数赋值(非复写规则)

S -> aAC {C.i = f(A.s)}

属性栈里只保存了 A.s,没有保存 C.i
-> 新建符号 N 作为跳板:

S -> aANC	{N.i = A.s; C.i = N.s}
N -> e		{N.s = f(N.i)}

添加一个符号保存运算结果为 N.s,这样 C.i 就可以通过 top-1 访问。