‘壹’ 如何通俗易懂地解释编译原理中语法分析的过程
分成词法分析,语法分析(LL算法,递归下降算法,LR算法),语义分析,运行时环境,中间代码,代码生成,代码优化这些部分。其实现在很多编译原理的教材都是按照85,86出版的那本龙书来安排教学内容的,所以那本龙书的内容格式几乎成了现在编译原理教材的定式,包括国内的教材也是如此。一般来说,大学里面的本科教学是不可能把上面的所有部分都认真讲完的,而是比较偏重于前面几个部分。像代码优化那部分东西,就像个无底洞一样,如果要认真讲,就是单独开一个学期的课也不可能讲得清楚。所以,一般对于本科生,对词法分析和语法分析掌握要求就相对要高一点了。
词法分析相对来说比较简单。可能是词法分析程序本身实现起来很简单吧,很多没有学过编译原理的人也同样可以写出各种各样的词法分析程序。不过编译原理在讲解词法分析的时候,重点把正则表达式和自动机原理加了进来,然后以一种十分标准的方式来讲解词法分析程序的产生。这样的做法道理很明显,就是要让词法分析从程序上升到理论的地步。
语法分析部分就比较麻烦一点了。现在一般有两种语法分析算法,LL自顶向下算法和LR自底向上算法。LL算法还好说,到了LR算法的时候,困难就来了。很多自学编译原理的都是遇到LR算法的理解成问题后就放弃了自学。其实这些东西都是只要大家理解就可以了,又不是像词法分析那样非得自己写出来才算真正的会。像LR算法的语法分析器,一般都是用工具Yacc来生成,实践中完全没有比较自己来实现。对于LL算法中特殊的递归下降算法,因为其实践十分简单,那么就应该要求每个学生都能自己写。当然,现在也有不少好的LL算法的语法分析器,不过要是换在非C平台,比如Java,Delphi,你不能运用YACC工具了,那么你就只有自己来写语法分析器。
‘贰’ 编译原理笔记9:语法分析树、语法树、二义性的消除
语法分析树和语法树不是一种东西 。习惯上,我们把前者叫做“具体语法树”,其能够体现推导的过程;后者叫做“抽象语法树”,其不体现过程,只关心最后的结果。
语法分析树是语言推导过程的图形化表示方法。这种表示方法反映了语言的实质以及语言的推导过程。
定义:对于 CFG G 的句型,分析树被定义为具有下述性质的一棵树:
推导,有最左推导和最右推导,这两种推导方式在推导过程中的分析树可能不同,但因最终得到的句子是相同的,所以最终的分析树是一样的。
分析树能反映句型的推导过程,也能反映句型的结构。然而实际上,我们往往不关心推导的过程,而只关心推导的结果。因此,我们要对 分析树 进行改造,得到 语法树 。语法树中全是终结符,没有非终结符。而且语法树中没有括号
定义:
说白了,语法树这玩意,就一句话: 叶子全是操作数,内部全是操作符 ,树里没有非终结符也不能有括号。
语法树要表达的东西,是操作符(运算)作用于操作数(运算对象)
举俩例子吧:
【例】: -(id+id) 的语法树:
【例】:-id+id 的语法树:
显然,我们从上面这两个语法树中,直接就能观察出来它们的运算顺序。
【例】:句型 if C then s1 else s2
二义性问题:一个句子可能对应多于一棵语法树。
【例】: 设文法 G: E → E+E | E*E | (E) | -E | id
则,句子 id+id*id、id+id+id 可能的分析树有:
在该例中,虽然 id+id+id 的 “+” 的结合性无论左右都不会影响结果。但万一,万一“+”的含义变成了“减法”,那么左结合和右结合就会引起很大的问题了。
我们在这里讲的“二义性”的“义”并非语义——我们现在在学习的内容是“语法分析器”,尚未到需要研究语言背后含义的阶段。
我们现在讲的“二义性”指的是一个句子对应多种分析树。
二义性的体现,是文法对同一句子有不止一棵分析树。这种问题由【句子产生过程中的某些推导有多于一种选择】引起。悬空 else 问题就可以很好地体现这种【超过一种选择】带来的二义性问题,示例如下。
看下面这么个例子。。
(其实,我感觉这个其实比较像是“说话大喘气”带来的理解歧义问题。。。)上面的产生式中并没体现出来该咋算分一块,所以两种完全不同的句子结构都是合法的。
二义性问题是有救的,大概有以下这三种办法:
这些办法的核心,其实都是将优先级和结合性说明白。
核心:把优先级和结合性说明白
既然要说明白,那就不能让一个非终结符可以直接在当次推导中能推出会带来优先级和结合性歧义的东西。(对分析树的一个内部节点,不会有出现在其下面的分支是相同的非终结符的情况。如果有得选,那就有得歧义了。没得选才能确定地一路走到黑)
改写为非二义文法的二义文法大概有下面这几个特点:
改写的关键步骤:
【例】改写下面的二义文法为非二义文法。图右侧是要达成的优先级和结合性
改写的核心其实就两句话:
所以能够得到非终结符与运算的对应关系(因为不同的运算有不同的优先级,我们想要引入多个优先级就要引入多个新的非终结符。这样每个非终结符就可以负责一个优先级的运算符号,也就是说新的非终结符是与运算有关系的了。因此这里搞出来了“对应关系”四个字)如下:
优先级由低到高分别是 +、 、-,而距离开始符号越近,优先级越低。因此在这里的排序也可以+ -顺序。每个符号对应一层的非终结符。根据所需要的结合性,则可确定是左递归还是右递归,以确定新的产生式长什么样子
【例】:规定优先级和结合性,写出改写的非二义文法
我们已经掌握了一种叫做【改写】的工具,能让我们消除二义性。接下来我们就要用这个工具来尝试搞搞悬空 else 问题!
悬空 else 问题出现的原因是 then 数量多于 else,让 else 有多个可以结合的 then。在二义文法中,由于选哪两个 then、else 配对都可以,故会引起出现二义的情况。在这里,我们规定 else 右结合,即与左边最靠近的 then 结合。
为改写此文法,可以将 S 分为完全匹配(MS)和不完全匹配(UMS)两类。在 MS 中体现 then、else 个数相等即匹配且右结合;在UMS 中 then、else 不匹配,体现 else 右结合。
【例】:用改写后的文法写一个条件语句
经过检查,无法再根据文法写出其他分析树,故已经消除了二义性
虽然二义文法会导致二义性,但是其并非一无是处。其有两个显着的优点:
在 Yacc 中,我们可以直接指定优先级、结合性而无需自己重写文法。
left 表示左结合,right 表示右结合。越往下的算符优先级越高。
嗯就这么简单。。。
我们其实可以把语言本身定义成没有优先级和结合性的。。然后所有的优先、结合都交由括号进行控制,哪个先算就加括号。把一个过程的结束用明确的标志标记出来。
比如在 Ada 中:
在 Pascal 中,给表达式加括号:
‘叁’ 编译原理, 写一个简单文法的词法/语法分析器有简单的方法吗
那就用 lex 和 yacc 写
不过实验课程就是让你手算一次
如果你用工具直接生成了意义不大
‘肆’ 编译的语法分析
编译程序的语法分析器以单词符号作为输入,分析单词符号串是否形成符合语法规则的语法单位,如表达式、赋值、循环等,最后看是否构成一个符合要求的程序,按该语言使用的语法规则分析检查每条语句是否有正确的逻辑结构,程序是最终的一个语法单位。编译程序的语法规则可用上下文无关文法来刻画。
语法分析的方法分为两种:自上而下分析法和自下而上分析法。自上而下就是从文法的开始符号出发,向下推导,推出句子。而自下而上分析法采用的是移进归约法,基本思想是:用一个寄存符号的先进后出栈,把输入符号一个一个地移进栈里,当栈顶形成某个产生式的一个候选式时,即把栈顶的这一部分归约成该产生式的左邻符号。
‘伍’ 编译原理题,在建立LL(1)语法分析器时,提左因子和消除左递归的目的是什么
消除左递归是因为LL文法不能处理含有左递归的文法。
提左因子只是推后产生式的选择决定,等到获取足够多的输入再作选择。
‘陆’ 编译原理语法分析器程序设计,用C语言或C++,哪里有这个程序
1.文法简略,没有实现的部分,可以在此文法的基础上进行扩充,本程序的采用自顶向下的LL(1)文法。
2.可以自动实现求First
集和
Follow
集。
3.处终结符外(有些硬编码的成分),终结符的文法可以自定义,也就是说读者可以自定义文法。
4.为方便理解,C语言的文法描述写成中文。
5.程序将词法分析和语法分析结合起来,词法分析的结果作为语法分析的输入。
6.最终结果在控制台显示的有:词法分析、First集、Follow集、Select集,在preciateResult.txt
中写入了语法分析结果,在preciateTable.txt
中写入了预测分析表。
7.文法的词素之间必须有空格分开。
‘柒’ 一般设计编译器要将词法分析和语法分析分开的原因是什么
简单性——词法分析技术不如语法分析技术技术复杂,分开之后词法分析过程更简单。(这里还有一些意思差不多的话)
效率——词法分析占用的时间是整个编译时间的一大部分,所以将它们分开有利于优化词法分析,而提高编译效率
可移植性——词法分析通常平台相关,语法分析器可以是平台无关的。分开了对移植有利。
(引自《程序设计语言概念》(第9版) Sebesta着)
‘捌’ 运用编译系统的设计原理,设计并实现编译系统的前端词法分析器和语法分析器
#include <stdio.h>
#include <ctype.h>
#include <malloc.h>
#include <stdlib.h>
#include <string.h>
#define NULL 0
#define MAX_KEY_NUM 10
#define MAX_BORDER_NUM 6
#define MAX_ARITH_NUM 4
#define MAX_RELATION_NUM 6
#define MAX_CONSTS_NUM 20
#define MAX_LABEL_NUM 20
FILE *fp;
char cbuffer;
char *key[MAX_KEY_NUM]={"if","else","for","while","do","return","break","continue","main","int"};
char *border[MAX_BORDER_NUM]={",",";","{","}","(",")"};
char *arithmetic[MAX_ARITH_NUM]={"+","-","*","/"};
char *relation[MAX_RELATION_NUM]={"<","<=","==",">=",">","="};
char *consts[MAX_CONSTS_NUM];
char *label[MAX_LABEL_NUM];
int constnum=0,labelnum=0;
//===============================================
int search(char searchchar[],int wordtype)
{
int i=0;
switch (wordtype) {
case 1:
for (i=0;i<MAX_KEY_NUM;i++)
{
if (strcmp(key[i],searchchar)==0)
return(i+1);
}
case 2:
{for (i=0;i<MAX_BORDER_NUM;i++)
{
if (strcmp(border[i],searchchar)==0)
return(i+1);
}
return(0);
}
case 3:
{for (i=0;i<MAX_ARITH_NUM;i++)
{
if (strcmp(arithmetic[i],searchchar)==0)
{
return(i+1);
}
}
return(0);
}
case 4:
{for (i=0;i<MAX_RELATION_NUM;i++)
{
if (strcmp(relation[i],searchchar)==0)
{
return(i+1);
}
}
return(0);
}
case 5:
{
for (i=0;i<constnum;i++)
{
if(constnum>0)
if (strcmp(consts[i],searchchar)==0)
{
return(i+1);
}
}
consts[i]=(char *)malloc(sizeof(searchchar));
strcpy(consts[i],searchchar);
constnum++;
return(i);
}
case 6:
{
for (i=0;i<labelnum;i++)
{
if (strcmp(label[i],searchchar)==0)
{
return(i+1);
}
}
label[i]=(char *)malloc(sizeof(searchchar));
strcpy(label[i],searchchar);
labelnum++;
return(i);
}
} // end of switch
}
//===============================================
char alphaprocess(char buffer)
{
int atype;
int i=-1;
char alphatp[20];
while ((isalpha(buffer))||(isdigit(buffer)))
{
alphatp[++i]=buffer;
buffer=fgetc(fp);
}
alphatp[i+1]='\0';
if (atype=search(alphatp,1))
printf("%s (1,%d)\n",alphatp,atype);
else
{
atype=search(alphatp,6);
printf("%s (6,%d)\n",alphatp,atype);
}
return(buffer);
}
//===============================================
char digitprocess(char buffer)
{
int i=-1;
char digittp[20];
int dtype;
while ((isdigit(buffer)))
{
digittp[++i]=buffer;
buffer=fgetc(fp);
}
digittp[i+1]='\0';
dtype=search(digittp,5);
printf("%s (5,%d)\n",digittp,dtype);
return(buffer);
}
//===============================================
char otherprocess(char buffer)
{
int i=-1;
char othertp[20];
int otype,otypetp;
othertp[0]=buffer;
othertp[1]='\0';
if (otype=search(othertp,3))
{
printf("%s (3,%d)\n",othertp,otype-1);
buffer=fgetc(fp);
goto out;
}
if (otype=search(othertp,4))
{
buffer=fgetc(fp);
othertp[1]=buffer;
othertp[2]='\0';
if (otypetp=search(othertp,4))
{
printf("%s (4,%d)\n",othertp,otypetp-1);
goto out;
}
else
othertp[1]='\0';
printf("%s (4,%d)\n",othertp,otype-1);
goto out;
}
if (buffer==':')
{
buffer=fgetc(fp);
if (buffer=='=')
printf(":= (2,2)\n");
buffer=fgetc(fp);
goto out;
}
else
{
if (otype=search(othertp,2))
{
printf("%s (2,%d)\n",othertp,otype-1);
buffer=fgetc(fp);
goto out;
}
}
if ((buffer!='\n')&&(buffer!=' '))
printf("%c error,not a word\n",buffer);
buffer=fgetc(fp);
out: return(buffer);
}
//===============================================
void main()
{
int i;
for (i=0;i<=20;i++)
{
label[i]=NULL;
consts[i]=NULL;
};
if ((fp=fopen("c:\example.c","r"))==NULL)
printf("error");
else
{
cbuffer = fgetc(fp);
while (cbuffer!=EOF)
{
if (isalpha(cbuffer))
cbuffer=alphaprocess(cbuffer);
else if (isdigit(cbuffer))
cbuffer=digitprocess(cbuffer);
else cbuffer=otherprocess(cbuffer);
}
printf("over\n");
getchar();
}
}
‘玖’ 简述将源程序编译成可执行程序的过程
一个源程序到一个可执行程序的过程:预编译、编译、汇编、链接。其中,编译是主要部分,其中又分为六个部分:词法分析、语法分析、语义分析、中间代码生成、目标代码生成和优化。
预编译:主要处理源代码文件中的以“#”开头的预编译指令。处理规则如下:
1、删除所有的#define,展开所有的宏定义。
2、处理所有的条件预编译指令,如“#if”、“#endif”、“#ifdef”、“#elif”和“#else”。
3、处理“#include”预编译指令,将文件内容替换到它的位置,这个过程是递归进行的,文件中包含其他文件。
4、删除所有的注释,“//”和“/**/”。
5、保留所有的#pragma 编译器指令,编译器需要用到他们,如:#pragma once 是为了防止有文件被重复引用。
6、添加行号和文件标识,便于编译时编译器产生调试用的行号信息,和编译时产生编译错误或警告是能够显示行号。
(9)编译时语法分析器扩展阅读:
编译过程中语法分析器只是完成了对表达式语法层面的分析,语义分析器则对表达式是否有意义进行判断,其分析的语义是静态语义——在编译期能分期的语义,相对应的动态语义是在运行期才能确定的语义。
其中,静态语义通常包括:声明和类型的匹配,类型的转换,那么语义分析就会对这些方面进行检查,例如将一个int型赋值给int*型时,语义分析程序会发现这个类型不匹配,编译器就会报错。
‘拾’ 求一个尽量完整的编译器:词法分析器+语法分析器
在一个模式被匹配之前,词法分析器往往需要超前扫描该词素后面的若干个字符,使用将字符退回输入流的方法,需要移动大量字符的时间,由于 词法分析器是编译期间唯一需要逐一扫描源程序字符的过程,因此它的效率将极大的影响编译器的性能,因此人们发明了双缓冲区的技术。
双缓冲区技术原理如下:
把一个缓冲区分成前后两个部分,每部分能够容纳N(1024/4096)个字符,每次系统读命令读入N个字符到前半部分或者后半部分,如果剩余的不足N个字符,则在最后增加一个不同于其他任何字符的字符,如eof/#,用于标识源文件的结束。缓冲区包括两个指针beginning和forward,在两个指针之间的字符串就是当前的词素。一开始两个指针都指向第一个字符,然后forward向后扫描,直至发现一个匹配的词素为止。如果forward跨过中间标记,则往后半部分读入N个字符。如果forward指针移过最后位置,则向前半部分读入N个字符,且forward指针重新指向开始继续处理过程。为了处理方便在两个部分的最后都增加一个文件结束标识eof。示意图如下:
______________________________________________________________________
|............for......while.... ........................................ |....int i .................................................. ...................| |_______________________________eof|_______________eof________________eof|
| |
beginning forward
下面是双缓冲区的一个c实现:
#include <stdio.h>
#include <string.h>
#define MAXWORD 1000
struct bibuffer
{
char* buffer[2048]; //缓冲区空间
char* beginning,forward; //前向和后向指针
int count; //前向指针记数
} bbuf;
void parse(char c)
{
if(c=' ')
{
memcpy(word[i],beginning,(size_t)(forward-beginning));
i++;
}
else forward++;
}
int main(int argc,char* argv)
{
File* fp;
char* word[MAXWORD];
int i=0;
buffer=new char[2048];
fp=open("test.c","r");
read(fp,buffer,1023);
buffer[1023]='#';
read(fp,buffer+1024,1023);
buffer[2047]='#';
bbuf->buffer=buffer;
bbuf->beginning=bbuf->forward=bbuf->buffer;
bbuf->count=0;
while(1)
{
forward=forward+1;
if(count==1023)
{
read(fp,buffer+1024,1023);
forward++;
//这个函数的具体代码就要和具体的词法分析规则而定,这里假设只识别空格分割的单词
parse(*forward);
}
else if(count>=2048)
{
read(fp,buffer,1023);
forward=bbuf->buffer;
//这个函数的具体代码就要和具体的词法分析规则而定,这里假设只识别空格分割的单词
parse(*forward);
}
else if(count!=1023&&count<2048&&(*forward)='#')
{
break; //词法分析结束
}
}
}