6&8. 语义分析和中间代码生成
从几个问题说起:
要计算 3+4*5 的值如何计算?按照以前的方法,我们会想到利用符号栈和数值栈来完成这个运算。但是有了语法分析之后我们不再需要进行这么复杂的代码构造了。第一,我们可以使用自顶而下的递归下降分析方法;第二,我们可以使用自底而上的LR分析方法;而这一切的前提都是我们有能力将一些问题改成形式化的描述,即构造其文法;再使用语法分析的方法,在语法分析的基础上,再特定位置加上相应的处理功能——语义动作。
语法制导翻译(具体例子在下文)
所谓语法制导翻译就是在解析输入的字符串的时候,在特定位置执行指定的动作;换言之,根据语法把输入的字符串”翻译“成一串动作。方法有四种:
-
方法一:在确定的递归下降语法分析程序中,利用隐含堆栈存储各递归下降函数内的局部变量所表示的语义信息。
-
方法二:在自底向上语法分析程序中使用和语法分析栈同步操作的语义栈进行语法分析翻译**(实际上是每个规则都有对应的语义动作)**
-
方法三:在LL(1)语法分析程序中,利用翻译文法实施语法分析翻译。
- 翻译文法是在描述语言的文法(即源文法或输入文法)中加入语义动作符号而形成的。
-
方法四:利用属性文法进行语法分析翻译。
- 属性文法也是一种翻译文法
- 其符号(文法符号和动作符号)都扩展为带有语义属性和同一规则内各属性间的运算规则。(一般的程序设计语言都必须有,因为要同一类型的操作数才能运算)
有关语义分析的基本概念
语法分析的实施
- 语法分析的时候直接调用语义分析函数进行语义分析(语法制导翻译);
- 先生成相应的语法树,再做语义分析;
语义分析通常包括两个方面
- 检查语法结构的静态语义,通常是分析句子的作用域和类型;
- 对合乎语义的句子进行翻译,并生成某种中间形式(树、后缀表示、三元组、四元组、P代码);
语句的种类
- 说明语句:用于定义各种名字的属性;
- 执行语句:用于完成指定功能;
语义分析的种类
- 分析说明语句:需要把名字的各种属性登记到符号表,以便在分析的时候使用到;
- 分析执行语句:通过分析该语句生成相应的中间代码;
语义分析的例题(2018级考试题)
题目:小明发明了一种新的类型声明语句,形式如下,请你写出其对应的文法及语义分析程序。
i:float
i,j,k:int
文法规则:
S->V
V->i,V | i :T
T->int |float|double|char
ep 2.设计语法分析程序
void V()
{
match(token);
if(token==',')
{
match(token);
V();
}
else if(token==':')
{
match(token);
T();
}
else
{
error();
}
}
void T()
{
if(token == "int"||token=="float"||token=="char")
{
match(token);
}
else
{
error();
}
}
ep 3.添加语义动作
void V()//V-> i [ ,V(1) | :T ]
{
match(i);
enter(i);
if(token==',')
{
match(token);
V();
i.type=V(1).type;
}
else if(token==':')
{
match(token);
T();
Fill(entry(i),T.type);
V.type=T.type;
}
else
{
error();
}
}
void T()
{
if(token == "int"||token=="float"||token=="char")
{
match(token);
T.type=token;
}
else
{
error();
}
}
常见的中间代码形式
中间代码是一种结构简单、含义明确的记号系统,复杂性介于源代码和机器代码之间,容易将其翻译成目标代码。
形式:
- 树
- 后缀表示
- 三元组
- 四元组
- P代码
如何将一个算数表达式转换为逆波兰表示(后缀表达式)、四元组表示、三元组表示
课件以逆波兰表示为例子解释。
自顶而下递归下降
Step 1.构造文法
为了简单考虑,把算数表达式简化为如下的简单文法
这里需不需要提取左公因子和消除左递归,在于选用什么样的方法做语义分析,可以是个自顶而下的递归下降、LL(1)分析方法,也可以是自底而上的LR分析方法等。这里选择用自顶而下的方法,则必须提取左公因子和消除左递归。改造后的文法如下:
Step 2.设计语法分析程序
void E()
{
T();
while(token == '+')
{
match(token);
T();
}
}
void T()
{
F();
while(token == '*')
{
match(token);
F();
}
}
void F()
{
if(token == '(')
{
match('(');
E();
match(')');
}
else if(token == n)
{
match(token);
}
else
{
Error();
}
}
Step 3. 添加语义动作
void E()
{
T();
while(token == '+')
{
match(token);
T();
ans[pos++] = '+';//语义动作
}
}
void T()
{
F();
while(token == '*')
{
match(token);
F();
ans[pos++] = '*';//语义动作
}
}
void F()
{
if(token == '(')
{
match('(');
E();
match(')');
}
else if(token == n)
{
match(token);
ans[pos++] = n;//语义动作
ans[pos++] = '@';//语义动作
}
else
{
Error();
}
}
自底而上LR分析方法
画出DFA图之后,很容易发现存在移进-归约冲突,不属于LR(0)文法,尝试使用SLR(1),可以解决,因此,是SLR(1)文法。画出SLR(1)分析表,在归约的地方写语义动作。(实际上每一条规则都会有其语义动作,只不过有可能是空)
语义动作
-
E → E + T
//转后缀表达式 { post[p++] = '+'; }
-
E → T
-
T → T * F
//转后缀表达式 { post[p++] = '*'; }
-
T → F
-
F → (E)
-
F → n
//转后缀表达式 { pos[p++] = n; }
至于为什么在3归约到E的时候继续读取+,是因为DFA图决定的,又或者说文法决定的。本质上是遇到了+需要移进。以上还可以改成求其结果。
如何将一段代码翻译成中间代码(后缀、三元组、四元组)(主要是四元组)
扩充后缀表达式
-
算数后缀表达式
-
数组运算后缀表达式
a[e] → e a SUBS
其中SUBS表示数组下标运算
-
条件语句后缀表达式
if ( u ) S1 else S2 → u L1 BZ S1 L2 BR S2
-
BZ为双目运算符,表示当u不成立(为零)时转向标号L1部分继续执行
-
L1表示语句S2开始执行的位置
-
BR为一个单目运算符,表示无条件转向L2部分继续执行
-
L2表示该条件语句下一个语句开始执行的位置
-
-
while语句后缀表达式
while (u) s →
L1
u L2 BZ s L1 BR
L2
跳转再执行语句变成执行语句再跳转
-
repeat语句后缀表达式
repeat s until u →
L1
s u L2 BZ L1 BR
L2
本质上代码的后缀表达式是根据汇编代码改变来的,遵循逆波兰表示法原则:运算对象顺序不变;符号在后;
以if来看:
u
BZ L1
S1
BR L2
L1: S2
L2:...
以逆波兰表示原则,则 u L1 BZ S1 L2 BR S2
四元组的表示方法
四元组的表示方法
(OP , P1 , P2 , T)
其中OP为运算符,OP1、OP2 为运算对象,T为计算结果的临时暂存变量。
( + , b , c , T1)
(*, a , T1, T2)
扩充表示
**赋值语句 a=e **
先将e转换为四元组,在将 = 运算转换为四元组。
如a=b+c,则四元组表示为:
(+, b , c , T1)
(= ,T1 , ,a)
关系比较语句
a rop b ( rop 表示< 、> 、<>、>=、<=、=等关系比较运算符)
按照汇编的生成就行(J, , , ?)无条件跳转到?的位置
理解课件中逻辑表达式、if语句和while语句需要做文法分割的地方。分割的意义是为了能够尽早地做语义动作,不要拖到后面一个大的归约项再做。
- 如何判断在哪里分割文法?
- 可以选择先转成汇编代码,需要标号的地方必定是需要分割的。
- 根据经验总结判断,(E) 的 ) 处需要分割、逻辑表达式的 && 和 || 需要分割、if语句的 else 需要分割、while语句的 while 需要分割、repeat语句的 repeat 和 until 需要分割,其中until处需要改变跳转的方向、dowhile语句的 do 和 while 需要分割,其中while处需要决定跳转的标号。
- 如何判断要做怎样的语义动作?
- 可以先画出语句归约的路线,通过经验或者汇编代码知道哪里需要传递?传递什么?如何传递?以及不是每一个东西都需要传递。
注意:大部分只有一个链的非终结符,如:S、C、T等,他们都是带着FC的出口,不会带着TC的出口,因为TC大概率都会在归约到C、T时填入。而BT、BE、BF都是有自己的两个出口。
递归下降+语义动作练习:
下面一些题目是根据课件给出的代码语句题目的练习,在课件中用的是LR方法+语义动作,下面将利用递归下降+语义动作的练习。并不是说只有LR方法才需要切割文法,递归下降也需要隐式地做切割,在切割处加上语义动作。
题目1: if文法用递归下降生成四元组(这只是核心,S还有别的规则才能终止,并且exp也有一套文法)
step 1. 文法如下:
step 2. 递归下降
void S()
{
match(IF);
match('(');
exp();
match(')');
S();
if(token == ELSE)
{
match(token);
S();
}
}
step 3. 加语义动作
ListNode* S()
{
ListNode* s;
ListNode* p1;
ListNode* p2;
ListNode* p3;
ListNode* t;
ListNode* c;
match(IF);
match('(');
p1 = exp();
match(')');
//-----对应C->if(exp)的语义动作------
BackPatch(p1.TC, nextStat);
c = new ListNode();
c.chain = p1.FC;
p2 = S();
if(token == ELSE)
{
match(token);
//-----对应T->CS else的语义动作------
int q = nextStat;
Gen(j,_,_,0);
BackPatch(c.chain, nextStat);
t = new ListNode();
t.chain = merge(p2.chain, q);
p3 = S();
//--------对应S->TS的语义动作------------
s = new ListNode();
s.chain = merge(t.chain, p3.chain);
}
else
{
//-------对应S->CS的语义动作-------
s = new ListNode();
s.chain = merge(c.chain, p2.chain);
}
return s;
}
题目2: while文法用递归下降生成四元组(这只是核心,S还有别的规则才能终止,并且exp也有一套文法)
step 1. 文法如下:
step 2. 递归下降
void S()
{
match(WHILE);
match('(');
exp();
match(')');
S();
}
step 3. 加语义动作
ListNode* S()
{
ListNode* s,* e;ListNode* w;
ListNode* s1;
match(WHILE);
//---W->while----
int head = nextStat;
match('(');
e = exp();
match(')');
//----Wd->W(exp)-----
BackPatch(e.TC, nextStat);
w = new ListNode();
w.chain = e.FC;
w.head = head;
s1 = S();
//------S->Wd S-------
s = new ListNode();
BackPatch(s1.chain, w.head);
Gen(j,_,_,w.head);
s.chain = w.chain;
return s;
}
题目3: repeat文法用递归下降生成四元组(这只是核心,S还有别的规则才能终止,并且exp也有一套文法)
step 1. 文法如下:
通过快速顺一遍发现需要回到repeat的位置,所以repeat处要分割;until处要把S的假出口链回填,所以也要分割。(exp)的 ) 按照if和while的经验可知需要分割出真假出口。
step 2. 递归下降
void S()
{
match(REPEAT);
S();
match(UNTIL)
match('(');
exp();
match(')');
}
step 3. 加语义动作
ListNode* S()
{
ListNode* s;
ListNode* s1;
ListNode* e;
match(REPEAT);
//-----R->repeat-----
int head = nextStat;
s1 = S();
match(UNTIL)
//------Ru->RS until-----
BackPatch(s1.chain, nextStat);
match('(');
e = exp();
match(')');
//-------S->Ru (exp)--------
BackPatch(e.FC, head);
s = new ListNode();
s.chain = e.TC;
return s;
}
题目4: do…while文法用递归下降生成四元组(这只是核心,S还有别的规则才能终止,并且exp也有一套文法)
step 1. 文法如下:
和repeat几乎相同,只是需要改变while的真假出口的四元组。
step 2. 递归下降
void S()
{
match(DO);
S();
match(WHILE);
match('(');
exp();
match(')');
}
step 3. 加语义动作
ListNode* S()
{
ListNode* s;
ListNode* s1;
ListNode* e;
match(DO);
//-----D->do-----
int head = nextStat;
s1 = S();
match(WHILE);
//------DW->do S while-----
BackPatch(s1.chain, nextStat);
match('(');
e = exp();
match(')');
//-------S->DW (exp)--------
BackPatch(e.TC, head);
s = new ListNode();
s.chain = e.FC;
return s;
}
题目5: for文法用递归下降生成四元组(这只是核心,S还有别的规则才能终止,并且exp也有一套文法)
step 1. 文法如下:
exp1;处需要分割,记录exp2的开始位置,为了后面进行循环的时候回到此处。exp2;需要判断是否符合条件,要传递真出口链和假出口链和和记录exp3的开始位置;) 要生成一个跳转回exp2的开始位置的无条件跳转,回填exp2的真出口,传递exp2的假出口链和exp3的开始位置;最后的S时,要生成一个跳转回exp3的开始位置的无条件跳转,回填第二个S的假出口为exp3的开始位置,传递exp2的假出口链。
step 2. 递归下降
void S()
{
match(FOR);
match('(');
S();
match(';');
int head= nextstat;
t= exp();
match(';');
f.fc=t.fc;
f.tc=t.tc;
exp();
match(')');
S();
}
step 3. 加语义动作
ListNode* S()
{
ListNode* s;
ListNode* e1;
ListNode* e2;
ListNode* e3;
ListNode* f;
ListNode* fd;
ListNode* s1;
match(FOR);
match('(');
e1 = S();
match(';');
//----FF->for(exp;-------
int head = nextStat;
//int head = nextStat;
e2 = exp();
match(';');
//----F->FF exp;-------
int head2 = nextStat;
f = new ListNode();
f.TC = e2.TC;
f.FC = e2.FC;
f.head = head;
f.head2 = head2;
e3 = exp();
match(')');
//-----Fd->F exp)------
fd.head2 = f.head2;
fd.chain = f.FC;
Gen(j,_,_,f.head);
BackPatch(f.TC, nextStat);
s1 = S();
//-----S->Fd S---------
Gen(j,_,_,fd.head2);
s = new ListNode();
s.chain = fd.chain;
BackPatch(s1.chian, fd.head2);
return s;
}
题目6: 赋值语句的文法
step 1. 文法如下:
step 2. 递归下降
void S()
{
if(first(T).find(token) != first(T).end())
{
T();
}
match(ID);
match('=');
exp();
match(';');
}
void T()
{
switch(token)
{
case INT:
match(token);
break;
case FLOAT:
match(token);
break;
case DOUBLE:
match(token);
break;
case CHAR:
match(token);
break;
default:
Error();
break;
}
}
step 3. 加语义动作
ListNode* S()
{
ListNode* s;
ListNode* t;
ListNode* e;
if(first(T).find(token) != first(T).end())
{
t = T();
int i = entry(ID);
fill(i, t.type);
match(ID);
}
int i = entry(ID);
match(ID);
match('=');
e = exp();
match(';');
Gen('=',i,_,e.val);
return s;
}
ListNode* T()
{
ListNode* t;
switch(token)
{
case INT:
t.type = token;
match(token);
break;
case FLOAT:
t.type = token;
match(token);
break;
case DOUBLE:
t.type = token;
match(token);
break;
case CHAR:
t.type = token;
match(token);
break;
default:
Error();
break;
}
return t;
}
三元组的表示方法
三元组的表示方法
(OP , P1 , P2 )
其中OP为运算符,P1、P2 为运算对象.
• 用元组编号来来代表结果保存的位置。
• 对于表达式a*(b+c)对应的三元组为:
(1) ( + , b , c )
(2) (*, a , (1))
这里的(1)指的就是三元组(1)
语法制导翻译例题(2018级考试题)
六、综合分析设计题 (矩阵连乘)
当有多个矩阵进行运算时,各个矩阵之间的运算顺序可影响到最终的运算次数。如 A 为 50×20 的矩阵,B为 20×5 的矩阵,C为 5×30 的矩阵,那么 (AB)C 的运算次数为12500次,A(BC) 的运算次数为33000次,下面请你用编译原理的方法完成这一类式子的计算和判别功能,写出相关的思路和代码,要求的输入输出格式如下:
注意:不用编译原理相关的方法最多得3分!!
Input:
(ABC
(AB)C
A(BC)
Output:
No
12500
33000
Step 1. 构建文法
套用算术表达式的文法模型,进行改造。(这里直接套了没有左递归的文法规则)
a表示基本正则表达式,这里代表的是任意的矩阵。
如果认为空字符串不合法,这里的第一条规则应该是T{T}
Step 2. 设计语法分析程序
void E()
{
while(token!='\0')
{
T();
}
}
void T()
{
if(token == '(')
{
match('(');
E();
match(')');
}
else if(token == a)
{
match(token);
}
else
{
Error();
}
}
void Error()
{
cout<<"No"<<endl;
}
Step 3. 添加语义动作并完善主程序和其他必要的元素
int ans = 0;//全局变量
struct Matrix//矩阵的结构体
{
char name;
int row = 0;
int col = 0;
Matrix(char n, int r, int c)
{
name = n;
row = r;
col = c;
}
};
map<char, Matrix> mm;//存储字符和其规模
void match(char c)//匹配函数
{
if(token != c)
{
cout<<"No"<<endl;
}
token = getToken();//不是语法分析程序中的函数,功能是获取下一个字符
}
Matrix* E()
{
Matrix* m = new Matrix('@', 0, 0);
while(token!='\0' && token != ')')
{
Matrix* p = T();
//-----------------语义动作--------------
ans += m.row * m.col * p.col;
if(m.row = 0) m.row = p.row;
m.col = p.col;
//--------------------------------------
}
return m;
}
Matrix* T()
{
Matrix* m;
if(token == '(')
{
match('(');
m = E();
match(')');
}
else if(token == a)
{
match(token);
m = mm[a];//语义动作
}
else
{
Error();
}
return m;
}
void Error()
{
cout<<"No"<<endl;
}
void main()
{
getData();//预处理,获取矩阵的名称、行和列,这里没有实现,具体要看输入行和列时是怎么样的
getToken();//不是语法分析程序中的函数,功能是获取下一个字符
Matrix* m = E();//实际上这个m用不上
cout<<ans<<endl;//输出结果
}
复习课例题
2进制数转10进制数
Step 1. 构建文法
Step 2. 设计语法分析程序
void E()
{
if(token == 0 || token == 1)
{
D();
E();
}
else if(token == EOF)
{
}
else
{
Error();
}
}
void D()
{
if(token == '0' || token == '1')
{
match(token);
}
else
{
Error();
}
}
Step 3. 添加语义动作并完善主程序和其他必要的元素
int E()
{
int temp = 0;
if(token == 0 || token == 1)
{
int d = D();
int e = E();
temp = d*2 + e;
}
else if(token == EOF)
{
}
else
{
Error();
}
return temp;
}
int D()
{
int temp;
if(token == '0' || token == '1')
{
match(token);
temp = atoi(token);
}
else
{
Error();
}
return temp;
}