用java语言编写的递归下降语法分析器,是一种适合手写语法编译器的方法,且非常简单。递归下降法对语言所用的文法有一些限制,但递归下降是现阶段主流的语法分析方法,因为它可以由开发人员高度控制,在提供错误信息方面也很有优势。就连微软C#官方的编译器也是手写而成的递归下降语法分析器。

使用递归下降法编写语法分析器无需任何类库,编写简单的分析器时甚至连前面学习的词法分析库都无需使用。我们来看一个例子:现在有一种表示二叉树的字符串表达式,它的文法是:N → a ( N, N )

N → ε

其中终结符a表示任意一个英文字母,ε表示空。这个文法的含义是,二叉树的节点要么是空,要么是一个字母开头,并带有一对括号,括号中逗号左边是这个节点的左儿子,逗号右边是这个节点的右儿子。例如字符串 A(B(,C(,)),D(,))就表示这样一棵二叉树:

注意

文法规定节点即使没有儿子(儿子是空),括号和逗号也是不可省略的,所以只有一个节点的话也要写成A(,)。现在我们要写一个解析器,输入这种字符串,然后在内存中建立起这棵二叉树。

其中内存中的二叉树是用下面这样的类来表示的:class Node

{

public Node LeftChild { get; private set; }

public Node RightChild { get; private set; }

public char Label { get; private set; }

public Node(char label, Node left, Node right)

{

Label = label;

LeftChild = left;

RightChild = right;

}

}

这是一道微软面试题,曾经难倒了不少参加面试的候选人。不知在座各位是否对写出这段程序有信心呢?不少参选者想到了要用栈,或者用递归,去寻找逗号的位置将字符串拆解开来等等方法。但是若是使用递归下降法,这个程序写起来非常容易。

一般步骤:

使用一个索引来记录当前扫描的位置。通常将它做成一个整数字段。

为每个非终结符编写一个方法。

如果一个非终结符有超过一个的产生式,则在这个方法中对采用哪个产生式进行分支预测。

处理单一产生式时,遇到正确终结符则将第一步创建的扫描索引位置向前移动;如遇到非终结符则调用第二步中创建的相应方法。

如果需要产生解析的结果(比如本例中的二叉树),在方法返回之前将它构造出来。

我们马上来试验一下。首先建立一个类,然后存放一个索引变量来保存当前扫描位置。然后要为每一个非终结符创建一个方法,我们的文法中只有一个非终结符N,所以只需创建一个方法:class BinaryTreeParser

{

private string m_inputString;

private int m_index;

//初始化输入字符串和索引的构造函数,略

Node ParseNode()

{

}

}

回到刚才的产生式,我们看到非终结符N有两个产生式,所以在ParseNode方法的一开始我们必须做出分支预测。分支预测的方法是超前查看(look ahead)。就是说我们先“偷窥”当前位置前方的字符,然后判断应该用哪个产生式继续分析。非终结符N的两个产生式其中一个会产生a(N, N)这个的结构,而另一个则直接产生空字符串。那现在知道,起码有一种可能就是会遇到一个字母,这时候应该采用N → a(N, N)这个产生式继续分析。那么什么时候应该采用N → ε进行分析呢?我们观察产生式右侧所有出现N的地方,倘若N是空字符串,那么N后面的字符就会直接出现,也就是逗号和右括号。于是这就是我们的分支预测:

如果超前查看遇到英文字母,预测分支N → a(N, N)

如果超前查看遇到逗号、右括号预测分支N → ε

转化成代码就是这样:Node ParseNode()

{

int lookAheadIndex = m_index;

char lookAheadChar = m_inputString[lookAheadIndex];

if (Char.IsLetter(lookAheadChar))

{

//采用N → a(N, N)继续分析

}

else if (lookAheadChar == ',' || lookAheadChar == ')' )

{

//采用N → ε继续分析

}

else

{

throw new Exception("语法错误");

}

}

接下来我们分别来看两个分支怎么处理。先来看N → ε,这种情况下非终结符是个空字符串,所以我们不需要移动当前索引,直接返回null表示空节点。再来看N → a(N, N) 分支,倘若输入的字符串没有任何语法错误,那就应该依次遇到字母、左括号、N、逗号、N右括号。根据上面的规则,凡是遇到终结符,就移动当前索引,直接向前扫描;而要是遇到非终结符,就递归调用相应节点的方法。所以(不考虑语法错误)的完整方法代码如下:Node ParseNode()

