来源:http://www.360doc.com/content/07/0625/16/13829_578971.shtml
第 1 部分 使用 BNF 和 JavaCC 构建定制的解析器
复杂语言的语法通常都是使用 BNF描述的。自动化工具可以使用那些描述(我将使用通用的术语 BNF来 指代这两种变体)或与它们近似的描述来为您生成解析代码。本文就描述了这样的一种解析器-生成器工具,称为 JavaCC。
一个非常简单的 BNF 开始,它描述了一种语言,该语言仅由两个只对整数进行运算的算术运算符构成。我称这种语言为 SimpleLang
:
simpleLang ::= integerLiteral ( ( "+" | "-" ) integerLiteral )? integerLiteral ::= [ 0-9 ]+ |
该语法中的每个规则都是一个 结果(production) ,其中左边的项(结果的名称)是依据语法中的其它结果描述的。最上面的结果 simpleLang
表明,该语言中有效的(或合法的)表达式是这样构成的,一个整数值,可以任意选择其后跟一个加号(+)或减号(-)以及另一个整数值或不跟任何东西。按照 这种语法,单个整数“42”是有效的,同样,表达式“42 + 1”也是有效的。第二个结果以类 regex 的方式更特定地描述了一个整数值看上去象什么:一个或多个数字的连续序列。
该语法描述了两个结果 simpleLang
和 integerLiteral
之间存在的抽象关系。它还详细描述了三个 记号(加号、减号和整数)组合的具体项,解析器在扫描整个输入流时希望遇到这些项。解析器中负责该任务的部件称为 扫描器(scanner)或 记号赋予器(tokenizer) 一点也不稀奇。在该语言中, simpleLang
是 非终端(non-terminal) 符号的一个示例,它对其它结果进行引用;另一方面,规则 integerLiteral
描述了 终端(terminal)符号:这是一种不能进一步分解成其它结果的符号。
如果解析器在其扫描期间发现了除这三个记号外的任何 其它记号,则认为它正在扫描的表达式是无效的。解析器的主要工作之一就是确定您传递给它的任何表达式的有效性,并且让您知道。一旦认为某个表达式是有效的,则它的第二项工作是将输入流分解成其组件块,并以某个有用的方式将它们提供给您。
JavaCC 使用称为 .jj 的文件。该文件中的语法描述是使用非常类似于 BNF 的表示法编写的,这样从一种形式转换到另一种形式通常就相当容易。(该表示法有自己的语法,从而使其在 JavaCC 中是可表达的。)JavaCC .jj 文件语法和标准的 BNF 之间的主要区别在于:利用 JavaCC 版本,您可以在语法中嵌入操作。一旦成功遍历了语法中的那些部分,则执行这些操作。操作都是 Java 语句,它们是解析器 Java 源代码的一部分,该部分作为解析器生成过程的一部分产生。
调用记号赋予器(Tokenizer.java)以返回输入流中的下一个记号。记号赋予器穿过输入流,每次检查一个字符,直到它遇到一个整数或者直至文件结束。如果是前者,则以 <INT>
记号将值“包”起来;如果是后者,则当作 <EOF>
;并将记号返回给 integerLiteral()
做进一步处理。如果记号赋予器未遇到这两个记号,则返回词法错误。
当您对 .jj 文件运行 JavaCC 时,它会生成许多 Java 源文件。JavaCC 总共生成了以下七个 Java 文件。前三个是特定于这个特殊语法的;后四个是通用的助手类 Java 文件,无论语法是怎么样的,都会生成这几个文件。
- Parser_1.java
- Parser_1Constants.java
- Parser_1TokenManager.java
- ParseException.java
- SimpleCharStream.java
- Token.java
- TokenMgrError.java
一 旦 JavaCC 生成了这七个 Java 源文件,则可以编译它们并将它们链接到您的 Java 应用程序中。
第 2 部分 使用 JJTree 构建和遍历定制解析树
它是 JavaCC 的伙伴工具。JJTree 被设置成提供一个解析器,该解析器在运行时的主要工作不是执行嵌入的 Java 操作,而是构建正在解析的表达式的独立解析树表示。
要使用 JJTree,您需要能够:
- 创建 JJTree 作为输入获取的 .jjt 脚本
- 编写客户机端代码以遍历在运行时生成的解析树并对其求值
JJTree 是一个预处理器,为特定 BNF 生成解析器只需要简单的两步:
- 对所谓的 .jjt 文件运行 JJTree;它会产生一个中间的 .jj 文件
- 用 JavaCC 编译该文件( 第 1 部分中讨论了这个过程)
.jjt 文件的结构只是我在第 1 部分中向您显示的 .jj 格式的较小扩展。主要区别是 JJTree 添加了一个新的语法 node-constructor构造,该构造可以让您指定在解析期间在哪里以及在什么条件下生成解析树节点。换句话说,该构造管理由解析器构造的解析树的形状和内容。
树构建器在构造树期间使用节点堆栈;没有参数的节点构建器的缺省行为是将自己放在正在构造的解析树的顶部,将所有节点弹出在同一个 节点作用域 中创建的节点堆栈,并把自己提升到那些节点父代的位置。参数 2
告诉新的父节点(在此示例中是一个 Add
节点)要恰好采用 两个子节点,它们是 下一段文字 中描述的两个 IntLiteral
子节点。JJTree 文档更详细地描述了这个过程。使用好的调试器在运行时遍历解析树是另一个宝贵的辅助方法,它有助于理解树构建在 JJTree 中是如何工作的。
在 .jjt 脚本中声明的每个节点都指示解析器生成 JJTree SimpleNode
的一个子类。接下来, SimpleNode
又实现名为 Node
的 Java 接口。这两个类的源文件都是由 JJTree 脚本和定制 .jj 文件一起自动生成的。 清单 1 显示了定制 .jj 文件的当前示例。在当前示例中,JJTree 还提供了您自己的 Root
、 Add
和 IntLiteral
类以及没有在这里看到的一些附加的助手类的源文件。
所有 SimpleNode
子类都继承了有用的行为。 SimpleNode
方法 dump()
就是这样一个例子。它还充当了我以前的论点(使用解析树使调试更容易,从而缩短开发时间)的示例。以下三行客户机端代码的代码片段实例化了解析器、调用解析器、抓取所返回的解析树,并且将一个简单的解析树的文本表示转储到控制台:
SimpleParser parser = new SimpleParser(new StringReader( expression )); SimpleNode rootNode = parser.simpleLang(); rootNode.dump(); |
另一个有用的内置 SimpleNode
方法是 jjtGetChild(int)
。当您在客户机端向下浏览解析树,并且遇到 Add
节点时,您会要抓取它的 IntLiteral
子节点、抽取它们表示的整数值,并将这些数字加到一起 ― 毕竟,那是用来练习的。假设下一段代码中显示的 addNode
是表示我们感兴趣的 Add
类型节点的变量,那我们就可以访问 addNode
的两个子节点。( lhs
和 rhs
分别是 左边(left-hand side)和 右边(right-hand side)的常用缩写。)
SimpleNode lhs = addNode.jjtGetChild( 0 ); SimpleNode rhs = addNode.jjtGetChild( 1 ); |
要将所扫描的记号的值存储到适当的节点中,将以下修改添加到 SimpleNode
:
public class SimpleNode extends Node { String m_text; public void setText( String text ) { m_text = text; } public String getText() { return m_text; } ... } |
将 JJTree 脚本中的以下结果:
void integerLiteral() : #IntLiteral {} <INT> } |
更改成:
void integerLiteral() : #IntLiteral { Token t; } { t=<INT> { jjtThis.setText( t.image );} } |
该结果抓取它刚在 t.image
中遇到的整数的原始文本值,并使用您的 setText()
setter 方法将该字符串存储到当前节点中。 清单 5 中的客户机端 eval()
代码显示了如何使用相应的 getText()
getter 方法。