【编译原理】语法分析LL(1)分析法的FIRST和FOLLOW集

  近来复习编译原理,语法分析中的自上而下LL(1)分析法,需要构造求出一个文法的FIRST和FOLLOW集,然后构造分析表,利用分析表+一个栈来做自上而下的语法分析(递归下降/预测分析),可是这个FIRST集合FOLLOW集看得我头大。。。

  教课书上的规则如下,用我理解的语言描述的:

任意符号α的FIRST集求法:
1. α为终结符,则把它自身加入FIRSRT(α)
2. α为非终结符,则:
(1)若存在产生式α->a...,则把a加入FIRST(α),其中a可以为ε
(2)若存在一串非终结符Y1,Y2, ..., Yk-1,且它们的FIRST集都含空串,且有产生式α->Y1Y2...Yk...,那么把FIRST(Yk)-{ε}加入FIRST(α)。如果k-1抵达产生式末尾,那么把ε加入FIRST(α)   注意(2)要连续进行,通俗地描述就是:沿途的Yi都能推出空串,则把这一路遇到的Yi的FIRST集都加进来,直到遇到第一个不能推出空串的Yk为止。重复1,2步骤直至每个FIRST集都不再增大为止。
任意非终结符A的FOLLOW集求法:
1. A为开始符号,则把#加入FOLLOW(A)
2. 对于产生式A-->αBβ:  (1)把FIRST(β)-{ε}加到FOLLOW(B)  (2)若β为ε或者ε属于FIRST(β),则把FOLLOW(A)加到FOLLOW(B)重复1,2步骤直至每个FOLLOW集都不再增大为止。

老师和同学能很敏锐地求出来,而我只能按照规则,像程序一样一条条执行。于是我把这个过程写成了程序,如下:

数据元素的定义:

 1 const int MAX_N = 20;//产生式体的最大长度
 2 const char nullStr = '$';//空串的字面值
 3 typedef int Type;//符号类型
 4
 5 const Type NON = -1;//非法类型
 6 const Type T = 0;//终结符
 7 const Type N = 1;//非终结符
 8 const Type NUL = 2;//空串
 9
10 struct Production//产生式
11 {
12     char head;
13     char* body;
14     Production(){}
15     Production(char h, char b[]){
16         head = h;
17         body = (char*)malloc(strlen(b)*sizeof(char));
18         strcpy(body, b);
19     }
20     bool operator<(const Production& p)const{//内部const则外部也为const
21         if(head == p.head) return body[0] < p.body[0];//注意此处只适用于LL(1)文法,即同一VN各候选的首符不能有相同的,否则这里的小于符号还要向前多看几个字符,就不是LL(1)文法了
22         return head < p.head;
23     }
24     void print() const{//要加const
25         printf("%c -- > %s\n", head, body);
26     }
27 };
28
29 //以下几个集合可以再封装为一个大结构体--文法
30 set<Production> P;//产生式集
31 set<char> VN, VT;//非终结符号集,终结符号集
32 char S;//开始符号
33 map<char, set<char> > FIRST;//FIRST集
34 map<char, set<char> > FOLLOW;//FOLLOW集
35
36 set<char>::iterator first;//全局共享的迭代器,其实觉得应该用局部变量
37 set<char>::iterator follow;
38 set<char>::iterator vn;
39 set<char>::iterator vt;
40 set<Production>::iterator p;
41
42 Type get_type(char alpha){//判读符号类型
43     if(alpha == '$') return NUL;//空串
44     else if(VT.find(alpha) != VT.end()) return T;//终结符
45     else if(VN.find(alpha) != VN.end()) return N;//非终结符
46     else return NON;//非法字符
47 }

