CH4 静态语义分析
本章着重在于静态语义的分析方法和程序设计语言中的典型结构的翻译方法。
语
义
{
静
态
语
义
分
析
:
读
代
码
动
态
语
义
分
析
:
运
行
起
来
(
不
讨
论
)
语义\begin{cases}静态语义分析:读代码\\动态语义分析:运行起来(不讨论)\end{cases}
语义{静态语义分析:读代码动态语义分析:运行起来(不讨论)
4.1 语法制导翻译
4.1.1 语法与语义
语法:语言的结构,即语言的样子
语义:附着于语言结构上的实际含义,即语言的“意义”。
(1)语法与语义的关系
- 语义不能离开语法而独立存在
- 语法与语义之间没有明确的界限
- 语义远比语法复杂
- 同一个语言可以包含多种含义,不同的语言结构可以表示相同的含义
- “猫吃老鼠”和“老鼠吃猫”
- linux Bash Script中的分支结构和C/C++中的分支结构
(2)语义分析的两个作用
{ 检 查 结 构 正 确 的 句 子 意 思 是 否 合 法 执 行 规 定 语 义 的 动 作 { 表 达 式 求 值 ( 如 1 + 2 直 接 求 值 得 到 3 ) 符 号 表 的 查 询 和 填 写 中 间 代 码 生 成 . . . . . . \begin{cases}检查结构正确的句子意思是否合法\\执行规定语义的动作\begin{cases}表达式求值(如1+2直接求值得到3)\\符号表的查询和填写\\中间代码生成\\......\end{cases}\end{cases} ⎩⎪⎪⎪⎪⎪⎪⎨⎪⎪⎪⎪⎪⎪⎧检查结构正确的句子意思是否合法执行规定语义的动作⎩⎪⎪⎪⎨⎪⎪⎪⎧表达式求值(如1+2直接求值得到3)符号表的查询和填写中间代码生成......
- 检查结构正确的句子意思是否合法
- 执行规定语义的动作,如:
- 表达式求值 1+2 直接求3
- 符号表的查询和填写
- 中间代码生成
4.1.2 属性与语义规则
(1)语法制导翻译的基本思想
基本思想:以语法分析为基础,伴随语法分析的各个步骤,执行相应的语义动作。
具体方法:
- 将文法符号所代表的的语言结构的意思,用附着于该文法符号的属性表示
- 用语义规则产生式所代表的语言结构之间的关系(即属性之间的关系),即用语义规则实现语义(属性)计算
- 语义规则的执行:在语法分析的适当时刻(如推导或者规约)执行附着在对应的产生式上的语义规则(实现对语义的处理,如计算,查填符号表,生成中间代码,发布出错信息等)
(2)属性的抽象表示:.attr
例如:
- E.val(值)
- E.type(类型)
- E.code(代码序列)
- E.place(存储空间)
(3)本章约定
采用二义文法,采用常规意义下的优先级和结合性
(4)属性/语义规则的定义
定义4.1 属性的定义
对于产生式
A
→
α
A\rightarrow \alpha
A→α,其中
α
\alpha
α是由文法符号X1X2…Xn组成的序列,它的语义规则可以表示为(4.1)所示的关于属性的函数f:
b
:
=
f
(
c
1
,
c
2
,
.
.
.
,
c
k
)
(
4.1
)
b:=f(c_1,c_2,...,c_k)\ \ \ (4.1)
b:=f(c1,c2,...,ck) (4.1)
语义规则中的属性存在下面的性质和关系:
-
称4.1中的属性b依赖于属性 c 1 , c 2 , . . . , c k c_1,c_2,...,c_k c1,c2,...,ck。【隐含计算先后次序,被依赖的先计算】
-
若b是A的属性, c 1 , c 2 , . . . , c k c_1,c_2,...,c_k c1,c2,...,ck是 α \alpha α中文法符号的属性,或者A中的其他属性,那么则称b是A的综合属性。自下而上,包含自身
-
若b是某个文法符号Xi的属性, c 1 , c 2 , . . . , c k c_1,c_2,...,c_k c1,c2,...,ck是A的属性或者 α \alpha α中其他的文法符号的属性,那么b是Xi的继承属性。自上而下,包含兄弟
-
若语义规则的形式如下所述,则可将其想象为产生式左部文法符号A的一个虚拟属性。属性之间的依赖关系,在虚拟属性上依然存在。(无返回值,不关注结果)
f ( c 1 , c 2 , . . . , c k ) f(c_1,c_2,...,c_k) f(c1,c2,...,ck)
4.1.3 语义规则的两种表示形式
(1)语法制导定义
用抽象的属性和运算表示的语义规则:公式,做什么
(2)翻译方案
用具体的属性和运算表示的语义规则:程序段,如何做
- 忽略细节,二者等价(类似于概要设计和详细设计)
- 语义规则也被习惯上称为语义动作
Example:
产生式 | 语法制导定义 | 翻译方案1 | 翻译方案2 |
---|---|---|---|
L → E L\rightarrow E L→E | print(E.post) | print_post(post) | |
E → E 1 + E 2 E\rightarrow E_1+E_2 E→E1+E2 | E.post = E_1.post||E_2.post||'+' | post[k]:='+'; k = k + 1 | print('+') |
E → n u m E\rightarrow num E→num | E.post = num.lexval | post[k] = lexval; k = k + 1 | print(lexval) |
上述例子中注意的问题:
- 翻译方案可能不唯一
- print为虚拟属性
- "||"表示拼接
以 3 + 5 + 8为例使用翻译方案1和2分别描述:
(3)属性作为分析树的注释
将属性附着在分析树对应的文法符号上,形成注释分析树。
注意虚拟属性的标注方式。
(4)综合属性与继承属性的计算顺序
注释分析树上看:综合属性是自下而上计算的,继承属性是自上而下计算的
约定:除非特别提醒,本章的语法制导翻译是综合属性,且采用LR分析法
4.1.4 LR分析方案设计
LR分析中的语法制导翻译实质上是对LR语法分析的扩充:
$$ \begin{cases}扩充LR分析器的功能:\begin{cases}当执行规约动作时,也执行产生式对应的语义动作\\由于是规约时执行相应的语义动作,因此限制语义动作仅能放在产生式右部的最右边\end{cases}\\扩充分析栈:增加一个与分析栈并列的语义栈,用于存放分析栈中文法符号对应的属性值\end{cases} $$ Example:3+5*8求值的语法制导翻译:
产生式 | 语法制导定义 | 翻译方案 |
---|---|---|
L → E L\rightarrow E L→E | p r i n t ( E . v a l ) ; print(E.val); print(E.val); | p r i n t ( v a l [ t o p ] ) ; print(val[top]); print(val[top]); |
E → E 1 + E 2 E\rightarrow E_1+E_2 E→E1+E2 | E . v a l : = E 1 . v a l + E 2 . v a l ; E.val:=E_1.val+E_2.val; E.val:=E1.val+E2.val; | v a l [ t o p ] : = v a l [ t o p ] + v a l [ t o p + 2 ] ; val[top] :=val[top] + val[top+2]; val[top]:=val[top]+val[top+2]; |
E → E 1 ∗ E 2 E\rightarrow E_1* E_2 E→E1∗E2 | E . v a l : = E 1 . v a l ∗ E 2 . v a l ; E.val:=E_1.val*E_2.val; E.val:=E1.val∗E2.val; | v a l [ t o p ] : = v a l [ t o p ] ∗ v a l [ t o p + 2 ] ; val[top] :=val[top] * val[top+2]; val[top]:=val[top]∗val[top+2]; |
E → n E\rightarrow n E→n | E . v a l : = n . l e x v a l ; E.val:=n.lexval; E.val:=n.lexval; | v a l [ t o p ] = l e x v a l ; val[top] = lexval; val[top]=lexval; |
4.1.5 递归下降分析方案的设计
递归下降方法是用程序实现对非终结符的展开和终结符的匹配。翻译方案要解决两个问题:
- 在递归下降子程序中,语义动作出现的位置:产生式右部的任何位置
- 如何为文法符号的属性设计存储空间:函数返回值,参数,变量等
e.g. A → B t A\rightarrow Bt A→Bt
A{
<... 继承属性
B()
<...
Match(t)
}
4.3 中间代码简介
中间代码是编译器前端和后端的分水岭。
对中间代码的要求:
- 便于语法制导翻译
- 既与机器指令的结构相近,又与具体机器无关
中间代码的主要形式:
- 树
- 后缀式
- 三地址码
4.3.1 后缀式
(1)后缀式的特征
- 操作数在前,操作符紧跟其后
例如:
- 无需用括号限制运算的优先级和结合性
(2)计算后缀式
算法4.2 后缀式计算
输入:后缀式
输出:计算结果
方法:采用下述过程进行计算,最终结果留在栈中
x := first_token;
while not end_of_exp
loop if x is an operand // 操作数
then
push(x);
else
pop(operands); // 弹出操作数
push(evaluate); // 计算,并将结果进栈
end if;
x := next_token();
end loop;
– 遇到操作数进栈
– 遇到操作符弹出操作数,计算后结果压栈
(3)将后缀式推广到其他语句
后缀式并不局限于算数运算的表达式,可以推广到任何语句,只要遵守操作数在前,操作符紧跟其后的原则即可。
对语句:if e then x else y
后缀式可以改写为:e x y if-then-else (1)
上述表达式中,e,x和y均需要计算。
而实际上,根据条件e的取值x和y只需要计算一个:
e p1 jez x p2 jump p1:y p2; (2)
其中,p1,p2是标号,
e p1 jez
表示e的结果为0(假),那么转向p1,
p2 jump
表示无条件转向p2
与 (1)比较,(2)中将if-then-else分解,首先计算e,根据e的结果是否为真,决定计算x还是计算y。
4.3.2 三地址码
(1)三地址码的直观表示
三地址:不超过3个地址组成的运算
语法 | 语义 |
---|---|
result := arg1 op arg2 | 结果存放在result 中的二元运算arg1 op arg2 |
result := op arg1 | 结果放在result 中的一元运算op arg1 |
op arg1 | 一元运算op arg1 |
result := arg1 | 直接拷贝 |
结构上与汇编指令相似,但又不涉及具体机器
例如:赋值语x := a + b * c
的三地址码序列:
T
1
:
=
b
∗
c
T
2
:
=
a
+
T
1
x
:
=
T
2
T1 := b*c\\ T2 := a + T1\\ x := T2
T1:=b∗cT2:=a+T1x:=T2
注意直观表示与源程序中的表达式/赋值句的区别。
(2)三地址码的种类
(3)三地址码的实现:三元式与四元式
a. 三元式
形式:(i) (op, arg1, arg2)
三地址码:(i) := arg1 op arg2
序号的双重含义:既代表此三元式,又代表存放的结果
Example:表达式x:=a+b*c
的三元式
(
1
)
(
∗
,
b
,
c
)
(
2
)
(
+
,
a
,
(
1
)
)
(
3
)
(
:
=
,
x
,
(
2
)
)
(1)(*,b,c)\\ (2)(+,a,(1))\\ (3)(:=,x,(2))\\
(1)(∗,b,c)(2)(+,a,(1))(3)(:=,x,(2))
- 标识符a,b,c,x分别表示他们的存储位置
- 序号(1),(2)和(3)分别是它们在三元式表中的位置
存放方式:数组结构,三元式在数组中的位置由下标决定
弱点:给代码的优化带来困难
因为代码优化常使用的方法是删除某些代码或者移动某些代码的位置,而一旦进行了代码的删除或者移动,则意味着某三元式的序号会发生变化,从而使得其他三元式中对原序号的引用无效。
b.三元式的语法制导翻译
语法制导翻译的基本步骤:
- 文法符号属性的设计
- 语义规则的设计
- 必要的辅助操作(函数)/变量的设计
<1>文法符号属性设计
- 属性.code:三元式的代码/序号,或者指示标识符的存储单元
- 属性.name:标识符的名字
<2>必要函数/变量的设计
- 函数
trip(op, arg1, arg2)
:生成一个三元式,返回三元式的序号 - 函数
entry(id.name)
:返回标识符在符号表中的位置或者存储位置
<3>语义规则设计
产生式 | 语义规则 |
---|---|
A → i d : = E A\rightarrow id:=E A→id:=E | {A.code := trip(:=, entry(id.name), E.code)} |
E → E 1 + E 2 E\rightarrow E_1+E_2 E→E1+E2 | {E.code := trip(+,E1.code, E2.code)} |
E → E 1 ∗ E 2 E\rightarrow E_1*E_2 E→E1∗E2 | {E.code := trip(*,E1.code, E2.code)} |
E → ( E 1 ) E\rightarrow (E_1) E→(E1) | {E.code := E1.code} |
E → − E 1 E\rightarrow -E_1 E→−E1 | {E.code := trip(@, E1.code,)} |
E → i d E\rightarrow id E→id | {E.code := entry(id.name)} |
Example:生成x:=a+b*c
的三元式
c.四元式
四元式的语法:(i)(op, arg1, arg2, result)
所表示的计算:result := arg1 op arg2
-
三元式与四元式的唯一区别:将由序号表示的运算结果改为:用(临时)变量表示。
-
此改变使得四元式的运算结果与其在四元式序列中的位置无关。
即:四元式是对三元式的改进,将表示计算结果的三元式序号用一个显式的变量表示,从而避免了三元式的值与三元式在三元式组中的位置相关的弱点。
d.四元式的语法制导翻译
<1>属性的设计
1.属性.code:表示存放运算结果的位置(变量)
<2>函数
1.newtemp
:返回一个新的临时变量,如T1,T2等
<3>过程
过程emit(op, arg1, arg2, result)
:生成一个四元式,若为一元运算,则arg2为空
<4>语义规则设计
产生式 | 语义规则 |
---|---|
A → i d : = E A\rightarrow id:=E A→id:=E | {A.code := newtemp; trip(:=, entry(id.name), E.code, A.code)} |
E → E 1 + E 2 E\rightarrow E_1+E_2 E→E1+E2 | {E.code := newtemp; emit(+,E1.code, E2.code, E.code)} |
E → E 1 ∗ E 2 E\rightarrow E_1*E_2 E→E1∗E2 | {E.code := newtemp; emit(*,E1.code, E2.code, E.code)} |
E → ( E 1 ) E\rightarrow (E_1) E→(E1) | {E.code := E1.code} |
E → − E 1 E\rightarrow -E_1 E→−E1 | {E.code := newtemp; trip(@, E1.code, , E.code)} |
E → i d E\rightarrow id E→id | {E.code := entry(id.name)} |
4.3.3 树形表示
(1)树作为中间代码
语法树真实反映句子结构,对语法树稍加修改(加入语义信息),即可作为中间代码的一种形式——注释语法树。
例:赋值句x := (a+b)*(a+b)
的树的中间代码表示:
(2)树的语法制导翻译
<1>文法符号属性设计
- 属性.nptr:指向树节点的指针
<2>必要函数/变量设计
- 函数
mknode(op, nptr1, nptr2)
:生成一个根或者内部节点,节点数据是op, nptr1和nptr2分别指向左右孩子的子树。若仅有一个孩子,则nptr2为空。 - 函数
mkleaf(node)
:生成一个叶子结点
<3>语义规则
产生式 | 语义规则 |
---|---|
A → i d : = E A\rightarrow id:=E A→id:=E | {A.nptr := mknode(:=, mkleaf(entry(id.name)), E.nptr)} |
E → E 1 + E 2 E\rightarrow E_1+E_2 E→E1+E2 | {E.nptr := mknode(+,E1.nptr, E2.nptr)} |
E → E 1 ∗ E 2 E\rightarrow E_1*E_2 E→E1∗E2 | {E.nptr := mknode(*,E1.nptr, E2.nptr)} |
E → ( E 1 ) E\rightarrow (E_1) E→(E1) | {E.nptr := E1.nptr} |
E → − E 1 E\rightarrow -E_1 E→−E1 | {E.nptr := mknode(@, E1.nptr, )} |
E → i d E\rightarrow id E→id | {E.nptr := mkleaf(entry(id.name))} |
(3)树的优化表示
如果树上某些节点有完全相同的孩子结点,则这些结点可以指向同一个孩子,形成一个有向无环图DAG。
DAG与树的唯一区别是多个父亲可以共享一个孩子。
DAG的语法制导翻译与树的语法制导翻译相似,只需要在mknode
和mkleaf
中增加相应的查询功能。
首先查看所要构造的节点是否存在,若存在则无需构造新的结点,直接返回指向已经存在结点的指针即可。
(4)树与其他中间代码的关系
树表示的中间代码与其他形式之间有内在联系。
a. 树与后缀式
树–>后缀式:对树进行深度优先后序遍历,得到的线性序列就是后缀式,或者说后缀式是树的一个线性化序列。
b. 树与三元式/四元式
特点:树的每个非叶子结点和他的儿子节点对应一个三元式或者四元式
方法:对树进行深度优先后序遍历,即得到一个三元式/四元式序列
4.4 符号表简介
符号表的作用:连接声明与引用的桥梁,记住每个符号的相关信息,如作用域和类型等,帮助编译的各个阶段正确有效地工作。
符号表的基本目标:有效记录信息,快速准确查找
- 名字在声明时,相关信息被写进符号表
- 在引用时,根据符号表信息生成相应的可执行语句
即以下的基本要求:
- 正确存储各类基本信息
- 适应不同阶段的要求
- 便于有效地进行查找,插入,删除和修改等操作
- 空间可以动态扩充
4.4.1 符号表条目
在逻辑上:每个声明的符号在符号表中占据一行,称为一个条目,用于存放符号的相关信息。
-
条目内容:名字+属性
词法分析器遇到一个新名字时,就为他建立一个新的条目,并把目前已知的信息填写到属性中,把暂时不知道的属性空起来,等确定以后再填。
条 目 中 的 名 字 不 唯 一 { 不 同 作 用 域 中 的 两 个 变 量 用 同 一 个 名 字 一 个 作 用 域 中 用 一 个 名 字 表 示 两 个 以 上 的 不 同 类 型 的 对 象 条目中的名字不唯一\begin{cases}不同作用域中的两个变量用同一个名字\\一个作用域中用一个名字表示两个以上的不同类型的对象\end{cases} 条目中的名字不唯一{不同作用域中的两个变量用同一个名字一个作用域中用一个名字表示两个以上的不同类型的对象
-
符号表中的内容:保留字,标识符,特殊符号等等
-
多个子表:
(1)不同类别的符号可以放在不同的子表中,如变量名表,过程名表,保留字表
(2)每个作用域一个子表
-
查询符号表的依据:组合关键字
比如有下面的代码:
int x;
{
int x;
struct x {double y, z}; // 引用x
}
在为C/C++构造的符号表中,组合关键字至少包括三项:
名字 + 作用域 + 类型(符号种类)
- 若同一个名字在同一个作用域中有多个(正确)声明,则引用x时需要根据上下文确定x到底指代哪个对象
- 因此在有些程序设计语言中,不允许这样的设计,以简化编译时的处理
4.4.2 构成名字的字符串的存储方式
(1)直接存储:定长数据
名字 | 属性 | |
---|---|---|
sort | proc | … |
a | int | … |
readarray | proc | … |
draw_a_red_line_for_object_a | boolean | …… |
(2)间接存储:变长数据
名字 | 属性 | |
---|---|---|
101 | proc | …… |
106 | int | …… |
108 | proc | …… |
118 | boolean | …… |
间接存储可以扩展到任意属性:
- 任何一个复杂的属性,均可以为其另辟空间(空间本身是任意的复杂结构)
- 并将此空间的存储位置(指针)保留在该属性在条目中的对应位置即可。
4.4.3 名字的作用域
1.范围划分的方式
源程序中的名字可以出现在不同的范围内,并且可以有不同的意义。
-
两种划分范围的方式
-
并列
C语言的过程定义的是并列的,过程内不能再定义过程
-
嵌套
Pascal过程定义是嵌套的:一个过程内部可以定义另一个过程
C/C++允许程序块嵌套
-
2.名字的作用域的概念
- 名字作用域的意义
- 在并列的两个范围内声明的名字的作用域互不相干
- 在嵌套的两个范围内声明的名字其作用域需要制定规则,以使得任何一个名字在任何范围内含义都是无二义的
名字作用域的规则:规定一个名字在什么样的范围内应该表示什么意义的规则
通用程序设计语言应该遵守下面的两条规则:
总方针:
<1>静态作用域规则
编译时就可以确定名字的作用域(读程序时就确定名字的作用域)
总方针下的具体规则:
<2>最近嵌套原则
- 程序块B中声明的名字的作用域包括B;
- 如果x未在B中声明,那么B中的x出现在外围程序块B’的x声明的作用域中,即B’满足:B’有x的声明,并且B’比其他的任何含有x的声明更加接近被嵌套的B.
通俗的说,最近嵌套原则是指:
(在声明处)从外向内看:一个名字的声明在离其最近的内层,包括声明层起作用
(在名字引用处)从内向外看:该引用遇到的一个声明该名字的作用域中。
void main () {
int a = 0, b = 0; /* B0层 */
{
int b = 1; /* B1层, 被B0嵌套 */
{
int a = 2, c = 4, d = 5; /* B2层,被B1层嵌套 */
cout << a << b << endl; /* 结果为2,1 */
}
{
int b = 3; /* B3层,与B2层并列 */
cout << b << endl; /* 结果为0,3 */
}
cout << a << b << endl; /* 结果为0,1 */
}
cout << a << b << endl; /* 结果为0,0 */
}
声明 | 作用域 |
---|---|
int a = 0; | B0 - B2 |
int b = 0; | B0 - B1 |
int b = 1; | B1 - B3 |
int a = 2; | B2 |
int b = 3; | B3 |
4.4.4 线性表
线性表:最简单和最容易实现符号表的数据结构。
线性表可以表示为一个数组或者是单链表。
为了正确反应名字的作用域,线性表应该具有栈的性质,即符号的加入和删除均在线性表的一段(表头/栈顶)进行。
线性表上的操作: 关键字 = 名字 + (当前)作用域
(1)查找
从表头(栈顶)开始,遇到的第一个符合条件的名字。
(2)插入
先查找,若查找到了,那么返回该名字在符号表中的位置,否则加入到表头。
(3)删除
a.暂时:将同一作用域的名字同时摘走,适当保存
b.永久:将同一作用域的名字同时摘走,不再保存
(4)修改
先查找,修改遇到的第一个符合条件的名字的信息。
线性表上操作的效率:
- 查找一个名字
- 成功查找(平均): n + 1 2 \frac{n+1}{2} 2n+1
- 不成功查找: n n n
- 插入一个新名字
- 查找n次再插入到表头
建立x个条目的符号表: c ∑ i = 1 x i = c x ( x + 1 ) / 2 c\sum\limits_{i=1}^{x}i = cx(x+1)/2 ci=1∑xi=cx(x+1)/2
4.4.5 散列表
(1)散列表的构成
将线性表分成m个小表,构造哈希函数,使符号均匀分布在m个子表中。若散列均匀,则时间复杂度会降到原线性表的1/m
其中
h
a
s
h
(
S
1
)
=
h
a
s
h
(
S
2
)
=
i
h
a
s
h
(
S
3
)
=
h
a
s
h
(
S
4
)
=
k
hash(S_1) = hash(S_2) = i\\ hash(S_3) = hash(S_4) = k
hash(S1)=hash(S2)=ihash(S3)=hash(S4)=k
名字挂在两个链上(便于删除操作)
- 散列链:链接所有具有相同hash值的元素,表头在表头数组中
- 作用域链:链接所有在同一作用域中的元素,表头在作用域表中
S1,S2和S4在统一作用域,S3在另一作用域
(2)散列表上的操作
a.查找
首先计算符号的hash值k,然后进入下标为k的子表,在该表中沿着hash link,像查找单链表中的名字一样查找
b.插入
首先查找,以确定要插入的名字是否已经在表中,若不在,那么沿着hash link和scope link插入到两个链中,方法均是插在表头,即两个表均可看做是栈。
c.删除
把以作用域链连在一起的所有元素从当前符号表中删除
- 临时删除:保留作用域链的子表,下次使用时直接加入到散列链中
- 永久删除
(3)散列函数计算
目标:使得符号分布均匀
关键:hash:string→integer
可行方法:除留取余法
4.5 声明语句的翻译
程 序 设 计 语 言 的 分 类 { 声 明 语 句 可 执 行 语 句 程序设计语言的分类\begin{cases}声明语句\\可执行语句\end{cases} 程序设计语言的分类{声明语句可执行语句
声明语句的作用是为可执行语句提供信息以便于执行。
对声明语句的处理,主要是将所需要的信息正确填写进合理组织的符号表中。
声
明
语
句
的
翻
译
{
变
量
声
明
过
程
声
明
声明语句的翻译\begin{cases}变量声明\\过程声明\end{cases}
声明语句的翻译{变量声明过程声明
遇到声明语句:登记到符号表中
遇到可执行语句:查询符号表
4.5.1 变量的声明
变量声明:类型 + 名字
(1)变量的类型定义与声明
类型定义:为编译器提供存储空间大小的信息
变量声明:为变量分配存储空间
不
同
类
型
变
量
的
存
储
空
间
大
小
分
配
{
基
本
类
型
:
编
译
器
知
道
存
储
空
间
大
小
{
i
n
t
:
4
b
y
t
e
s
d
o
u
b
l
e
:
8
b
y
t
e
s
c
h
a
r
:
1
b
y
t
e
…
…
用
户
自
定
义
类
型
(
组
合
数
据
类
型
)
:
编
译
过
程
中
动
态
计
算
不同类型变量的存储空间大小分配\begin{cases}基本类型:编译器知道存储空间大小\begin{cases}int:4bytes\\double:8bytes\\char:1byte\\……\end{cases}\\用户自定义类型(组合数据类型):编译过程中动态计算\end{cases}
不同类型变量的存储空间大小分配⎩⎪⎪⎪⎪⎪⎪⎨⎪⎪⎪⎪⎪⎪⎧基本类型:编译器知道存储空间大小⎩⎪⎪⎪⎨⎪⎪⎪⎧int:4bytesdouble:8byteschar:1byte……用户自定义类型(组合数据类型):编译过程中动态计算
// 先定义后声明
type player = array[1..2] of integer;
matrix = array[1..24] of char;
var c, p : player;
winner : boolean;
display : matrix;
movect : integer;
// 定义与声明同时
var c, p : array[1..2] of integer;
display : array[1..24] of char;
(2)变量声明的语法制导翻译
a.变量声明的文法
D → D ; D (1) // 整个输入是若干声明构成的序列
| id : T (2) // 若干个声明
T → int (3) // 基本类型
| real (4)
| array [num] of T (5) // 数组类型
| ^T (6) // 指针类型
(5)和(6)中有T的递归引用,自定义类型
产生式(5)需要分析输入来动态计算空间大小。
而指针本身占据的空间大小一定是常量。
此文法可以声明多维数组,如数组A的声明形式可以是:
// A:
A : array [d1] of
array [d2] of
...
array [dn] of int
// A2
A2 : array [2] of
array [3] of int
b.填写符号表的语法制导翻译
<1>属性设计
- 全局量offset:记录当前符号存储地址(偏移量,初值设置为0)
- 属性.type:变量的类型
- 属性.width:变量占据的存储空间的大小
<2>过程和函数
- 过程
enter(name, type, offset)
:为type类型的变量name建立符号表条目,并为其分配存储空间(位置)offset array(n, type)
:生成数组类型pointer(type)
:生成指针类型
<3>语义规则
产生式 | 语义规则 |
---|---|
D → D ; D D\rightarrow D;D D→D;D | |
D → i d : T D\rightarrow id :T D→id:T | {enter(id.name, T.type, offset); offset := offset + T.width;} |
T → i n t T\rightarrow int T→int | {T.type := integer; T.width := 4;} |
T → r e a l T\rightarrow real T→real | {T.type := real; T.width := 8;} |
T → a r r a y [ n u m ] o f T 1 T\rightarrow array[num]\ of\ T1 T→array[num] of T1 | {T.type := array(num.val, T1.type); T.width := num.val*T1.width;} |
$T\rightarrow KaTeX parse error: Expected group after '^' at position 1: ^̲T1$ | {T.type := pointer(T1.type); T.width := 4;} |
例:声明的语法制导翻译a : array [10] of int; x : int
4.5.3 过程的定义与声明
1.过程(procedure)
过程头(做什么)+过程体(怎么做)
函数:有返回值的过程
主程序:被操作系统调用的过程/函数
2.过程的三种形式
形 式 { 过 程 定 义 { 过 程 体 过 程 头 过 程 声 明 : 过 程 头 过 程 调 用 形式\begin{cases}过程定义\begin{cases}过程体\\过程头\end{cases}\\过程声明:过程头\\过程调用\end{cases} 形式⎩⎪⎪⎪⎨⎪⎪⎪⎧过程定义{过程体过程头过程声明:过程头过程调用
先声明后引用原则:
- 若过程定义出现在对它的与引用之后或者引用时看不到的地方,则必须在引用前调用前先声明该过程
- 若引用前已经出现定义,则声明可以省略,因为定义已经包括了声明
4.5.3.1 左值与右值
直观上,出现在赋值号左边和右边的量分别称为左值和右值。
实质上:
- 左值:一个量的内存地址(必须具有存储空间)
- 右值:一个量的值(可以仅有值而没有存储空间)
形象地讲:左值是容器,右值是内容
4.5.3.2 参数传递
1.形参与实参
- 声明时的参数称为形参
- 引用时的参数称为实参
2.常见的参数传递形式
- 值调用
- 引用调用
- 复写-恢复调用
- 换名调用
参数传递的本质区别:传递的是左值/右值/实参本身的正文
<1>值调用
-
实参的特点:任何可以作为右值的对象均可作为实参
-
参数传递和过程内对参数的使用原则
- 过程定义中,形参被当做局部量使用,并在过程内部为形参分配存储单元
- 调用过程前,首先计算出实参并将其值(实参的右值)放入(copy)形参的存储单元
-
过程内部对形参单元中的数据直接访问
值调用的特点:
过程内部对参数的修改,不影响实参。
<2>引用调用
- 实参的特点:必须是左值
- 参数传递和过程内对参数的使用原则
- 过程定义中,形参被当做局部量对待,并在过程内部为形参分配存储单元
- 调用前,将作为实参的变量的地址放进形参的存储单元
- 过程内把形参单元中的数据当做地址,间接访问
- 引用调用的特点:过程内部对形参的修改,实际上是对实参的修改
<3>复写-恢复调用
引用调用的缺点:
// --------- 引用调用的副作用的程序实例
#include <iostream.h>
int a=2;
void add_one(int &x){a = x + 1; x = x + 1;}
void main ()
{ cout<<"before: a="<<a<<endl;
add_one(a);
cout<<"after: a="<<a<<endl;
}
本意:a = 2 + 1 = 3
结果:a = 4
-
复写恢复调用的特点:
- 过程内对形参的修改不直接影响实参,避免了副作用——与值调用类似
- 返回时将形参的内容恢复给实参,实现参数的返回——与引用调用类似
-
实参的特点:必须是左值。
-
参数传递和过程内对参数的使用原则:
- 过程定义中,形参被当做局部量对待,并在过程内部为形参分配存储单元
- 调用过程前,首先计算出实参的值(实参的右值)放入形参的存储单元
- 过程内部对形参中的数据单元直接访问
- 返回时将形参的右值放回实参的存储单元
<4>换名调用
- 过程被认为宏,每次对过程的调用,实质上是用过程体替换过程调用,替换中用实参的文字替换体中的形参,这样的方式被称为宏替换或宏展开
- 应区分被调用过程的局部名和调用过程的局部名。可以认为在宏展开前,被调用过程的每个局部名被系统地重新命名为可区别的名字
- 应当保持实参的完整性,可以为实参加括弧
【宏体中的形参名,会用实参中的文本替换:替换的本质,传递的是实参的文本】
example:
#define M(x){x+1};
M(10) // 替换为 10 + 1
宏是有缺陷的。
一种折中的方法是:C++的内联函数
与换名一样高效,消除了换名调用的副作用。
4.5.3.3 作用域信息的保存
<1>过程的作用域
与程序块类似, 在允许嵌套定义的程序设计语言中,相同的名字可以同时出现在不同的作用域中,因此有必要讨论如何设计符号表来存储他们。
遵守两个原则:
- 静态作用域原则:写好就知道作用域
- 最近嵌套原则
定义4.4 设主程序(最外层过程)的嵌套深度 d m a i n = 1 d_{main} = 1 dmain=1;
- 若过程A直接嵌套定义过程B,那么 d B = d A + 1 d_B = d_A + 1 dB=dA+1
- 变量的嵌套深度 = 变量声明时所在过程的深度
program sort; // (省略了参数的声明部分)
var a:array[0..10]of integer;
x:integer;
procedure readarray;
var i:integer;
procedure exchange;
procedure quicksort;
var i,v:integer;
function partition:integer;
var i,j:integer;
过程 | 变量 | 嵌套深度 |
---|---|---|
sort | a, x | 1 |
readarray | i | 2 |
exchange | 2 | |
quicksort | i, v | 2 |
partition | i, j | 3 |
<2>符号表中作用域信息的保存
符号表和过程一一对应:
-
每个过程一个子表
嵌套过程中名字作用域信息的保存,可以用具有多重嵌套关系的符号表来实现,每个过程可以被认为是一个子符号表,或者是符号表树中的一个结点
-
有嵌套关系的结点之间可以使用双向链表连接
正向链表示过程的嵌套关系,逆向链用来实现按作用域对名字的访问
符号表的的组成:
第一行:符号表头部——逆向链(父节点)、对过程体中的所有变量的存储空间大小之和
后面行:所有名字的局部量的声明,以及正向链
符
号
表
组
成
{
第
一
行
{
逆
向
链
过
程
体
中
所
有
变
量
存
储
空
间
的
综
合
后
面
行
{
所
有
名
字
局
部
量
的
声
明
局
部
量
的
正
向
链
符号表组成\begin{cases}第一行\begin{cases}逆向链\\过程体中所有变量存储空间的综合\end{cases}\\后面行\begin{cases}所有名字局部量的声明\\局部量的正向链\end{cases}\end{cases}
符号表组成⎩⎪⎪⎪⎪⎨⎪⎪⎪⎪⎧第一行{逆向链过程体中所有变量存储空间的综合后面行{所有名字局部量的声明局部量的正向链
完整的符号表示意:
<3>语法制导翻译生成符号表
(a)简化的过程定义文法(忽略了参数)
P → D (1)
D → D ; D (2) G4.7
| id : T (3)
| proc id ; D; S (4)
修改文法G4.7使得在分析D之前生成符号表(LR分析)
P → M D (1)
D → D ; D (2)
| id : T (3) G4.8
| proc id ; N D; S (4)
M →ε (5) // 引入标记非终结符,创建符号表
N →ε (6)
(b)全称量、属性与语义函数
全称量:有序对栈(tblptr, offset
)
其中
tblptr
保存指向符号表节点的指针offset
保存对应过程所有局部量的存储空间大小,初值是0
有 序 对 栈 { t b l p t r : 符 号 表 的 指 针 o f f s e t : 相 应 过 程 中 所 有 局 部 量 的 存 储 空 间 大 小 : 初 值 是 0 , 偏 移 量 表 示 有序对栈\begin{cases}tblptr:符号表的指针\\offset:相应过程中所有局部量的存储空间大小:初值是0,偏移量表示\end{cases} 有序对栈{tblptr:符号表的指针offset:相应过程中所有局部量的存储空间大小:初值是0,偏移量表示
栈上的操作:
push(t, o)
,pop
,top(stack)
语义函数/过程
-
函数
mktable(previous)
:建立一个新的符号表节点,并返回指向该节点的指针。参数
previous
是逆向链,指向该节点的前驱,即符号表的外层结点 -
过程
enter(table, name, type, offset)
在table指向的符号表中,为变量name建立新的条目,包括名字的类型和存储位置等
-
过程
addwidth(table, width)
计算table指向的符号表结点中所有条目的累计宽度,并记录在table的头部信息中
-
过程
enterproc(table, name, newtable)
在table指向的符号表节点中,为过程name建立一个新的条目。参数newtable是正向链,指向过程name自身的符号表节点
语义规则
产生式 | 语义规则 |
---|---|
P → M D | {addwidth(top(tblptr), top(offset)); pop;} |
M →ε | {t := mmktable(null); push(t, 0)} |
D → D ; D | |
D → id ; T | {enter(top(tblptr), id.name, T.type, top(offset))} |
D →proc id ; N D1; S | {t := top(tblptr); addwidth(t, top(offset)); pop; enterproc(top(tblptr), id.name, t);} |
N → ε | {t:=mktable(top(tblptr)); push(t,0);} |
(d)语法制导翻译的过程
4.7 数组元素的引用
确定数组元素的存储分配:
- 以第一个元素地址为首地址,分配一个连续空间。
- 确定一个元素地址的两个要素:首地址和相对首地址的偏移量
多维到一维的映射方法:
以行/列为主
考虑3行,3列的二维数组:a[0...2, 2...4]
;
以行为主排列时:
a
[
0
,
2
]
,
a
[
0
,
3
]
,
a
[
0
,
4
]
,
a
[
1
,
2
]
,
a
[
1
,
3
]
,
a
[
1
,
4
]
,
a
[
2
,
2
]
,
a
[
2
,
3
]
,
a
[
2
,
4
]
a[0,2],a[0,3],a[0,4],a[1,2],a[1,3],a[1,4],a[2,2],a[2,3],a[2,4]
a[0,2],a[0,3],a[0,4],a[1,2],a[1,3],a[1,4],a[2,2],a[2,3],a[2,4]
以列为主排列时:
a
[
0
,
2
]
,
a
[
1
,
2
]
,
a
[
2
,
2
]
,
a
[
0
,
3
]
,
a
[
1
,
3
]
,
a
[
2
,
3
]
,
a
[
0
,
4
]
,
a
[
1
,
4
]
,
a
[
2
,
4
]
a[0,2],a[1,2],a[2,2],a[0,3],a[1,3],a[2,3],a[0,4],a[1,4],a[2,4]
a[0,2],a[1,2],a[2,2],a[0,3],a[1,3],a[2,3],a[0,4],a[1,4],a[2,4]
不同的映射方式,使得同一个元素相对于首地址的偏移量不同
例如: a [ 1 , 4 ] a[1,4] a[1,4],偏移量分别为5和7
确定映射的两种方法:
-
由定义数组类型时的语法确定映射方式
a : array[d1] of array[d2] of ... array[dn] of integer; // 引用方式 a[i1, i2, ... , in] 或 a[i1][i2]...[in]
-
由编译器确定映射方式
a : array[d1, d2, ... , dn] of integer
4.7.1 元素地址的计算
三个假设条件:
- 数组元素以行为主存放
- 数组每维下标的下界为1
- 每个元素占w个存储单元
约定:
- 数组的声明: A [ d 1 , d 2 , . . . , d n ] A[d1, d2, ... , dn] A[d1,d2,...,dn]
- 数组元素的引用: A [ i 1 , i 2 , . . . , i n ] A[i1, i2, ... , in] A[i1,i2,...,in]
一维数组A[d1]元素地址的计算:
a
d
d
r
(
A
[
i
]
)
=
a
+
(
i
−
1
)
∗
w
addr(A[i]) = a + (i-1)*w
addr(A[i])=a+(i−1)∗w
二维数组A[d1,d2]元素地址计算:
a
d
d
r
(
A
[
i
1
,
i
2
]
)
=
a
+
(
(
i
1
−
1
)
∗
d
2
+
(
i
2
−
1
)
)
∗
w
addr(A[i_1, i_2]) = a + ((i_1- 1)*d_2 + (i_2-1)) *w
addr(A[i1,i2])=a+((i1−1)∗d2+(i2−1))∗w
前
i
1
−
1
i_1-1
i1−1行加上本行的前
i
2
−
1
i_2-1
i2−1个
三维数组A[d1, d2, d3]元素地址计算:
a
d
d
r
(
A
[
i
1
,
i
2
,
i
3
]
)
=
a
+
(
(
i
1
−
1
)
∗
d
2
∗
d
3
+
(
i
2
−
1
)
∗
d
3
+
(
i
3
−
1
)
)
∗
w
addr(A[i_1,i_2,i_3]) = a + ((i_1-1)*d_2*d_3 + (i_2 - 1)*d_3 + (i_3-1))*w
addr(A[i1,i2,i3])=a+((i1−1)∗d2∗d3+(i2−1)∗d3+(i3−1))∗w
1,2,3维数组元素的地址计算
{
a
d
d
r
(
A
[
i
]
)
=
a
+
(
i
−
1
)
∗
w
a
d
d
r
(
A
[
i
1
,
i
2
]
)
=
a
+
(
(
i
1
−
1
)
∗
d
2
+
(
i
2
−
1
)
)
∗
w
a
d
d
r
(
A
[
i
1
,
i
2
,
i
3
]
)
=
a
+
(
(
i
1
−
1
)
∗
d
2
∗
d
3
+
(
i
2
−
1
)
∗
d
3
+
(
i
3
−
1
)
)
∗
w
\begin{cases}addr(A[i]) = a + (i-1)*w\\addr(A[i_1, i_2]) = a + ((i_1- 1)*d_2 + (i_2-1)) *w\\ addr(A[i_1,i_2,i_3]) = a + ((i_1-1)*d_2*d_3 + (i_2 - 1)*d_3 + (i_3-1))*w\end{cases}
⎩⎪⎨⎪⎧addr(A[i])=a+(i−1)∗waddr(A[i1,i2])=a+((i1−1)∗d2+(i2−1))∗waddr(A[i1,i2,i3])=a+((i1−1)∗d2∗d3+(i2−1)∗d3+(i3−1))∗w
n维数组元素的地址计算:
A
[
d
1
,
d
2
,
.
.
.
,
d
n
]
=
a
d
d
r
(
A
[
i
1
,
i
2
,
.
.
.
,
i
n
]
)
=
a
−
(
d
2
∗
d
3
∗
.
.
.
∗
d
n
+
1
)
∗
w
+
(
i
1
∗
d
2
∗
d
3
∗
.
.
.
∗
d
n
+
i
2
∗
d
3
∗
d
4
∗
.
.
.
∗
d
n
+
.
.
.
+
i
n
−
1
∗
d
n
+
i
n
)
∗
w
=
a
−
c
∗
w
+
v
∗
w
=
C
O
N
S
P
A
R
T
+
V
A
R
P
A
R
T
A[d_1,d_2,...,d_n] = addr(A[i_1, i_2, ..., i_n]) \\= a-(d_2*d_3*...*d_n+1)*w+(i_1*d_2*d_3*...*d_n+i_2*d_3*d_4*...*d_n+...+i_{n-1}*d_n+i_n)*w\\ =a-c*w+v*w=CONSPART+VARPART
A[d1,d2,...,dn]=addr(A[i1,i2,...,in])=a−(d2∗d3∗...∗dn+1)∗w+(i1∗d2∗d3∗...∗dn+i2∗d3∗d4∗...∗dn+...+in−1∗dn+in)∗w=a−c∗w+v∗w=CONSPART+VARPART
4.7.2 数组元素引用的语法制导翻译
数组元素的寻址:CONSTPART[VARPART],或简写:T1[ T ]
数组元素的两种操作:
- 三地址码:取值
X := T1[T]
- 赋值:
T1[T] = X
(1)引入数组元素后的赋值句文法
A → V := E
V → id | id[EL] (G4.10)
EL→ E | EL ,E
E → E + E | ( E ) | V
引入了新的非终结符V,使得分析树增高。
【自下而上分析】根据此文法进行语法制导翻译有困难,无法使用递归公式(因为dj需要知道数组名)
修改文法以适应递归公式的计算:
A → V := E (1) G4.11
V → id (2)
| EL ] (3) —— 完成数组元素的分析和对v的计算
EL→ id [ E (4) ——得数组名,第一维下标,即得到v1
| EL , E (5) ——递归计算第i维的vi
E → E + E (6)
| ( E ) (7)
| V (8)
(2)属性和函数
属性
- 属性.array:数组名在符号表中的入口——数组空间的首地址
- 属性.dim:数组维数计数器,记录当前分析到的维数
- 属性.place:根据不同给的文法符号有不同
- 下标列表EL:存放 v j = v j − 1 ∗ d j + i j ( j = 2 , 3 , . . . , n ) v_j = v_{j-1}*d_j+i_j(j=2, 3, ..., n) vj=vj−1∗dj+ij(j=2,3,...,n)的地址(临时变量)
- 简单变量id:仍然表示简单变量的地址
- 数组元素id[EL]:存放不变部分,一般可以是一个临时变量
- 属性.offset:保存数组元素地址中的可变部分,简单变量的offset为空,可以记做null
函数
函数limit(array, k)
:计算并返回数组array中的第k维成员个数
d
k
d_k
dk
语义规则
产生式 | 语义规则 |
---|---|
$A → V := E $ | {if V.offset = null then emit(V.place := E.place) ;else emit(V.place[V.offset] := E.place); end if;} // 四元式的创建 |
V → i d V → id V→id | V.place := entry(id.name); V.offset := null; |
V → E L ] V→EL] V→EL] | V.place := newtemp; emit(V.place := EL.array - C);// C的信息记录在内情向量中 V.offsset := newtemp; emit(V.offset := EL.place*w) |
$EL→id[E $ | {EL.place = E.place;// 记下第一下标的信息 El.dim = 1;EL.array:=entry(id.name)} |
E L → E L 1 , E EL→EL1,E EL→EL1,E | {T := newtemp; k := EL1.dim+1; dk = limit(EL1.array, k); emit(T := EL1.place * dk); emit(T := E.place + T); EL.array := EL1.array; EL.place := T; EL.dim == k;} |
E → E 1 + E 2 E→E1+E2 E→E1+E2 | {T := newtemp; emit(T := E1.place + E2.place); E.place := T;} |
E → ( E 1 ) E→(E1) E→(E1) | {E.place := E1.place;} |
E → V E→V E→V | {if V.offset := null then E.place := V.place;} else T := newtemp; emit(T:=V.place[V.offset]); E.place := T; end if;} |
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-goXctqyo-1644385417731)(C:/Users/%E5%AD%99%E8%95%B4%E7%90%A6/AppData/Roaming/Typora/typora-user-images/image-20220208222246934.png)]