前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >Java递归下降分析器_递归下降语法分析器[通俗易懂]

Java递归下降分析器_递归下降语法分析器[通俗易懂]

作者头像
全栈程序员站长
发布于 2022-09-07 03:42:48
发布于 2022-09-07 03:42:48
1.2K0
举报

大家好,又见面了,我是你们的朋友全栈君。

用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语法分析器的时候一次又一次地用到这种变换。

发布者:全栈程序员栈长,转载请注明出处:https://javaforall.cn/148208.html原文链接:https://javaforall.cn

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
编译原理 期末速成
问题:源代码中括号不匹配的错误一般在编译的哪个阶段(采用五阶段划分模型)被检查出来,简述这一阶段的名称和主要任务
IsLand1314
2025/05/25
1730
编译原理 期末速成
编译原理学习笔记-5:自顶向下语法分析
在词法分析中,我们扫描输入源程序的每个字符,得到多种类型的单词(token),一系列的单词就构成了一条单词流。可以设想,单词流的某个部分有多个并排的单词,它们可能会构成某个句子,但是这个句子是否真的符合语法规则呢?我们需要借助语法分析器才能进行判断。更直接点,我们可以说语法分析器是用来判断句子是否符合某个给定的上下文无关文法的。
Chor
2020/05/12
5.3K2
用c语言手搓一个600行的类c语言解释器: 给编程初学者的解释器教程(4)- 语法分析1:EBNF和递归下降文法
用c语言手搓一个600行的类c语言解释器: 给编程初学者的解释器教程(1)- 目标和前言 用c语言手搓一个600行的类c语言解释器: 给编程初学者的解释器教程(2)- 简介和设计 用c语言手搓一个600行的类c语言解释器: 给编程初学者的解释器教程(3)- 词法分析 用c语言手搓一个600行的类c语言解释器: 给编程初学者的解释器教程(4)- 语法分析1:EBNF和递归下降文法 用c语言手搓一个600行的类c语言解释器: 给编程初学者的解释器教程(5)- 语法分析2: tryC的语法分析实现 用c语言手搓一个600行的类c语言解释器: 给编程初学者的解释器教程(6)- 语义分析:符号表和变量、函数
云微
2023/02/11
5730
65.精读《手写 SQL 编译器 - 文法介绍》
我们将一块语法规则称为 产生式,使用 “Left → Right” 表示任意产生式,用 “Left => Right” 表示产生式的推导过程,比如对于产生式:
黄子毅
2022/03/14
5910
编译原理 第四章&第五章:语法分析 LR(0)分析器 SLR(1)分析器
关于first集和follow集的求法已经放到了另一篇博客中编译原理必考大题:first集和follow集的求法
小徐在进步
2024/09/25
1.2K0
编译原理 第四章&第五章:语法分析 LR(0)分析器 SLR(1)分析器
编译原理预测分析表自顶向下的语法分析实现
递归子程序方法的思路:递归子程序法是一种确定的自顶向下语法分析方法,要求文法是LL(1)文法。它的实现思想是对应文法中每个非终结符编写一个递归过程,每个过程的功能是识别由该非终结符推出的串,当某非终结符的产生式有多个候选式时能够按LL(1)形式唯一地确定选择某个候选式进行推导。
里克贝斯
2021/05/21
1.9K0
编译原理预测分析表自顶向下的语法分析实现
第四章 自顶向下语法分析方法
基本方法:对任何输入串,试图从文法的开始符号出发, 自上而下地为输入串建立一棵语法树,或者说为输入串寻找一个最左推导。
Here_SDUT
2022/09/09
1.3K0
第四章 自顶向下语法分析方法
java编写简单的语法分析预测程序
编译原理课程中,编了一个简单的语法分析预测程序,这个程序时根据固定的文法得到预测分析表,然后编写程序来判断表达式是否会正确推到出来。
用户7886150
2020/12/13
6610
编译原理文法详解_编译原理为什么存在递归文法
学完了词法分析,我们知道词法分析器将正则表达式转换成词法单元流,但对于这个记号流我们不知道是否能由正确的文法产生,因此我们需要通过语法分析器来检测其合法性。语法分析器的输出是一棵语法分析树(无论显性还是隐性),并且进行一些语法纠错处理。语法分析的整个过程大概就是我们先定义一个语法,再用相应的算法来检测我们的词法单元流是否符合该语法。这里主要讨论上下文无关文法构成的语法和自顶向下、自底向上的语法分析。
全栈程序员站长
2022/11/17
8170
编译原理文法详解_编译原理为什么存在递归文法
漫谈模式之解释器模式
在平时编码中,其实我们或多或少的已经接触到这个解释器设计模式了。比如:使用正则表达式提取相关内容,或者判断是否符合某种格式;
孟君
2023/03/22
5680
语法分析
code-child
2023/05/30
3490
语法分析
Stanford公开课《编译原理》学习笔记(2)递归下降法
课程里涉及到的内容讲的还是很清楚的,但个别地方有点脱节,建议课下自己配合经典著作《Compilers-priciples, Techniques and Tools》(也就是大名鼎鼎的龙书)作为补充阅读。
大史不说话
2019/10/08
1.1K0
Stanford公开课《编译原理》学习笔记(2)递归下降法
66. 精读《手写 SQL 编译器 - 语法分析》
自顶而下一般采用递归下降方式处理,称为 LL(k),第一个 L 是指从左到右分析,第二个 L 指从左开始推导,k 是指超前查看的数量,如果实现了回溯功能,k 就是无限大的,所以带有回溯功能的 LL(k) 几乎是最强大的。LL 系列一般分为 LL(0)、LL(1)、LL(k)、LL(∞)。
黄子毅
2022/03/14
1.5K0
66. 精读《手写 SQL 编译器 - 语法分析》
懂前端的你也可以轻松定义自己业务的DSL
jison是一个 JavaScript 编写的解析器生成器,可以用来生成自定义的编程语言解析器。它的令人兴奋的点在于,它允许开发人员使用 JavaScript 语言来定义语法规则,然后将其转换为解析器,从而支持自定义的编程语言。
老码小张
2023/03/12
2.6K0
懂前端的你也可以轻松定义自己业务的DSL
编译入门 - 从零实现中文计算器
“如果你不知道编译器咋工作的你就不知道电脑是咋工作的。” -- STEVE YEGGE
羽月
2022/10/08
8240
编译入门 - 从零实现中文计算器
前端工程师为什么要学习编译原理?
普遍的观点认为,前端就是打好 HTML、CSS、JS 三大基础,深刻理解语义化标签,了解 N 种不同的布局方式,掌握语言的语法、特性、内置 API。再学习一些主流的前端框架,使用社区成熟的脚手架,即可快速搭建一个前端项目。胜任前端工作非常容易。再往深处学习,你会发现前端这个领域,总是有学不完的框架、工具、库,不断有新的轮子出现。技术推陈出新,版本快速迭代,但万变不离其宗。工具致力于流程自动化、规范化,服务于简洁、优雅、高效的编码,将问题高度抽象化、层次化。在如今前端开源界如此火热的现状下,框架的使用者与框架的维护者联系更加紧密,不仅能深入源码来更彻底地认识框架,还能够提出问题,参与讨论,贡献代码,共同解决技术问题,推进前端生态的发展和壮大。而编译原理,作为一门基础理论学科,除了 JS 语言本身的编译器之外,更成为 Babel、ESLint、Stylus、Flow、Pug、YAML、Vue、React、Marked 等开源前端框架的理论基石之一。了解编译原理能够对所接触的框架有更充分的认识。
Nealyang
2019/09/29
1.6K0
前端工程师为什么要学习编译原理?
编译原理 | 期末复习笔记
warning: 这篇文章距离上次修改已过396天,其中的内容可能已经有所变动。
Ranlychan
2023/03/05
1.7K0
编译原理 | 期末复习笔记
正则引擎设计与实现——基于子集构造法
在自然语言中, 以英语为例, 构成句子的最小单元,可以是单词、短语, 这些最小单元称作 词素(lexeme) . 词素具有属性, 比如动词、名词、副词、形容词等, 这些属性决定了语法层面, 其在句子里可充当的成分.
hunterzju
2023/05/09
3590
正则引擎设计与实现——基于子集构造法
理解递归下降分析和parsec应用
本文将会从上下文无关文法开始介绍,从使用 BNF 描述语法到理解递归下降分析思想,最后实现一个简单的 html 解析器收尾。本文的亮点是使用 typescript 编写组合子编译器,对于前端开发某些特定领域会有重要意义和价值。同时本文注重实用价值,配合简短 js 代码示例来帮助理解。
玖柒的小窝
2021/10/31
1.8K0
理解递归下降分析和parsec应用
编译原理学习(到LL1文法部分)
机器语言:计算机只认识由0和1构成的机器语言,每台机器自己独特的指令系统即机器语言。 机器语言->汇编语言->高级语言 编译程序最初的定义是把一种高级语言设计的源程序(面向人的)翻译成另一种等价的低级程序设计语言(面向硬件的)即机器语言或汇编语言。
且陶陶
2023/04/12
8170
编译原理学习(到LL1文法部分)
推荐阅读
相关推荐
编译原理 期末速成
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档