用java语言编写的递归下降语法分析器,是一种适合手写语法编译器的方法,且非常简单。递归下降法对语言所用的文法有一些限制,但递归下降是现阶段主流的语法分析方法,因为它可以由开发人员高度控制,在提供错误信息方面也很有优势。就连微软C#官方的编译器也是手写而成的递归下降语法分析器。
使用递归下降法编写语法分析器无需任何类库,编写简单的分析器时甚至连前面学习的词法分析库都无需使用。我们来看一个例子:现在有一种表示二叉树的字符串表达式,它的文法是:N→a(N,N)N→ε
其中终结符a表示任意一个英文字母,ε表示空。这个文法的含义是,二叉树的节点要么是空,要么是一个字母开头,并带有一对括号,括号中逗号左边是这个节点的左儿子,逗号右边是这个节点的右儿子。例如字符串A(B(,C(,)),D(,))就表示这样一棵二叉树:
注意
文法规定节点即使没有儿子(儿子是空),括号和逗号也是不可省略的,所以只有一个节点的话也要写成A(,)。现在我们要写一个解析器,输入这种字符串,然后在内存中建立起这棵二叉树。
其中内存中的二叉树是用下面这样的类来表示的:
classNode{publicNodeLeftChild{get;privateset;}publicNodeRightChild{get;privateset;}publiccharLabel{get;privateset;}publicNode(charlabel,Nodeleft,Noderight){Label=label;LeftChild=left;RightChild=right;}}
这是一道微软面试题,曾经难倒了不少参加面试的候选人。不知在座各位是否对写出这段程序有信心呢?不少参选者想到了要用栈,或者用递归,去寻找逗号的位置将字符串拆解开来等等方法。但是若是使用递归下降法,这个程序写起来非常容易。
一般步骤:
使用一个索引来记录当前扫描的位置。通常将它做成一个整数字段。
为每个非终结符编写一个方法。
如果一个非终结符有超过一个的产生式,则在这个方法中对采用哪个产生式进行分支预测。
处理单一产生式时,遇到正确终结符则将第一步创建的扫描索引位置向前移动;如遇到非终结符则调用第二步中创建的相应方法。
如果需要产生解析的结果(比如本例中的二叉树),在方法返回之前将它构造出来。
我们马上来试验一下。首先建立一个类,然后存放一个索引变量来保存当前扫描位置。然后要为每一个非终结符创建一个方法,我们的文法中只有一个非终结符N,所以只需创建一个方法:
classBinaryTreeParser{privatestringm_inputString;privateintm_index;//初始化输入字符串和索引的构造函数,略NodeParseNode(){}}
回到刚才的产生式,我们看到非终结符N有两个产生式,所以在ParseNode方法的一开始我们必须做出分支预测。分支预测的方法是超前查看(lookahead)。就是说我们先“偷窥”当前位置前方的字符,然后判断应该用哪个产生式继续分析。非终结符N的两个产生式其中一个会产生a(N,N)这个的结构,而另一个则直接产生空字符串。那现在知道,起码有一种可能就是会遇到一个字母,这时候应该采用N→a(N,N)这个产生式继续分析。那么什么时候应该采用N→ε进行分析呢?我们观察产生式右侧所有出现N的地方,倘若N是空字符串,那么N后面的字符就会直接出现,也就是逗号和右括号。于是这就是我们的分支预测:
如果超前查看遇到英文字母,预测分支N→a(N,N)
如果超前查看遇到逗号、右括号预测分支N→ε
转化成代码就是这样:
NodeParseNode(){intlookAheadIndex=m_index;charlookAheadChar=m_inputString[lookAheadIndex];if(Char.IsLetter(lookAheadChar)){//采用N→a(N,N)继续分析}elseif(lookAheadChar==','||lookAheadChar==')'){//采用N→ε继续分析}else{thrownewException("语法错误");}}
接下来我们分别来看两个分支怎么处理。先来看N→ε,这种情况下非终结符是个空字符串,所以我们不需要移动当前索引,直接返回null表示空节点。再来看N→a(N,N)分支,倘若输入的字符串没有任何语法错误,那就应该依次遇到字母、左括号、N、逗号、N右括号。根据上面的规则,凡是遇到终结符,就移动当前索引,直接向前扫描;而要是遇到非终结符,就递归调用相应节点的方法。所以(不考虑语法错误)的完整方法代码如下:
NodeParseNode(){intlookAheadIndex=m_index;charlookAheadChar=m_inputString[lookAheadIndex];if(Char.IsLetter(lookAheadChar)){//采用N→a(N,N)继续分析charlabel=m_inputString[m_index++];//解析字母m_index++;//解析左括号,因为不需要使用它的值,所以直接跳过Nodeleft=ParseNode();//非终结符N,递归调用m_index++;//解析逗号,跳过Noderight=ParseNode();//非终结符N,递归调用m_index++;//解析右括号,跳过returnnewNode(label,left,right);}elseif(lookAheadChar==','||lookAheadChar==')'){//采用N→ε继续分析//无需消耗输入字符,直接返回nullreturnnull;}else{thrownewException("语法错误");}}
因为存在语法约束,所以一旦我们完成了分支预测,就能清楚地知道下一个字符或非终结符一定是什么,无需再进行任何判断(除非要进行语法错误检查)。因此根本就不需要寻找逗号在什么位置,我们解析到逗号时,逗号一定就在那,这种感觉是不是很棒?只需要寥寥几行代码就已经写出了一个完整的Parser。大家感兴趣可以继续补全一些辅助代码,然后用真正的字符串输入试验一下,是否工作正常。前面假设输入字符串的语法是正确的,但真实世界的程序总会写错,所以编译器需要能够帮助检查语法错误。在上述程序中加入语法错误检查非常容易,只要验证每个位置的字符,是否真的等于产生式中规定的终结符就可以了。这就留给大家做个练习吧。
上面我们采用的分支预测法是“人肉观察法”,编译原理书里一般都有一些计算FIRST集合或FOLLOW集合的算法,可以算出一个产生式可能开头的字符,这样就可以用自动的方法写出分支预测,从而实现递归下降语法分析器的自动化生成。ANTLR就是用这种原理实现的一个著名工具。有兴趣的同学可以去看编译原理书。其实我觉得“人肉观察法”在实践中并不困难,因为编程语言的文法都特别有规律,而且我们天天用编程语言写代码,都很有经验了。
下面我们要研究一下递归下降法对文法有什么限制。首先,我们必须要通过超前查看进行分支预测。支持递归下降的文法,必须能通过从左往右超前查看k个字符决定采用哪一个产生式。我们把这样的文法称作LL(k)文法。这个名字中第一个L表示从左往右扫描字符串,这一点可以从我们的index变量从0开始递增的特性看出来;而第二个L表示最左推导,想必大家还记得上一篇介绍的最左推导的例子。大家可以用调试器跟踪一遍递归下降语法分析器的分析过程,就能很容易地感受到它的确是最左推导的(总是先展开当前句型最左边的非终结符)。最后括号中的k表示需要超前查看k个字符。如果在每个非终结符的解析方法开头超前查看k个字符不能决定采用哪个产生式,那这个文法就不能用递归下降的方法来解析。比如下面的文法:F→idF→(E)E→F*FE→F/F
当我们编写非终结符E的解析方法时,需要在两个E产生式中进行分支预测。然而两个E产生式都以F开头,而且F本身又可能是任意长的表达式,无论超前查看多少字符,都无法判定到底应该用乘号的产生式还是除号的产生式。遇到这种情况,我们可以用提取左公因式的方法,将它转化为LL(k)的文法:F→idF→(E)G→*FG→/FE→FG
我们将一个左公因式F提取出来,然后将剩下的部分做成一个新的产生式G。在解析G的时候,很容易进行分支预测。而解析E的时候则无需再进行分支预测了。在实践中,提取左公因式不仅可以将文法转化为LL(k)型,还能有助于减少重复的解析,提高性能。
下面我们来看LL(k)文法的第二个重要的限制——不支持左递归。所谓左递归,就是产生式产生的第一个符号有可能是该产生式本身的非终结符。下面的文法是一个直截了当的左递归例子:F→idE→E+FE→F
这个表达式类似于我们上篇末尾得到的无歧义二元运算符的文法。但这个文法存在左递归:E产生的第一个符号就是E本身。我们想像一下,如果在编写E的递归下降解析函数时,直接在函数的开头递归调用自己,输入字符串完全没有消耗,这种递归调用就会变成一种死循环。所以,左递归是必须要消除的文法结构。解决的方法通常是将左递归转化为等价的右递归形式:F→idE→FGG→+FGG→ε
大家应该牢牢记住这个例子,这不仅仅是个例子,更是解除大部分左递归的万能公式!我们将要在编写miniSharp语法分析器的时候一次又一次地用到这种变换。
Tags:递归.
小编点评:活动窗口记录器ActiveWindo.
下载小编点评:ThinkPHP视频打赏平台源码,
下载小编点评:固码免签监控系统这套源码是客户花了.
下载小编点评:软件介绍文本处理辅助工具是一款非常专业.
下载小编点评:软件介绍《疯狂小鸡卡丁车3》是一款怪鸡卡丁车.
下载小编点评:软件介绍RogerHicks与Pa.
下载小编点评:梦幻捕鱼千炮版手游是一款超经典的街.
下载小编点评:真武修仙最新版是一款画面细腻的仙侠手游.
下载小编点评:山谷物语国际服是一款极具欧美色彩的卡.
下载小编点评:鸿钧道传游戏是一款跨服战斗竞技手游,画面.
下载小编点评:花伴识别app是一个植物识别软件,
下载小编点评:节奏象棋手机版是一款好玩的象棋风格.
下载小编点评:翻滚吧内马尔内购破解版是一款非常魔性.
下载下坡滑行手游下载-下坡滑行手机版下载v1.0.1 安卓版
水滴解谜游戏下载-3ds水滴解谜游戏下载v7.04 安卓版
华为兔子吃萝卜下载-华为兔子吃萝卜游戏下载v3.4.0 安卓版
形之谜游戏下载-形之谜手机版下载v1.3.144 安卓版
屹立不倒安卓下载-屹立不倒游戏下载v1.0 安卓最新版
三星快速分享连接app下载安装-三星快速分享连接最新版下载v1.5.02.3 安卓版
小三软件库app官方版免费下载-小三软件库手机版下载v5.0.0 安卓最新版
工地记工时软件下载-工地记工时app下载v1.06 安卓版
美国篮球季后赛手机版下载-美国篮球季后赛游戏下载v2.0.0 安卓版
开心花园游戏无限金币版下载-开心花园破解版下载v1.0.1 安卓版
发布引擎app下载安装-发布引擎平台2024下载v2.1.12 官方安卓版
mp3转换免费软件下载-MP3转换APP(又名音频剪辑)下载v4.35.323 安卓版
raptormark软件下载-raptormark最新版下载v0.3.0 安卓版