{

int lookAheadIndex = m_index;

char lookAheadChar = m_inputString[lookAheadIndex];

if (Char.IsLetter(lookAheadChar))

{

//采用N → a(N, N)继续分析

char label = m_inputString[m_index++]; //解析字母

m_index++; //解析左括号,因为不需要使用它的值,所以直接跳过

Node left = ParseNode(); //非终结符N,递归调用

m_index++; //解析逗号,跳过

Node right = ParseNode(); //非终结符N,递归调用

m_index++; //解析右括号,跳过

return new Node(label, left, right);

}

else if (lookAheadChar == ',' || lookAheadChar == ')')

{

//采用N → ε继续分析

//无需消耗输入字符,直接返回null

return null;

}

else

{

throw new Exception("语法错误");

}

}

因为存在语法约束,所以一旦我们完成了分支预测,就能清楚地知道下一个字符或非终结符一定是什么,无需再进行任何判断(除非要进行语法错误检查)。因此根本就不需要寻找逗号在什么位置,我们解析到逗号时,逗号一定就在那,这种感觉是不是很棒?只需要寥寥几行代码就已经写出了一个完整的Parser。大家感兴趣可以继续补全一些辅助代码,然后用真正的字符串输入试验一下,是否工作正常。前面假设输入字符串的语法是正确的,但真实世界的程序总会写错,所以编译器需要能够帮助检查语法错误。在上述程序中加入语法错误检查非常容易,只要验证每个位置的字符,是否真的等于产生式中规定的终结符就可以了。这就留给大家做个练习吧。

上面我们采用的分支预测法是“人肉观察法”,编译原理书里一般都有一些计算FIRST集合或FOLLOW集合的算法,可以算出一个产生式可能开头的字符,这样就可以用自动的方法写出分支预测,从而实现递归下降语法分析器的自动化生成。ANTLR就是用这种原理实现的一个著名工具。有兴趣的同学可以去看编译原理书。其实我觉得“人肉观察法”在实践中并不困难,因为编程语言的文法都特别有规律,而且我们天天用编程语言写代码,都很有经验了。

下面我们要研究一下递归下降法对文法有什么限制。首先,我们必须要通过超前查看进行分支预测。支持递归下降的文法,必须能通过从左往右超前查看k个字符决定采用哪一个产生式。我们把这样的文法称作LL(k)文法。这个名字中第一个L表示从左往右扫描字符串,这一点可以从我们的index变量从0开始递增的特性看出来;而第二个L表示最左推导,想必大家还记得上一篇介绍的最左推导的例子。大家可以用调试器跟踪一遍递归下降语法分析器的分析过程,就能很容易地感受到它的确是最左推导的(总是先展开当前句型最左边的非终结符)。最后括号中的k表示需要超前查看k个字符。如果在每个非终结符的解析方法开头超前查看k个字符不能决定采用哪个产生式,那这个文法就不能用递归下降的方法来解析。比如下面的文法:F → id

F → ( E )

E → F * F

E → F / F

当我们编写非终结符E的解析方法时,需要在两个E产生式中进行分支预测。然而两个E产生式都以F开头,而且F本身又可能是任意长的表达式,无论超前查看多少字符,都无法判定到底应该用乘号的产生式还是除号的产生式。遇到这种情况,我们可以用提取左公因式的方法,将它转化为LL(k)的文法:F → id

F → ( E )

G → * F

G → / FE → FG

我们将一个左公因式F提取出来,然后将剩下的部分做成一个新的产生式G。在解析G的时候,很容易进行分支预测。而解析E的时候则无需再进行分支预测了。在实践中,提取左公因式不仅可以将文法转化为LL(k)型,还能有助于减少重复的解析,提高性能。

下面我们来看LL(k)文法的第二个重要的限制——不支持左递归。所谓左递归,就是产生式产生的第一个符号有可能是该产生式本身的非终结符。下面的文法是一个直截了当的左递归例子:F → id

E → E + F

E → F

这个表达式类似于我们上篇末尾得到的无歧义二元运算符的文法。但这个文法存在左递归:E产生的第一个符号就是E本身。我们想像一下,如果在编写E的递归下降解析函数时,直接在函数的开头递归调用自己,输入字符串完全没有消耗,这种递归调用就会变成一种死循环。所以,左递归是必须要消除的文法结构。解决的方法通常是将左递归转化为等价的右递归形式:F → id

E → FG

G → + FG

G → ε

大家应该牢牢记住这个例子,这不仅仅是个例子,更是解除大部分左递归的万能公式!我们将要在编写miniSharp语法分析器的时候一次又一次地用到这种变换。