主函数的流程很简单,从文件读入指定格式的文法,然后依次求文法的FIRST集、FOLLOW集

 1 int main()
 2 {
 3     FREAD("grammar2.txt");//从文件读取文法
 4     int numN = 0;
 5     int numT = 0;
 6     char c = ' ';
 7     S = getchar();//开始符号
 8     printf("%c", S);
 9     VN.insert(S);
10     numN++;
11     while((c=getchar()) != '\n'){//读入非终结符
12         printf("%c", c);
13         VN.insert(c);
14         numN++;
15     }
16     pn();
17     while((c=getchar()) != '\n'){//读入终结符
18         printf("%c", c);
19         VT.insert(c);
20         numT++;
21     }
22     pn();
23     REP(numN){//读入产生式
24         c = getchar();
25         int n; RINT(n);
26         while(n--){
27             char body[MAX_N];
28             scanf("%s", body);
29             printf("%c --> %s\n", c, body);
30             P.insert(Production(c, body));
31         }
32         getchar();
33     }
34
35     get_first();//生成FIRST集
36     for(vn = VN.begin(); vn != VN.end(); vn++){//打印非终结符的FIRST集
37         printf("FIRST(%c) = { ", *vn);
38         for(first = FIRST[*vn].begin(); first != FIRST[*vn].end(); first++){
39             printf("%c, ", *first);
40         }
41         printf("}\n");
42     }
43
44     get_follow();//生成非终结符的FOLLOW集
45     for(vn = VN.begin(); vn != VN.end(); vn++){//打印非终结符的FOLLOW集
46         printf("FOLLOW(%c) = { ", *vn);
47         for(follow = FOLLOW[*vn].begin(); follow != FOLLOW[*vn].end(); follow++){
48             printf("%c, ", *follow);
49         }
50         printf("}\n");
51     }
52     return 0;
53 }

主函数

其中文法文件的数据格式为(按照平时做题的输入格式设计的):

第一行:所有非终结符,无空格,第一个为开始符号;

第二行:所有终结符,无空格;

剩余行:每行描述了一个非终结符的所有产生式,第一个字符为产生式头(非终结符),后跟一个整数位候选式的个数n,之后是n个以空格分隔的字符串为产生式体。

示例文件如下:(注:非终结符本应都用大写字母,原题用的是加上标的方法,如E′,但我用char型存每个符号,所以用的是相应的小写字母,如e)

1 EeTtFfP
2 +*()^ab
3 E 1 Te
4 e 2 +E $
5 T 1 Ft
6 t 2 T $
7 F 1 Pf
8 f 2 *f $
9 P 4 (E) ^ a b

求FIRST集的部分:

 1 void get_first(){//生成FIRST集
 2     for(vt = VT.begin(); vt != VT.end(); vt++)
 3         FIRST[*vt].insert(*vt);//终结符的FIRST集包含它自身
 4     FIRST[nullStr].insert(nullStr);//空串的FIRST集包含它自身
 5     bool flag = true;
 6     while(flag){//上一轮迭代中集合有扩大
 7         flag = false;
 8         for(vn = VN.begin(); vn != VN.end(); vn++){//对于每个非终结符
 9             for(p = P.begin(); p != P.end(); p++){
10                 //(*p).print();
11                 if(p->head == *vn){//找所有左部为A的产生式
12                     int before = FIRST[*vn].size();
13                     put_body(*vn, &(p->body)[0]);
14                     if(FIRST[*vn].size() > before)//集合有扩大
15                         flag = true;
16                     //printf("%c size %d -> %d\n", *vn, before, FIRST[*vn].size());
17                 }
18             }
19         }
20     }
21 }

与FIRST集相关的几个辅助函数:

1 void put_first_first(char A, char B){//把FIRST[B]-{$}加到FIRST[A]
2     first = FIRST[B].begin();
3     for(; first != FIRST[B].end(); first++){
4         if(*first != nullStr)
5             FIRST[A].insert(*first);
6     }
7 }

put_first_first

 1 void put_body(char A, char* pb){//用产生式体从pb开始往后的部分扩充A的FIRST集
 2     if(*pb == '\0'){//抵达产生式体的末尾
 3         FIRST[A].insert(nullStr);//向FIRST(A)加入空串
 4         return ;
 5     }
 6     switch(get_type(*pb)){
 7         case 1://pb[0]为非终结符,把pb[0]的FIRST集加到A的FIRST集
 8             put_first_first(A, *pb);
 9             if(FIRST[*pb].find(nullStr) != FIRST[*pb].end())
10                 put_body(A, pb+1);
11             break;
12         case 0://pb[0]位终结符,把pb[0]加到A的FIRST集
13             FIRST[A].insert(*pb);
14             break;
15         case 2: //pb[0]为空,把空串加入A的FIRST集
16             FIRST[A].insert(nullStr);
17             break;
18         default: return ;
19     }
20 }

