C语言的语法分析器——java实现
2012-11-20 21:03
417 查看
编译原理语法分析的实验已经是上上周的事了,可是今天才得以更新博客,其原因必然是一直木有做完,唉想想就伤感。。我这两周的大部分青春都献给了编译原理,虽然花了好长时间,可说实话写这篇博文心里还是十分没底,因为到最后还是没有一个令自己满意的结果,特别愧疚,实验报告交的都是最基础的课件上的例子。但是已经拖了两周了,第三次实验又浩浩荡荡的袭来,唉水平有限,力不从心,老了啊。。没办法分数还得说的过去,昨天找老师检查了一下,先做到这个程度吧,后面需要学习的地方还有很多,可能能顿悟呢也说不定哦~有时间再回头修改吧~
上次实验用的是C,所以必然要通过读文件的方式来获取上次的结果,先把过程缕一下哈~
上次实验是词法分析,使用C程序将单词分开,忽略空格和注释,还有头文件(因为语法分析不想处理啦~),然后输出该单词,以及token(种别码,入口地址),当然有些没有入口地址,常量和变量的符号表也保存在文件中,并给以编号(即入口地址)。重复的常量会剔除,而重复的变量并没有(因为可能作用域不同,符号表的内容也不一定一样)。根据词法分析的结果,可以进行语法分析,即通过读入token串(相当于在词法分析识别了单词的含义),根据给定的文法进行产生式的推导。为了方便读入,我将之前的token文件只保留了种别码,并在末尾手动加上了结束符号100(种别码在词法分析的头文件里宏定义了,没有100,固选择100,之所以是数字因为是终结符,语法分析的程序通过判断是不是数字来判断是不是终结符)。我采用了自顶向下的分析方法,对文法的要求是LL1文法,要求同一非终结符的产生式的select集互不相交,这样才不会产生歧义导致不知道选择哪个产生式,其实单纯根据select集也可以分析出来,只不过通过预测分析表效率更高些,预测分析表也是通过Select集来的。所以求select集是很关键的,求出来就能分析了,有错的话可能推导不下去,错误处理待会儿再说。
求select集的过程中需要用到follow集和first集,算法如下:
有两点需要注意:
1、注意分清first(X)和first(α)的区别。X表示一个符号,而α表示一串符号,求Select集的时候用到的是求一串符号的first集,而中间会用到求一个符号的first集,两种算法分别如下:
2、如果采用递归求follow集的话可能出现死循环。先说一层的,比如A-->B A,那么求A的follow集会求左部(还是A)的follow集,这样产生死循环,在此我们可以加上判断,当二者不相等的时候在求,算法如下(但仍面临问题):
但是这只是一层,文法我们很难用肉眼看出来,如果出现求A的follow集需要求B的follow集,而求B的follow集又要求A的follow集,这样的循环恐怕递归不好判断吧。没有想到合适的方法,固在主程序中增加了follow集的集合,由于没有给终结符和非终结符编号,采用HashMap实现,注意follow集要先初始化,代码简单就不贴了,算法如下:
接下来就可以求select集了,我的程序还有一个Formula的产生式的类,数据结构如下:
保存左部,右部和select集,另外有一个格式化输出函数和判断和另一个产生式的右部是否相同(作用稍后提到)。
之前根据递归的求follow集的方法设置select集,有死循环出现,所以改用内存中保存的follow集的集合来设置,Select集的算法如下:
之后就可以根据预测分析表进行推导了,输出推导的过程,即采用的不同产生式,推导过程如下(附算法):
大体思路就是这样,运行课件上的例子倒是简单,但是运行C语言的程序就稍微困难一些了。关键是C语言的文法比较复杂,之前写好了有各种左递归的回溯(下附左递归,回溯的消除方法),直接的消除还好,关键是有许多间接的左递归和回溯不容易看出来,给人工消除造成了很大的麻烦。其实两个星期以前就开始纠结于文法不符合LL1的要求,后来一直是在处理文法的问题。人工解决不行我们还有程序嘛~所以就写了消除直接左递归和直接回溯的算法,由于有间接左递归和回溯,固消除之后将右部第一个非终结符进行替换,在进行消除,知道产生式不再变化为止,之前写的基于相同的左部在一起的算法,但是由于产生式不断变化,左部相同的不一定在一起,后来写了新的算法,还有新加的产生式不能直接在左部后面加"'"之类的符号,这样会出现歧义。比如两个相同的E'所表达的含义其实不一定是一样的,所以用了int的静态变量给新的产生式编号,由于文法确定后会扫描文法根据是否是数字判断是不是终结符,所以该新非终结符左右各加入尖括号,变成这样“<数字>”,优化后的算法如下(还是先伪代码后代码实现):
这样实现下来还可能遇到意义相同但长得不一样的非终结符,即推出的产生式都是完全一样的。这样的非终结符可以合并,以免产生式太多不好debug。所以产生式的类里有一个判断右部是否相同的方法,如下:
这样如果两个非终结符推出的产生式都完全一样,且个数相同(真子集也不能说明两个非终结符一样),就以一个为基础,将另一个的产生式删除,并且替换删掉的非终结符为基础的那个非终结符,算法如下:
这样我们新的文法就产生啦~!庆祝一下~!但是理论上是这样,事实上总是有差距的。。可能我程序有bug,还求大家帮忙看看~总之有的文法还是不能分析,或者一直死循环一直会有改变,所以我最后没办法才修改了文法,简化了许多许多许多。。多到我都不好意思贴出来了。。没有考虑运算符号的优先级,也没有考虑括号神马的。。唉。。时间就是生命啊。。不过话说回来,并不是所有文法都能转变为LL1的,这也可能是死循环的原因?额。。我不是故意推卸责任说自己算法木有问题的。。但是到底哪些能哪些不能呢?我还不太清楚额。。总之。。还是写了个算法判断是不是LL1文法,当然你开始判断一下是的话就万事大吉啦~什么都不用消除咯~算法如下:
有了上面这些做基础,就有了主要的程序来调用他们~得让他们有用武之地嘛~我写了好多测试函数,来测试文法,follow集,first集,select集,预测分析表等等,测试代码就不贴啦~主程序(main函数中)的代码如下:
最后遇到了好多问题,java也好久没用了。。(上次说C好久没有用不熟。。这次又说java。。请问您天天到底有木有编程啊!唉。。不吐槽了。。)我错了。。低级错误还是有的。。毕竟java还是我大学生涯的最低分啧啧。。还好谨记任大神语录——java都是指针好吗!其实不编理解还是不深刻啊。。那总结一下吧~
1、arraylist的赋值不能直接赋值,要clone或者构造函数中传参。虽然java都是指针,但是之前的函数跳出之后,内存会被回收。不用clone的话,即使是a = b;然后a.remove(0)之类的操作也把b给毁了。。本程序里b一般都是产生式。。亲~你把辛辛苦苦优化为LL1的产生式都毁了还编个毛毛呢~
2、类似这样的产生式E2->400T E2 会死循环,需要先判断,避免都是求E2的follow集而重复,如果递归时仍然是求E2的follow集则无需再求。这个其实只是判断了一层,归根到底应该还是不能用递归的,上文有说哦~看我这篇是上下文有关文法O(∩_∩)O哈哈~~开个玩笑。。
3、集合循环使用并给其他赋值,每次使用前要清空!因为每次都是调用add函数,不然后面的集合将包含之前的集合。
4、之前对求first集理解不深刻!再求select集的时候,注意first是对一串求,不是对一个非终结符求。而之前写的first算法是对某一个非终结符求first集,这个上面也说啦~!
5、终结符可能连续出栈,比如连着两个都是输入缓冲和栈顶匹配。不能用if判断,需要用While判断,并且需要用peek函数来判断。之前设置了变量保存栈顶和输入缓冲的队列头,但运行中不断变化,直接peek调用更加可靠。
6、考虑间接左递归和间接回溯!这是文法中最不容易看出来的地方,也是最最最最纠结的地方。。最终用程序实现了消除左递归。
7、程序修改后注意调用顺序,比如编程实现左递归之后,主程序还是先调用setSelect了,则不是对消除左递归之后的文法设置Select集,故分析出错。还有终结符,非终结符的集合填写也要在文法确定之后哦~改动太多也要注意函数的调用顺序呀~
8、左递归右部是空,不能当做β,要去掉。因为程序中用“@”表示空,也是字符串,判断时终结符是数字,而空面临当做非终结符处理的可能,需额外考虑。
9、对集合遍历时迭代器要归位啊,否则只能对第一次的遍历。
10、正则表达式没有检测-1,结束符也要是正数,设为100(种别码中没有,规定即可)附上正则表达式判断数字的函数吧~
11、对于结束符号的理解没有到位。刚开始写文法按照报告的要求分开写的,这样非常清晰。先不说左递归和回溯问题,关于结束符号理解为表达式的是“;”,函数的是“}”这样,所以程序只能分析单独的一类文法。而最终分析的程序又好多语句构成,需要把上面单独的文法合为整体,所以对token串的最后增加自己设定的结束符号(当做终结符,本程序中设为100)。而语句的右部需要增加“;”,其他表达式,函数调用等各种语句后面就不需要“;”了。
12、StringTokenizer为“->”时读取出错,读不到非终结符最右端的“>”,改为“#”即可。
13、开始符号也可能出现死循环,理由同问题2.
14、文法中可以直接考虑优先级的,虽然我最终还是木有考虑。。比如课件中的例子:
15、如果全是左递归,要添加E->E'的式子,否则出现永远不会在左部出现的非终结符,那推到那里就悲剧了哦~之前算法中没有考虑到。
16、正则表达式没有检测-1,结束符刚开始设为-1了额。。
最后梳理一下整个流程,然后输出课件上的例子的结果,不然结果太多了不容易看,尤其是种别码我自己规定的,数字意义不够明确额。。
主图:
流程图:
运行结果:
附上我写的C语言的文法,实在是简单的可以额。。。
下面是主程序处理之后的文法:
上次实验用的是C,所以必然要通过读文件的方式来获取上次的结果,先把过程缕一下哈~
上次实验是词法分析,使用C程序将单词分开,忽略空格和注释,还有头文件(因为语法分析不想处理啦~),然后输出该单词,以及token(种别码,入口地址),当然有些没有入口地址,常量和变量的符号表也保存在文件中,并给以编号(即入口地址)。重复的常量会剔除,而重复的变量并没有(因为可能作用域不同,符号表的内容也不一定一样)。根据词法分析的结果,可以进行语法分析,即通过读入token串(相当于在词法分析识别了单词的含义),根据给定的文法进行产生式的推导。为了方便读入,我将之前的token文件只保留了种别码,并在末尾手动加上了结束符号100(种别码在词法分析的头文件里宏定义了,没有100,固选择100,之所以是数字因为是终结符,语法分析的程序通过判断是不是数字来判断是不是终结符)。我采用了自顶向下的分析方法,对文法的要求是LL1文法,要求同一非终结符的产生式的select集互不相交,这样才不会产生歧义导致不知道选择哪个产生式,其实单纯根据select集也可以分析出来,只不过通过预测分析表效率更高些,预测分析表也是通过Select集来的。所以求select集是很关键的,求出来就能分析了,有错的话可能推导不下去,错误处理待会儿再说。
求select集的过程中需要用到follow集和first集,算法如下:
有两点需要注意:
1、注意分清first(X)和first(α)的区别。X表示一个符号,而α表示一串符号,求Select集的时候用到的是求一串符号的first集,而中间会用到求一个符号的first集,两种算法分别如下:
/** * 求一个符号的First集 * @param X * @param formulas * @return */ public static HashSet<Integer> getFirst(String X, ArrayList<Formula> formulas) { // System.out.println("getFirst:"+X); HashSet<Integer> first = new HashSet<Integer>(); if(isNumeric(X))//如果是终结符 { first.add(Integer.parseInt(X)); return first; } String tmp; for(Formula f: formulas) { int i=0; if(X.equals(f.getLeft())) { if(i==f.getRight().size()) { return first; } tmp = f.getRight().get(i); if(isNumeric(tmp)) { first.add(Integer.parseInt(tmp)); //不能return } else if(!tmp.equals("@")) { while(i<f.getRight().size()) { if(!f.getRight().get(i).equals(X))//避免死循环 { first.addAll(getFirst(f.getRight().get(i), formulas));//递归调用getFirst } if(!canNull(f.getRight().get(i++), formulas)) { break; } } } } } return first; } /** * 求一串的first集 * @param right * @param formulas * @return */ public static HashSet<Integer> getFirsts(ArrayList<String> right, ArrayList<Formula> formulas) { HashSet<Integer> first = new HashSet<Integer>(); first.addAll(getFirst(right.get(0), formulas)); int k=0; while(canNull(right.get(k), formulas) && k<right.size()-1) { first.addAll(getFirst(right.get(k+1), formulas)); k++; } return first; }
2、如果采用递归求follow集的话可能出现死循环。先说一层的,比如A-->B A,那么求A的follow集会求左部(还是A)的follow集,这样产生死循环,在此我们可以加上判断,当二者不相等的时候在求,算法如下(但仍面临问题):
/** * 获得Follow集 使用了递归 * @param X * @param formulas * @return */ public static HashSet<Integer> getFollow(String X, ArrayList<Formula> formulas) { // System.out.println("getFollow:"+X); HashSet<Integer> follow = new HashSet<Integer>(); ArrayList<String> right = null; if(X.equals(LL1.start))//开始符号也可能死循环 { follow.add(Integer.parseInt(LL1.end)); } for(Formula f: formulas) { right = f.getRight(); for(int i=0; i<right.size(); i++) { if(X.equals(right.get(i))) { //后面没有符号串 if(i==right.size()-1) { if(!f.getLeft().equals(X)) { follow.addAll(getFollow(f.getLeft(), formulas)); return follow;//右部后面没有符号,直接return } } else { if(isNumeric(right.get(i+1))) { follow.add(Integer.parseInt(right.get(i+1))); continue; } follow.addAll(getFirst(right.get(i+1), formulas)); int j; // 判断后面的符号串是否都能推出空 for(j=i+1; j<right.size() && canNull(right.get(j), formulas); j++); if(j == right.size()) { follow.addAll(getFollow(f.getLeft(), formulas)); } } } } } return follow; }
但是这只是一层,文法我们很难用肉眼看出来,如果出现求A的follow集需要求B的follow集,而求B的follow集又要求A的follow集,这样的循环恐怕递归不好判断吧。没有想到合适的方法,固在主程序中增加了follow集的集合,由于没有给终结符和非终结符编号,采用HashMap实现,注意follow集要先初始化,代码简单就不贴了,算法如下:
/** * 非递归求follow集 */ static void setFollows() { boolean changes; boolean flag; int sizeBefore; int sizeAfter; ArrayList<String> beta; HashSet<Integer> tmp = new HashSet<Integer>(); do { changes = false; for(Formula f: formulas) { beta = (ArrayList<String>) f.getRight().clone();//beta不能修改原来的right for(String right: f.getRight()) { beta.remove(0); if(nonTerminal.contains(right)) { // System.out.println("right+"+right); if(follows.get(right).isEmpty()) { sizeBefore = 0; } else { sizeBefore = follows.get(right).size(); } if(beta.size()!=0) { tmp = Util.getFirsts(beta, formulas); // flag = tmp.remove("@"); int x; for(x=0; x<beta.size() && Util.canNull(beta.get(x), formulas); x++); if(x==beta.size()) { flag = true; } else { flag = false; } follows.get(right).addAll(tmp); } else { flag = true; } if(flag && !right.equals(f.getLeft())) { follows.get(right).addAll(follows.get(f.getLeft())); } sizeAfter = follows.get(right).size(); if(sizeBefore!=sizeAfter) { changes = true; } } } } }while(changes); }
接下来就可以求select集了,我的程序还有一个Formula的产生式的类,数据结构如下:
String left; ArrayList<String> right = new ArrayList<String>(); HashSet<Integer> select = new HashSet<Integer>();
保存左部,右部和select集,另外有一个格式化输出函数和判断和另一个产生式的右部是否相同(作用稍后提到)。
之前根据递归的求follow集的方法设置select集,有死循环出现,所以改用内存中保存的follow集的集合来设置,Select集的算法如下:
/**根据保存的follow集来设置select的值 * */ static void setSelectFromFollow() { HashSet<Integer> select = new HashSet<Integer>(); int i; for(Formula f: formulas) { if(f.getRight().get(0).equals("@")) { select.addAll(follows.get(f.getLeft())); } else { select.addAll(Util.getFirsts(f.getRight(), formulas)); for(i=0; i<f.getRight().size() && Util.canNull(f.getRight().get(i), formulas); i++); if(i==f.getRight().size()) { select.addAll(follows.get(f.getLeft())); } } f.setSelect((HashSet<Integer>) select.clone()); select.clear(); } }然后将select集填入预测分析表,空的部分采用一些启发式规则选择填写error或者同步记号,本程序将follow集作为同步记号,置为-2,error空项置为-1,有产生式的表中内容为产生式的编号。算法如下:
/** * 建立预测分析表,不能基于左部都在一起,因为文法变换后顺序不定 */ static void fillPredictMap() { Iterator<Integer> terIter = terminal.iterator(); HashMap<Integer, Integer> map = new HashMap<Integer, Integer>(); Formula f; Integer terInt; for(int i=0; i<formulas.size(); i++) { f = formulas.get(i); while(terIter.hasNext()) { terInt = terIter.next(); if(f.getSelect().contains(terInt)) { map.put(terInt, i); if(predictMap.get(f.getLeft())==null)//没有该终非结符的表,建立 { predictMap.put(f.getLeft(), (HashMap<Integer, Integer>) map.clone()); } else//有则直接get再添加map { predictMap.get(f.getLeft()).put(terInt, i); } map.clear(); } } terIter = terminal.iterator();//迭代器复位 } //填synch和error项 Iterator<String> nonIter = nonTerminal.iterator(); String nonStr; while(nonIter.hasNext()) { terIter = terminal.iterator(); nonStr = nonIter.next(); while(terIter.hasNext()) { terInt = terIter.next(); if(predictMap.get(nonStr)==null) { System.out.println("nonStr="+nonStr); } if(predictMap.get(nonStr).get(terInt)==null) { if(follows.get(nonStr).contains(terInt)) { predictMap.get(nonStr).put(terInt, -2); } else { predictMap.get(nonStr).put(terInt, -1); } } } } }
之后就可以根据预测分析表进行推导了,输出推导的过程,即采用的不同产生式,推导过程如下(附算法):
/** * 根据预测分析表推导,输出栈里的内容 */ static void deriveFromTable() { int result; Formula f; while(!stack.isEmpty()) { // System.out.println("stack peek: "+stack.peek()+"\t"+"input peek:"+input.peek()); if(predictMap==null) { System.out.println("预测分析表为空"); } else if(predictMap.get(stack.peek())==null) { System.out.println("表中"+stack.peek()+"为空"); } else if(predictMap.get(stack.peek()).get(input.peek())==null) { System.out.println("表中表"+stack.peek()+"\t"+input.peek()+"为空"); } result = predictMap.get(stack.peek()).get(input.peek()); switch(result) { case -1: System.out.println(stack.peek()+" "+input.peek()+" error项,弹出输入符号:"+input.poll()); break; case -2: System.out.println(stack.peek()+" "+input.peek()+" synch项,弹出栈顶元素:"+stack.pop()); break; default: f = formulas.get(result); System.out.println(f.output()); //将产生式左部出队列 stack.pop(); if(!f.getRight().get(0).equals("@")) { //将产生式右部入队列 for(int i=(f.getRight().size()-1); i>=0; i--) { stack.push(f.getRight().get(i)); } } while(!stack.isEmpty() && Util.isNumeric(stack.peek()))//不是if是while,加上非空判断 { if( input.peek()==Integer.parseInt(stack.peek()))//不能是t,因为出栈后可能变化,是input.peek() { input.poll(); stack.pop(); } } } } }
大体思路就是这样,运行课件上的例子倒是简单,但是运行C语言的程序就稍微困难一些了。关键是C语言的文法比较复杂,之前写好了有各种左递归的回溯(下附左递归,回溯的消除方法),直接的消除还好,关键是有许多间接的左递归和回溯不容易看出来,给人工消除造成了很大的麻烦。其实两个星期以前就开始纠结于文法不符合LL1的要求,后来一直是在处理文法的问题。人工解决不行我们还有程序嘛~所以就写了消除直接左递归和直接回溯的算法,由于有间接左递归和回溯,固消除之后将右部第一个非终结符进行替换,在进行消除,知道产生式不再变化为止,之前写的基于相同的左部在一起的算法,但是由于产生式不断变化,左部相同的不一定在一起,后来写了新的算法,还有新加的产生式不能直接在左部后面加"'"之类的符号,这样会出现歧义。比如两个相同的E'所表达的含义其实不一定是一样的,所以用了int的静态变量给新的产生式编号,由于文法确定后会扫描文法根据是否是数字判断是不是终结符,所以该新非终结符左右各加入尖括号,变成这样“<数字>”,优化后的算法如下(还是先伪代码后代码实现):
* 消除直接左递归,左部相同的不一定在一起 */ static void removeLeftRecursion() { // System.out.println("removeLeft called..."); Formula fi; Formula fj; String left; boolean flag; ArrayList<String> forNull = new ArrayList<String>(); ArrayList<String> tmp = new ArrayList<String>(); forNull.add("@"); for(int i=formulas.size()-1; i>=0; i--) { fi = formulas.get(i); if(fi.getLeft().equals(fi.getRight().get(0))) { flag = true; // System.out.println(f.output()); formulaChange = true; System.out.println("removeLeftRecursion true"); left = fi.getLeft(); fi.getRight().remove(0); fi.getRight().add("<"+number+">"); fi.setLeft("<"+number+">"); for(int j=formulas.size()-1; j>=0; j--) { fj = formulas.get(j); if(left.equals(fj.getLeft())) { if(left.equals(fj.getRight().get(0)))//左递归 { fj.setLeft("<"+number+">");//加尖括号避免看成终结符 fj.getRight().remove(0); fj.getRight().add("<"+number+">"); } else { flag = false;//看是否都是左递归,进入此处表示有不是左递归的产生式 if(fj.getRight().get(0).equals("@")) { fj.getRight().remove(0); } fj.getRight().add("<"+number+">"); } } } formulas.add(new Formula("<"+number+">", (ArrayList<String>) forNull.clone())); if(flag) { tmp.add("<"+number+">"); formulas.add(new Formula(left, (ArrayList<String>) tmp.clone())); tmp.clear(); } number++; } } }
/** * 消除直接回溯,提取左因子 */ static void removeBack() { // System.out.println("removeBack called..."); int k; boolean flag = false; ArrayList<String> right = new ArrayList<String>(); do { flag = false; for(int i=formulas.size()-1; i>=0; i--) { Formula fi = formulas.get(i); for(int j=i-1; j>=0; j--) { Formula fj = formulas.get(j); if(fj.getRight().get(0).equals("@"))//空产生式略过 { continue; } if(fj.getLeft().equals(fi.getLeft()) && Util.isNumeric(fi.getRight().get(0))) { for(k=0; k<fi.getRight().size()&&k<fj.getRight().size()&& fi.getRight().get(k).equals(fj.getRight().get(k)); k++); if(k>0) { // System.out.println("i="+i+":"+fi.output()+" ||||| "+"j="+j+":"+fj.output()); formulaChange = true; // System.out.println("removeBack true"); for(int a=0; a<k; a++) { right.add(fi.getRight().get(0)); fi.getRight().remove(0); fj.getRight().remove(0); } right.add("<"+number+">"); formulas.add(new Formula(fi.getLeft(), (ArrayList<String>) right.clone())); right.clear(); if(fi.getRight().isEmpty()) { fi.getRight().add("@"); } if(fj.getRight().isEmpty()) { fj.getRight().add("@"); } fi.setLeft("<"+number+">"); fj.setLeft("<"+number+">"); number++; flag = true; break; } } } } }while(flag); }
/** * 将非终结符换位终结符代入,注意考虑空,此时不能有左递归,否则死循环, * 新的产生式加在后面,逆向遍历 */ static void substituteFormula() { Formula fi; Formula fj; ArrayList<String> right = new ArrayList<String>(); ArrayList<String> beta = new ArrayList<String>(); for(int i=formulas.size()-1; i>=0; i--) { fi = formulas.get(i); if(!Util.isNumeric(fi.getRight().get(0))&& !fi.getRight().get(0).equals("@") && Util.isBeginWithTerminal(fi.getRight().get(0), formulas)) { // System.out.println("substitute true.."); // System.out.println(formulas.get(i).output()); formulaChange = true; for(int j=formulas.size()-1; j>=0; j--) { fj = formulas.get(j); if(fj.getLeft().equals(fi.getRight().get(0))) { beta = (ArrayList<String>) fi.getRight().clone(); beta.remove(0); right = (ArrayList<String>) fj.getRight().clone(); right.addAll(beta); formulas.add(new Formula(fi.getLeft(), (ArrayList<String>) right.clone())); } } formulas.remove(i); } } }
这样实现下来还可能遇到意义相同但长得不一样的非终结符,即推出的产生式都是完全一样的。这样的非终结符可以合并,以免产生式太多不好debug。所以产生式的类里有一个判断右部是否相同的方法,如下:
public boolean isRightSame(Formula f) { int k; if(f.getRight().size()==right.size()) { for(k=0; k<right.size() && right.get(k).equals(f.getRight().get(k)); k++); if(k==right.size()) { return true; } } return false; }
这样如果两个非终结符推出的产生式都完全一样,且个数相同(真子集也不能说明两个非终结符一样),就以一个为基础,将另一个的产生式删除,并且替换删掉的非终结符为基础的那个非终结符,算法如下:
/** * 合并相同的产生式,即合并实质一样的非终结符 */ static void mergeFormulas() { Formula f1; Formula f2; String f2Left; ArrayList<String> right; for(int i=0; i<formulas.size(); i++) { f1 = formulas.get(i); for(int j=i+1; j<formulas.size(); j++) { if(!f1.getLeft().equals(formulas.get(j).getLeft())) { f2 = formulas.get(j); if(Util.isNonTerSame(f1.getLeft(), f2.getLeft(),formulas)) { f2Left = f2.getLeft(); //遍历一遍删掉该非终结符推出的产生式,并将右部有该非终结符的替换为i的 for(int k=0; k<formulas.size(); k++) { if(formulas.get(k).getLeft().equals(f2Left)) { formulas.remove(k); } else { right = formulas.get(k).getRight(); for(int m=0; m<right.size(); m++) { if(right.get(m).equals(f2Left)) { right.set(m, f1.getLeft()); } } } } } } } } }
这样我们新的文法就产生啦~!庆祝一下~!但是理论上是这样,事实上总是有差距的。。可能我程序有bug,还求大家帮忙看看~总之有的文法还是不能分析,或者一直死循环一直会有改变,所以我最后没办法才修改了文法,简化了许多许多许多。。多到我都不好意思贴出来了。。没有考虑运算符号的优先级,也没有考虑括号神马的。。唉。。时间就是生命啊。。不过话说回来,并不是所有文法都能转变为LL1的,这也可能是死循环的原因?额。。我不是故意推卸责任说自己算法木有问题的。。但是到底哪些能哪些不能呢?我还不太清楚额。。总之。。还是写了个算法判断是不是LL1文法,当然你开始判断一下是的话就万事大吉啦~什么都不用消除咯~算法如下:
/** * 判断是否是LL1文法 * @return */ static boolean isLL1() { HashSet<Integer> tmp = new HashSet<Integer>(); for(int i=0; i<formulas.size(); i++) { for(int j=i+1; j<formulas.size(); j++) { tmp = (HashSet<Integer>) formulas.get(i).getSelect().clone(); tmp.retainAll(formulas.get(j).getSelect()); if(formulas.get(i).getLeft().equals(formulas.get(j).getLeft()) && tmp.size()!=0 && !(formulas.get(i).getRight().get(0).equals("@") &&formulas.get(j).getRight().get(0).equals("@")))//可能产生相同的空产生式 { System.out.println("not LL1:"+i+"\t"+j); return false; } } } return true; }
有了上面这些做基础,就有了主要的程序来调用他们~得让他们有用武之地嘛~我写了好多测试函数,来测试文法,follow集,first集,select集,预测分析表等等,测试代码就不贴啦~主程序(main函数中)的代码如下:
public static void main(String[] args) { // TODO Auto-generated method stub initFormulas(formulas); // System.out.println("从文件读入的初始文法如下:"); // System.out.println("\n-------------------------------------------------------------------------------"); // System.out.println("消除左递归的文法如下:"); // System.out.println("\n-------------------------------------------------------------------------------"); // System.out.println("右部无非终结符的文法如下:"); // System.out.println("\n-------------------------------------------------------------------------------"); // System.out.println("消除回溯的文法如下:"); // removeLeftRecursion(); // testFormulas(); while(true) { formulaChange = false; removeBack(); removeLeftRecursion(); substituteFormula(); mergeFormulas(); if(!formulaChange) { break; } //不能在这判断是不是LL1文法,因为select集还没有确定 } System.out.println("处理后的文法如下:"); testFormulas(); System.out.println("\n-------------------------------------------------------------------------------"); fillSymbols();//要在文法确定之后再添加 initFollows(); setFollows(); setSelectFromFollow(); testSelect(); System.out.println("预测分析表如下:"); fillPredictMap(); testPredictMap(); //结束符号和开始符号入栈 stack.push(end); stack.push(start); try { BufferedReader in = new BufferedReader(new FileReader(tokenName)); String line; int i=0; while((line=in.readLine())!=null ) { //输入进队列 input.offer(Integer.parseInt(line)); } } catch (Exception e) { // TODO Auto-generated catch block e.printStackTrace(); } System.out.println("推导过程如下:"); deriveFromTable(); }
最后遇到了好多问题,java也好久没用了。。(上次说C好久没有用不熟。。这次又说java。。请问您天天到底有木有编程啊!唉。。不吐槽了。。)我错了。。低级错误还是有的。。毕竟java还是我大学生涯的最低分啧啧。。还好谨记任大神语录——java都是指针好吗!其实不编理解还是不深刻啊。。那总结一下吧~
1、arraylist的赋值不能直接赋值,要clone或者构造函数中传参。虽然java都是指针,但是之前的函数跳出之后,内存会被回收。不用clone的话,即使是a = b;然后a.remove(0)之类的操作也把b给毁了。。本程序里b一般都是产生式。。亲~你把辛辛苦苦优化为LL1的产生式都毁了还编个毛毛呢~
2、类似这样的产生式E2->400T E2 会死循环,需要先判断,避免都是求E2的follow集而重复,如果递归时仍然是求E2的follow集则无需再求。这个其实只是判断了一层,归根到底应该还是不能用递归的,上文有说哦~看我这篇是上下文有关文法O(∩_∩)O哈哈~~开个玩笑。。
3、集合循环使用并给其他赋值,每次使用前要清空!因为每次都是调用add函数,不然后面的集合将包含之前的集合。
4、之前对求first集理解不深刻!再求select集的时候,注意first是对一串求,不是对一个非终结符求。而之前写的first算法是对某一个非终结符求first集,这个上面也说啦~!
5、终结符可能连续出栈,比如连着两个都是输入缓冲和栈顶匹配。不能用if判断,需要用While判断,并且需要用peek函数来判断。之前设置了变量保存栈顶和输入缓冲的队列头,但运行中不断变化,直接peek调用更加可靠。
6、考虑间接左递归和间接回溯!这是文法中最不容易看出来的地方,也是最最最最纠结的地方。。最终用程序实现了消除左递归。
7、程序修改后注意调用顺序,比如编程实现左递归之后,主程序还是先调用setSelect了,则不是对消除左递归之后的文法设置Select集,故分析出错。还有终结符,非终结符的集合填写也要在文法确定之后哦~改动太多也要注意函数的调用顺序呀~
8、左递归右部是空,不能当做β,要去掉。因为程序中用“@”表示空,也是字符串,判断时终结符是数字,而空面临当做非终结符处理的可能,需额外考虑。
9、对集合遍历时迭代器要归位啊,否则只能对第一次的遍历。
10、正则表达式没有检测-1,结束符也要是正数,设为100(种别码中没有,规定即可)附上正则表达式判断数字的函数吧~
/** * 判断字符串是否是数字 * @param str * @return */ public static boolean isNumeric(String str){ Pattern pattern = Pattern.compile("[0-9]*"); Matcher isNum = pattern.matcher(str); if( !isNum.matches() ){ return false; } return true; }
11、对于结束符号的理解没有到位。刚开始写文法按照报告的要求分开写的,这样非常清晰。先不说左递归和回溯问题,关于结束符号理解为表达式的是“;”,函数的是“}”这样,所以程序只能分析单独的一类文法。而最终分析的程序又好多语句构成,需要把上面单独的文法合为整体,所以对token串的最后增加自己设定的结束符号(当做终结符,本程序中设为100)。而语句的右部需要增加“;”,其他表达式,函数调用等各种语句后面就不需要“;”了。
12、StringTokenizer为“->”时读取出错,读不到非终结符最右端的“>”,改为“#”即可。
13、开始符号也可能出现死循环,理由同问题2.
14、文法中可以直接考虑优先级的,虽然我最终还是木有考虑。。比如课件中的例子:
15、如果全是左递归,要添加E->E'的式子,否则出现永远不会在左部出现的非终结符,那推到那里就悲剧了哦~之前算法中没有考虑到。
16、正则表达式没有检测-1,结束符刚开始设为-1了额。。
最后梳理一下整个流程,然后输出课件上的例子的结果,不然结果太多了不容易看,尤其是种别码我自己规定的,数字意义不够明确额。。
主图:
流程图:
运行结果:
附上我写的C语言的文法,实在是简单的可以额。。。
PROGRAM#STATS STATS#@ STATS#STAT STATS STAT#ARITHME 405 STAT#FUNC_CALL STAT#FUNC_DEFINE STAT#LOOP STAT#IF_ELSE STAT#RETURN STAT#ASSIGN STAT#DECLARES STAT#SELF_CALC 405 STAT#405 SELF_CALC#300 421 SELF_CALC#300 422 SELF_CALC#421 300 SELF_CALC#422 300 FUNC_DEFINE#TYPE P 300 410 PARAMS_DEFINE 409 407 STATS 408 TYPE#288 TYPE#273 TYPE#260 TYPE#269 TYPE#265 TYPE#277 TYPE#274 P#402 P#@ PARAMS_DEFINE#TYPE P 300 ARRAY PARAMS_DEFINE_TAIL PARAMS_DEFINE#@ PARAMS_DEFINE_TAIL#406 TYPE P 300 ARRAY PARAMS_DEFINE_TAIL PARAMS_DEFINE_TAIL#@ RETURN#276 VALUE 405 RETURN#276 ARITHME 405 FUNC_CALL#300 410 PARAMS 409 405 PARAMS#VALUE PARAMS_TAIL PARAMS#@ PARAMS_TAIL#406 VALUE PARAMS_TAIL PARAMS_TAIL#@ DECLARES#TYPE P 300 ARRAY DECLARE_ASSIGN DECLARE_TAIL 405 DECLARE_ASSIGN#413 VALUE DECLARE_ASSIGN#@ ARRAY#414 301 415 ARRAY ARRAY#@ DECLARE_TAIL#406 P 300 ARRAY DECLARE_ASSIGN DECLARE_TAIL DECLARE_TAIL#@ ASSIGN#300 ARRAY 413 VALUE 405 ASSIGN#300 413 ARITHME 405 ASSIGN#300 413 FUNC_CALL VALUE#CONST VALUE#300 CONST#301 CONST#302 CONST#303 CONST#304 LOGIC#VALUE EQU VALUE LOGIC_TAIL LOGIC_TAIL#@ LOGIC_TAIL#431 LOGIC LOGIC_TAIL#430 LOGIC EQU#423 EQU#429 EQU#411 EQU#412 EQU#427 EQU#428 ARITHME#VALUE OP VALUE ARITHME_TAIL ARITHME_TAIL#@ ARITHME_TAIL#OP VALUE ARITHME_TAIL OP#400 OP#401 OP#402 OP#403 OP#404 FOR_ASSIGN#405 FOR_ASSIGN#300 413 VALUE 405 LOOP#270 410 FOR_ASSIGN LOGIC 405 SELF_CALC 409 407 STATS 408 LOOP#287 410 LOGIC 409 407 STATS 408 LOOP#264 407 STATS 408 287 410 LOGIC 409 405 IF_ELSE#272 410 LOGIC 409 407 STATS 408 ELSE_IF ELSE ELSE_IF#266 272 410 LOGIC 409 407 STATS 408 ELSE#266 407 STATS 408 ELSE#@
下面是主程序处理之后的文法:
0 PROGRAM-->STATS 1 STATS-->@ 2 STAT-->405 3 <2>-->421 4 <2>-->422 5 SELF_CALC-->421 300 6 SELF_CALC-->422 300 7 TYPE-->288 8 TYPE-->273 9 TYPE-->260 10 TYPE-->269 11 TYPE-->265 12 TYPE-->277 13 TYPE-->274 14 P-->402 15 P-->@ 16 PARAMS_DEFINE-->@ 17 PARAMS_DEFINE_TAIL-->406 TYPE P 300 ARRAY PARAMS_DEFINE_TAIL 18 PARAMS_DEFINE_TAIL-->@ 19 FUNC_CALL-->300 410 PARAMS 409 405 20 PARAMS-->@ 21 PARAMS_TAIL-->406 VALUE PARAMS_TAIL 22 PARAMS_TAIL-->@ 23 DECLARE_ASSIGN-->413 VALUE 24 DECLARE_ASSIGN-->@ 25 ARRAY-->414 301 415 ARRAY 26 ARRAY-->@ 27 DECLARE_TAIL-->406 P 300 ARRAY DECLARE_ASSIGN DECLARE_TAIL 28 DECLARE_TAIL-->@ 29 <3>-->ARRAY 413 VALUE 405 30 VALUE-->300 31 CONST-->301 32 CONST-->302 33 CONST-->303 34 CONST-->304 35 LOGIC_TAIL-->@ 36 LOGIC_TAIL-->431 LOGIC 37 LOGIC_TAIL-->430 LOGIC 38 EQU-->423 39 EQU-->429 40 EQU-->411 41 EQU-->412 42 EQU-->427 43 EQU-->428 44 ARITHME_TAIL-->@ 45 OP-->400 46 OP-->401 47 OP-->402 48 OP-->403 49 OP-->404 50 FOR_ASSIGN-->405 51 FOR_ASSIGN-->300 413 VALUE 405 52 LOOP-->270 410 FOR_ASSIGN LOGIC 405 SELF_CALC 409 407 STATS 408 53 LOOP-->287 410 LOGIC 409 407 STATS 408 54 LOOP-->264 407 STATS 408 287 410 LOGIC 409 405 55 IF_ELSE-->272 410 LOGIC 409 407 STATS 408 ELSE_IF ELSE 56 ELSE_IF-->266 272 410 LOGIC 409 407 STATS 408 57 ELSE-->266 407 STATS 408 58 ELSE-->@ 59 <3>-->413 <0> 60 RETURN-->276 <1> 61 SELF_CALC-->300 <2> 62 ASSIGN-->300 <3> 63 ARITHME_TAIL-->404 VALUE ARITHME_TAIL 64 ARITHME_TAIL-->403 VALUE ARITHME_TAIL 65 ARITHME_TAIL-->402 VALUE ARITHME_TAIL 66 ARITHME_TAIL-->401 VALUE ARITHME_TAIL 67 ARITHME_TAIL-->400 VALUE ARITHME_TAIL 68 VALUE-->304 69 VALUE-->303 70 VALUE-->302 71 VALUE-->301 72 <20>-->410 PARAMS 409 405 73 DECLARES-->274 P 300 ARRAY DECLARE_ASSIGN DECLARE_TAIL 405 74 DECLARES-->277 P 300 ARRAY DECLARE_ASSIGN DECLARE_TAIL 405 75 DECLARES-->265 P 300 ARRAY DECLARE_ASSIGN DECLARE_TAIL 405 76 DECLARES-->269 P 300 ARRAY DECLARE_ASSIGN DECLARE_TAIL 405 77 DECLARES-->260 P 300 ARRAY DECLARE_ASSIGN DECLARE_TAIL 405 78 DECLARES-->273 P 300 ARRAY DECLARE_ASSIGN DECLARE_TAIL 405 79 DECLARES-->288 P 300 ARRAY DECLARE_ASSIGN DECLARE_TAIL 405 80 PARAMS-->301 PARAMS_TAIL 81 PARAMS-->302 PARAMS_TAIL 82 PARAMS-->303 PARAMS_TAIL 83 PARAMS-->304 PARAMS_TAIL 84 PARAMS-->300 PARAMS_TAIL 85 <15>-->405 86 PARAMS_DEFINE-->274 P 300 ARRAY PARAMS_DEFINE_TAIL 87 PARAMS_DEFINE-->277 P 300 ARRAY PARAMS_DEFINE_TAIL 88 PARAMS_DEFINE-->265 P 300 ARRAY PARAMS_DEFINE_TAIL 89 PARAMS_DEFINE-->269 P 300 ARRAY PARAMS_DEFINE_TAIL 90 PARAMS_DEFINE-->260 P 300 ARRAY PARAMS_DEFINE_TAIL 91 PARAMS_DEFINE-->273 P 300 ARRAY PARAMS_DEFINE_TAIL 92 PARAMS_DEFINE-->288 P 300 ARRAY PARAMS_DEFINE_TAIL 93 FUNC_DEFINE-->274 P 300 410 PARAMS_DEFINE 409 407 STATS 408 94 FUNC_DEFINE-->277 P 300 410 PARAMS_DEFINE 409 407 STATS 408 95 FUNC_DEFINE-->265 P 300 410 PARAMS_DEFINE 409 407 STATS 408 96 FUNC_DEFINE-->269 P 300 410 PARAMS_DEFINE 409 407 STATS 408 97 FUNC_DEFINE-->260 P 300 410 PARAMS_DEFINE 409 407 STATS 408 98 FUNC_DEFINE-->273 P 300 410 PARAMS_DEFINE 409 407 STATS 408 99 FUNC_DEFINE-->288 P 300 410 PARAMS_DEFINE 409 407 STATS 408 100 STAT-->422 300 405 101 STAT-->421 300 405 102 <7>-->ARRAY DECLARE_ASSIGN DECLARE_TAIL 405 103 <4>--><3> 104 STAT-->276 <1> 105 STAT-->272 410 LOGIC 409 407 STATS 408 ELSE_IF ELSE 106 STAT-->264 407 STATS 408 287 410 LOGIC 409 405 107 STAT-->287 410 LOGIC 409 407 STATS 408 108 STAT-->270 410 FOR_ASSIGN LOGIC 405 SELF_CALC 409 407 STATS 408 109 <7>-->410 PARAMS_DEFINE 409 407 STATS 408 110 <4>-->410 PARAMS 409 405 111 <12>--><4> 112 STAT-->274 P 300 <7> 113 STAT-->277 P 300 <7> 114 STAT-->265 P 300 <7> 115 STAT-->269 P 300 <7> 116 STAT-->260 P 300 <7> 117 STAT-->273 P 300 <7> 118 STAT-->288 P 300 <7> 119 <14>--><12> 120 <12>-->422 405 121 <12>-->421 405 122 ARITHME-->301 OP VALUE ARITHME_TAIL 123 ARITHME-->302 OP VALUE ARITHME_TAIL 124 ARITHME-->303 OP VALUE ARITHME_TAIL 125 ARITHME-->304 OP VALUE ARITHME_TAIL 126 ARITHME-->300 OP VALUE ARITHME_TAIL 127 LOGIC-->301 EQU VALUE LOGIC_TAIL 128 LOGIC-->302 EQU VALUE LOGIC_TAIL 129 LOGIC-->303 EQU VALUE LOGIC_TAIL 130 LOGIC-->304 EQU VALUE LOGIC_TAIL 131 LOGIC-->300 EQU VALUE LOGIC_TAIL 132 <0>-->304 OP VALUE ARITHME_TAIL 405 133 <0>-->303 OP VALUE ARITHME_TAIL 405 134 <0>-->302 OP VALUE ARITHME_TAIL 405 135 <0>-->301 OP VALUE ARITHME_TAIL 405 136 STAT-->304 OP VALUE ARITHME_TAIL 405 137 STAT-->303 OP VALUE ARITHME_TAIL 405 138 STAT-->302 OP VALUE ARITHME_TAIL 405 139 STAT-->301 OP VALUE ARITHME_TAIL 405 140 STATS-->301 OP VALUE ARITHME_TAIL 405 STATS 141 STATS-->302 OP VALUE ARITHME_TAIL 405 STATS 142 STATS-->303 OP VALUE ARITHME_TAIL 405 STATS 143 STATS-->304 OP VALUE ARITHME_TAIL 405 STATS 144 <13>--><12> STATS 145 STATS-->288 P 300 <7> STATS 146 STATS-->273 P 300 <7> STATS 147 STATS-->260 P 300 <7> STATS 148 STATS-->269 P 300 <7> STATS 149 STATS-->265 P 300 <7> STATS 150 STATS-->277 P 300 <7> STATS 151 STATS-->274 P 300 <7> STATS 152 STATS-->270 410 FOR_ASSIGN LOGIC 405 SELF_CALC 409 407 STATS 408 STATS 153 STATS-->287 410 LOGIC 409 407 STATS 408 STATS 154 STATS-->264 407 STATS 408 287 410 LOGIC 409 405 STATS 155 STATS-->272 410 LOGIC 409 407 STATS 408 ELSE_IF ELSE STATS 156 STATS-->276 <1> STATS 157 STATS-->421 300 405 STATS 158 STATS-->422 300 405 STATS 159 STATS-->405 STATS 160 STATS-->300 <13> 161 STAT-->300 <14> 162 <1>-->301 <15> 163 <1>-->302 <15> 164 <1>-->303 <15> 165 <1>-->304 <15> 166 <1>-->300 <15> 167 <0>-->300 <20> 168 <13>-->404 VALUE ARITHME_TAIL 405 STATS 169 <13>-->403 VALUE ARITHME_TAIL 405 STATS 170 <13>-->402 VALUE ARITHME_TAIL 405 STATS 171 <13>-->401 VALUE ARITHME_TAIL 405 STATS 172 <13>-->400 VALUE ARITHME_TAIL 405 STATS 173 <14>-->404 VALUE ARITHME_TAIL 405 174 <14>-->403 VALUE ARITHME_TAIL 405 175 <14>-->402 VALUE ARITHME_TAIL 405 176 <14>-->401 VALUE ARITHME_TAIL 405 177 <14>-->400 VALUE ARITHME_TAIL 405 178 <15>-->404 VALUE ARITHME_TAIL 405 179 <15>-->403 VALUE ARITHME_TAIL 405 180 <15>-->402 VALUE ARITHME_TAIL 405 181 <15>-->401 VALUE ARITHME_TAIL 405 182 <15>-->400 VALUE ARITHME_TAIL 405 183 <16>-->403 VALUE ARITHME_TAIL 405 184 <16>-->401 VALUE ARITHME_TAIL 405 185 <17>-->401 VALUE ARITHME_TAIL 405 186 <20>-->404 VALUE ARITHME_TAIL 405 187 <20>-->403 VALUE ARITHME_TAIL 405 188 <20>-->402 VALUE ARITHME_TAIL 405 189 <20>-->401 VALUE ARITHME_TAIL 405 190 <20>-->400 VALUE ARITHME_TAIL 405
相关文章推荐
- java实现C语言子集的语法分析器
- 用C语言实现类似于JAVA readLine()的功能
- java实现C语言解释器:while 和 do while 循环的解释和执行
- jni实现C语言调用Java程序
- c语言实现Java语言中contains函数
- C语言实验——求三个整数的最大值(Java实现)
- 用C语言写一个数组,实现类似JAVA语言中ArrayList的功能
- 如何实现c语言中回调java函数
- C语言源程序词法分析器(Java实现)
- 利用java实现语法分析器
- java实现简单地语法分析器
- C语言和java实现且面条(均用递归实现)
- 数三退一的问题解决(C语言和Java实现)
- java如何实现类似c语言的条件汇编功能
- 在Android(Java)开发中如何实现类似C语言中的中断程序
- C语言写的方法转用java实现
- java实现简单的LL1语法分析器
- JavaSE JNI 动态注册本地方法(c语言实现native层)
- 先到先服务(FCFS)算法C语言,Java语言实现
- c语言实现一个简单的语法分析器