Java递归下降分析器_递归下降语法分析器相关推荐

  1. lr 1 语法分析器c语言,LR语法分析器

    1 LR语法分析器 本节介绍一个有效的自底向上的分析技术,可以用于一大类上下文无关文法的语法分析.这种技术叫做LR(k)分析法,其中L表示从左到右扫描输入串,R表示构造一个最右推导的逆过程,k指的是在 ...

  2. java递归获取文件名_递归打印文件名

    class="java" name="code">import java.io.File; import java.io.IOException; pub ...

  3. java递归分苹果_递归较难题——分苹果问题

    第四届程序设计大赛 苹果 Time Limit:1000MS  Memory Limit:65536K Total Submit:90 Accepted:48 Description 把M个同样的苹果 ...

  4. java递归分苹果_递归应用示例(放苹果)[较难 选听]

    ok,下面我们再看一个例子. 这个例子呢比较有意思. 这个例子名字叫放苹果.这是open jar 的上面比较经典的一道题目. 我们来看一下这个题目.说啊,有M个同样的苹果. 这M个苹果呢一模一样没有区 ...

  5. 递归与分治java策略实验报告_递归与分治策略–计算机算法设计与分析

    递归概念:直接或者间接调用自身的算法,称为递归运算. 分治思想:把一个规模为n的问题分解为k个规模较小的子问题,这些子问题相互独立且与原问题相等,递归解决子问题后再将结果合并 下方为一些应用函数.因为 ...

  6. python函数可以递归调用吗_递归调用函数

    我有10张,20张,50张钞票,当我需要支付任何金额时,我需要找到最好的组合.例如,当我需要付7英镑时,我会付5英镑和2英镑的钞票 我有一个函数可以计算这个.但我需要运行同一个函数3到4次,如下所示. ...

  7. 斐波那契递归调用次数_递归求解斐波那契数列的时间复杂度——几种简洁证明...

    TL:DR: 暴力递归求解斐波那契数列的时间复杂度的紧界不是 ,而是 . 本文将给出几个简洁证明 用最暴力的方法求解斐波那契数列,时间复杂度是多少?具体地说,就是求下面这个程序的复杂度: def fi ...

  8. 汉诺塔递归的空间复杂度_递归思维——快速排序的详细设计过程

    理解递归的运行模型(https://zhuanlan.zhihu.com/p/166173378)之后,就能根据递归函数的静态代码推算执行结果了. 递归思维,是理解递归的基础上,用递归研究并解决问题的 ...

  9. ll1语法分析器c语言E-E T,算术表达式的 LL(1)语法分析器

    算术表达式的 LL(1)语法分析器 张会霞 (辽宁师范大学 计算机与信息技术学院,辽宁 大连 116000) 摘要:语法分析是编译程序的核心部分,对其进行研究有着重要意义.本文介绍了编译过程语法分析阶 ...

  10. 递归下降语法分析器的构建_一文了解函数式查询优化器Spark SQL Catalyst

    大数据技术与架构点击右侧关注,大数据开发领域最强公众号! 暴走大数据点击右侧关注,暴走大数据!记录一下个人对sparkSql的catalyst这个函数式的可扩展的查询优化器的理解,目录如下: 0. O ...

最新文章

  1. django邮箱验证模块
  2. springboot 中文文档_还在用 Swagger生成接口文档?我推荐你试试它
  3. 无人出价!贾跃亭所持2210万股乐视网股票首次拍卖流拍
  4. Kotlin入门(23)适配器的进阶表达
  5. zip安装mysql没有软件,windows10系统安装mysql-8.0.13(zip安装) 的教程详解
  6. fabric批量操作远程操作主机的练习
  7. mysql单向加密_MariaDB(Mysql)双向加密与单向加密 | 彬菌
  8. linux emule 编译 wx-config --libs,Linux下的wxWidgets静态编译实现方法
  9. tinymce 编辑器 上传图片
  10. oracle imp 换表空间,imp导入切换表空间
  11. 如何把拼音放在文字的右侧,干货到,在WORD中怎么把拼音和汉字同排
  12. android系统wifi控制风扇,可手机APP控制的机箱风扇,光污染新玩法
  13. php 在服务器运行不起,PHP Cookies在localhost上运行良好,但在实时服务器上不起作用...
  14. APP推广|小众APP推广渠道,总有适合你的。
  15. 春运正当时 古代人们又是靠什么出行的呢?
  16. 各类PDA及手机硬重启方法
  17. Android降低屏幕白点吗,360手机桌面强化白点工具 降低安卓使用门槛
  18. 高通SDX62平台 MBIM搜网、查询信号等功能异常
  19. Windows中快速获取文件目录的方法
  20. srand()和rand()函数详解

热门文章

  1. C语言超时错误,C语言题目运行得到超时错误
  2. linux信号量使用
  3. UE5全局光照系统Lumen解析与优化
  4. PSS E v33.40 1CD(大型电力系统仿真计算软件)
  5. Python编写九九乘法表代码以及解决方案
  6. SubSonic的配置及运用
  7. SCM供应链管理系统的优点
  8. 【ArcPy视频教程】基于 ArcGIS Pro 的 ArcPy 教程(录制中)
  9. 基于bim二次开发的智能楼宇管理系统
  10. 项目验收文档模板(三)