put_body

 

求FOLLOW集的部分

 1 void get_follow(){//生成FOLLOW集
 2     FOLLOW[S].insert('#');//结束符放入文法开始符号的FOLLOW集
 3     bool flag = true;
 4     while(flag){
 5         flag = false;
 6         for(vn = VN.begin(); vn != VN.end(); vn++){//对于每个非终结符
 7             for(p = P.begin(); p != P.end(); p++){
 8                 //(*p).print();
 9                 char A = p->head;
10                 int i;
11                 for(i=0; (p->body)[i+1] != '\0'; i++){
12                     char B = (p->body)[i];
13                     char beta = (p->body)[i+1];
14                     int before = FOLLOW[B].size();
15                     if(get_type(B) == N){//跟在B后面的可以扩充B的FOLLOW集
16                         put_follow_first(B, beta);
17                         if(get_type(beta) == NUL)//beta为空串
18                             put_follow_follow(B, A);
19                         else if(FIRST[beta].find(nullStr) != FIRST[beta].end())
20                             put_follow_follow(B, A);
21                         if(FOLLOW[B].size() > before) flag = true;
22                         //printf("%c size %d -> %d\n", B, before, FOLLOW[B].size());
23                     }
24                 }
25                 put_follow_follow((p->body)[i], A);
26             }
27         }
28     }
29 }

与FOLLOW集相关的几个辅助函数:

1 void put_follow_first(char B, char beta){//把FIRST[beta]加到FOLLOW[B]
2     first = FIRST[beta].begin();
3     for(; first != FIRST[beta].end(); first++){
4         if(*first != nullStr)
5             FOLLOW[B].insert(*first);
6     }
7 }

put_follow_first

1 void put_follow_follow(char B, char A){//把FOLLOW[A]加到FOLLOW[B]
2     follow = FOLLOW[A].begin();
3     for(; follow != FOLLOW[A].end(); follow++){
4         FOLLOW[B].insert(*follow);
5     }
6 }

put_follow_follow

运行结果(请忽略集合最后一个元素后的逗号。。。):

注:

1. 语法分析的每个终结符号实际上代表一个单词,是从词法分析器获取的,这里为了简化问题所以只用了一个char型表示;而每个非终结符号则是一个语法单元,这里同样用char型表示了;

2. 感觉我的实现稍显复杂,C++的集合操作不太会用(没有找到原生的类似.addAll这样的方法,所以是自己用迭代器一个个加的),考完试用其他语言实现一个更简洁的。

3. 这样的算法用程序实现并不复杂,但是它规则比较多,且退出的条件是“集合不再增大”,手算起来一轮一轮的容易乱。祝我期末好运吧。

时间: 2024-09-14 19:57:47

【编译原理】语法分析LL(1)分析法的FIRST和FOLLOW集的相关文章

编译原理

编译原理 语法是指这样的一组规则,用它可以形成和产生一个合适的程序. 词法规则是指单词符号的形成规则. 语法规则是语法单位的形成规则,规定了如何从单词符号形成更大的结构(即语法单位或语法范畴). 一般程序语言的语法单位有:表达式.语句.分程序.函数.过程和程序等. 程序语言的基本功能是描述数据和对数据的运算.所谓程序,从本质上来说是描述一定数据的处理过程.   强制式语言也称过程式语言.其特点是命令驱动,面向语句.一个强制式语言程序由一系列的语句组成,每个语句的执行引起若干存储单元中的值的改变.

《编译原理实践与指导教程》——1.2 实验指导

1.2 实验指导 词法分析和语法分析这两块,可以说是在整个编译器当中被自动化得最好的部分.也就是说即使没有任何的理论基础,在掌握了工具的用法之后,也可以在短时间内做出功能很全很棒的词法分析程序和语法分析程序.当然这并不意味着,词法分析和语法分析部分的理论基础并不重要.恰恰相反,这一部分被认为是计算机理论在工程实践中最成功的应用之一,对它的介绍也是编译理论课中的重点.但本节指导内容的重点不在于理论而在于工具的使用. 本节指导内容将分别介绍词法分析工具GNU Flex和语法分析工具GNU Bison

基于LLVM的编译原理简明教程 (1) - 写编译器越来越容易了

基于LLVM的编译原理简明教程 (1) - 写编译器越来越容易了 进入21世纪,新的编程语言如雨后春笋一样不停地冒出来.需求当然是重要的驱动力量,但是在其中起了重要作用的就是工具链的改善. 2000年,UIUC的Chris Lattner主持开发了一套称为LLVM(Low Level Virtual Machine)的编译器工具库套件.后来,LLVM的scope越来越大,Low Level Virtual Machine已经不足以表示LLVM的全部,于是,LLVM就变成了正式的名字.LLVM可以

大前端开发者需要了解的基础编译原理和语言知识

在我刚刚进入大学,从零开始学习 C 语言的时候,我就不断的从学长的口中听到一个又一个语言,比如 C++.Java.Python.JavaScript 这些大众的,也有 Lisp.Perl.Ruby 这些相对小众的.一般来说,当程序员讨论一门语言的时候,默认的上下文经常是:"用 xxx 语言来完成 xxx 任务".所以一直困扰着的我的一个问题就是,为什么完成某个任务,一定要选择特定的语言,比如安卓开发是 Java,前端要用 JavaScript,iOS 开发使用 Objective-C

求各位大神帮忙做一下编译原理程序设计

问题描述 求各位大神帮忙做一下编译原理程序设计 1.设计词法分析器 设计各单词的状态转换图,并为不同的单词设计种别码.将词法分析器设计成供语 法分析器调用的子程序.功能包括:具备预处理功能.将不翻译的注释等符号先滤掉,只保留要翻译的符号串,即要求设计一个供词法分析调用的预处理子程序:能够拼出语言中的各个单词:http://ask.csdn.net/#将拼出的标识符填入符号表:返回(种别码, 属性值).2.目标代码生成器c. 能完成指定寄存器个数的情况下将一中间代码程序段翻译成汇编语言目标代码(汇

层次分析法

        层次分析法(Analytic Hierarchy Process,简称AHP)是将与决策总是有关的元素分解成目标.准则.方案等层次,在此基础之上进行定性和定量分析的决策方法.该方法是美国运筹学家匹茨堡大学教授萨蒂于20世纪70年代初,在为美国国防部研究"根据各个工业部门对国家福利的贡献大小而进行电力分配"课题时,应用网络系统理论和多目标综合评价方法,提出的一种层次权重决策分析方法.   应用实例编辑 1.建立递阶层次结构: 2.构造两两比较判断矩阵:(正互反矩阵)   

深入剖析ASP.NET的编译原理之二:预编译(Precompilation)

在本篇文章的第一部分:[原创]深入剖析ASP.NET的编译原理之一:动态编译(Dynamical Compilation),详细讨论了ASP.NET如何进行动态编译的,现在我们来谈谈另外一种重要的编译方式:预编译(Precompilation). 目录 一.为什么要进行预编译 二.In Place Pre-compilation V.S. Pre-compilation for Deployment 三.Non-updatable Pre-compilation V.S. Updatable P

深入剖析ASP.NET的编译原理之一:动态编译(Dynamical Compilation)

Microsoft 的Visual Studio为我们在应用开发中提供的强大功能,我们是有目共睹.借助该工具,是我们的开发 显得更加高效而轻松.从Microsoft把这个IDE的名字从VS.NET 该为VS(比如原来的Visual Studio.NET 2003,现在的版本叫VS2005),可以MS对该IDE的期望和野心:MS要把它改造成一个万能的IDE.不过任何都有其两面性,对于我们广大的开发者来说,VS是我们的各种行为简单化,傻瓜化:但是在另一方面,他也会蒙蔽我们的眼睛,使我们对它背后做的事

编译原理学习.

有时候感觉很无助,迷茫的时候,没有精神支柱的时候.[自暴自弃] 进入人生低谷的时候,找不到方向的时候, 总会出现一些让我兴奋和受到鼓舞的东西. 在一次次的脱变中,发现自己需要学习的东西还很多,很多... .. 我感觉不经历一些事情,就不会学会一些事情,不怕你做错事情,就怕你不肯改错. 我又接触词法分析的另一种词法分析算法[转换表],书中如此描述"理解了此算法思想,也就理解了词法分析器的核心". 仔细看了30分钟,反复琢磨,终于理解了此算法的真谛,让我狠高兴,很兴奋,在编程的学习道路又燃