实用javacc
前言
本系列的文章的宗旨是让大家能够写出自己的编译器,解释器或者脚本引擎,所以每到理论介绍到一个程度后,我都会来讨论实践问题.理论方面,编译原理的教材已经是够多了,而实践的问题却很少讨论.
前几节文章只讨论到了词法分析和LL文法分析,关键的LR文法分析这里却还没有讲,我们先不要管复杂的LR文法和算法,让我们使用LL算法来实际做一些东西后再说.本文将介绍一个在JAVA上广泛使用的LL算法分析工具Javacc.(这是我唯一能找到的使用LL算法的语法分析器构造工具).这一节的文章并非只针对JAVA开发者,如果你是C/C++开发者,那么也请你来看看这个JAVA下的优秀工具,或许你将来也用得着它.
Lex 和yacc这两个工具是经典的词法分析和语法分析工具,但是它们都是基于C语言下面的工具,而使用JAVA的朋友们就用不上了.但是JAVA下已经有了lex和yacc的替代品javacc( Java Compiler Compiler ) . 同时javacc也是使用LL算法的工具,我们也可以实践一下前面学的LL算法.
首先声明我不是一个JAVA专家,我也是刚刚才接触JAVA.Java里面或许有很多类似javacc一样的工具,但是据我所知,javacc还是最广泛,最标准的JAVA下的词法语法分析器.
Javacc 的获取
同lex和yacc一样,javacc也是一个免费可以获取的通用工具,它可以在很多JAVA相关的工具下载网站下载,当然,javacc所占的磁盘空间比起lex和yacc更大一些,里面有标准的文档和examples.相对lex和yacc来说,javacc做得更人性化,更容易一些.如果你实在找不到javacc,还是可以联系我,我这里有.现在最新的就是javacc 3.2版本.
Javacc 的原理
Javacc 可以同时完成对text的词法分析和语法分析的工作,使用起来相当方便.同样,它和lex和yacc一样,先输入一个按照它规定的格式的文件,然后javacc根据你输入的文件来生成相应的词法分析于语法分析程序.同时,新版本的Javacc除了常规的词法分析和语法分析以外,还提供JJTree等工具来帮助我们建立语法树.总之,Javacc在很多地方做得都比lex和yacc要人性化,这个在后面的输入文件格式中也能体现出来.
Javacc 的输入文件
Javacc 的输入文件格式做得比较简单.每个非终结符产生式对应一个Class中的函数,函数中可以嵌入相应的识别出该终结符文法时候的处理代码(也叫动作).这个与YACC中是一致的.
Javacc 的输入文件中,有一系列的系统参数,比如其中lookahead可以设置成大于1的整数,那么就是说,它可以为我们生成LL(k)算法(k>=1),而不是简单的递归下降那样的LL(1)算法了.要知道,LL(2)文法比起前面讨论的LL(1)文法判断每个非终结符时候需要看前面两个记号而不是一个,那么对于文法形式的限制就更少.不过LL(2)的算法当然也比LL(1)算法慢了不少.作为一般的计算机程序设计语言,LL(1)算法已经是足够了.就算不是LL(1)算法,我们也可以通过前面讲的左提公因式把它变成一个LL(1)文法来处理.不过既然javacc都把lookahead选择做出来了,那么在某些特定的情况下,我们可以直接调整一个lookahead的参数就可以,而不必纠正我们的文法.
下面我们来看看Javacc中自带的example中的例子.
例5.1
这个例子可以在javacc-3.2/doc/examples/SimpleExamples/Simple1.jj看到
PARSER_BEGIN(Simple1)
public class Simple1 {
public static void main(String args[]) throws ParseException {
Simple1 parser = new Simple1(System.in);
parser.Input();
}
}
PARSER_END(Simple1)
void Input() :
{}
{
MatchedBraces() ("\n"|"\r")* <EOF>
}
void MatchedBraces() :
{}
{
"{" [ MatchedBraces() ] "}"
}
设置好javacc的bin目录后,在命令提示符下输入
javacc Simple1.jj
然后 javacc 就会为你生成下面几个 java 源代码文件
Simple1.java
Simple1TokenManager.java
Simple1Constants.java
SimpleCharStream.java
Token.java
TokenMgrError.java
其中Simple1就是你的语法分析器的对象,它的构造函数参数就是要分析的输入流,这里的是System.in.
class Simple1 就定义在标记 PARSER_BEGIN(Simple1)
PARSER_END(Simple1) 之间.
但是必须清楚的是,PARSER_BEGIN和PARSER_END中的名字必须是词法分析器的名字(这里是Simple1).
PARSER_END 下面的定义就是文法非终结符号的定义了.
Simple1 的文法基本就是:
Input -> MatchedBraces ("\n"|"\r")* <EOF>
MatchedBraces -> “ { “ MatchedBraces “ } ”
从它的定义我们可以看到 , 每个非终结符号对于一个过程 .
比如 Input 的过程
void Input() :
{}
{
MatchedBraces() ("\n"|"\r")* <EOF>
}
在定义 void Input 后面记住需要加上一个冒号 ”:”, 然后接下来是两个块 {} 的定义 .
第一个 {} 中的代码是定义数据 , 初试化数据的代码 . 第二个 {} 中的部分就是真正定义 Input 的产生式了 .
每个产生式之间用 ”|” 符号连接 .
注意 : 这里的产生式并非需要严格 BNF 范式文法 , 它的文法既可以是 BNF, 同时还可以是混合了正则表达式中的定义方法 . 比如上面的
Input -> MatchedBraces ("\n"|"\r")* <EOF>
中 (“\n”|”\r”)* 就是个正则表达式 , 表示的是 \n 或者 \r 的 0 个到无限个的重复的记号 .
而 <EOF> 是 javacc 系统定义的记号 (TOKEN), 表示文件结束符号 .
除了 <EOF>, 无论是系统定义的 TOKEN, 还是自定义的 TOKEN, 里面的 TOKEN 都是以 <token’s name> 的方式表示 .
每个非终结符号 (Input 和 MatchedBraces) 都会在 javacc 生成的 Simple1.java 中形成 Class Simple1 的成员函数 . 当你在外部调用 Simple1 的 Input, 那么语法分析器就会开始进行语法分析了 .
例 5.2
在 javacc 提供的 example 里面没有 .javacc 提供的 example 里面提供的例子中 SimpleExamples 过于简单 , 而其它例子又过于庞大 . 下面我以我们最常见的数学四则混合运算的文法来构造一个 javacc 的文法识别器 . 这个例子是我自己写的 , 十分简单 ,. 其中还包括了文法识别同时嵌入的构建语法树 Parse-Tree 的代码 . 不过由于篇幅的原因 , 我并没有给出全部的代码 , 这里只给了 javacc 输入部分相关的代码 . 而 Parse-tree 就是一个普通的 4 叉树 ,3 个 child,1 个 next( 平行结点 ), 相信大家在学习数据结构的时候应该都是学过的 . 所以这里就省略过去了 .
在大家看这些输入代码之前 , 我先给出它所使用的文法定义 , 好让大家有个清楚的框架 .
Expression -> Term{ Addop Term }
Addop -> "+" | "-"
Term -> Factor { Mulop Factor }
Mulop -> "*" | "/"
Factor -> ID | NUM | "("Expression")"
这里的文法可能和BNF范式有点不同.{}的意思就是0次到无限次重复,它跟我们在学习正则表达式的时候的”*”符号相同,所以,在Javacc中的文法表示的时候,{…}部分的就是用(…)*来表示.
为了让词法分析做得更简单 , 我们通常都不会在文法分析的时候 , 使用 ”(”,”)“ 等字符号串来表示终结符号 , 而需要转而使用 LPAREN , RPAREN 这样的整型符号来表示 .
PARSER_BEGIN(Grammar)
public class Grammar implements NodeType {
public ParseTreeNode GetParseTree(InputStream in) throws ParseException
{
Grammar parser =new Grammar(in);
return parser.Expression();
}
}
PARSER_END(Grammar)
SKIP :
{
" " | "\t" | "\n" | "\r"
}
TOKEN :
{
< ID: ["a"-"z","A"-"Z","_"] ( ["a"-"z","A"-"Z","_","0"-"9"] )* >
| < NUM: ( ["0"-"9"] )+ >
| < PLUS: "+" >
| < MINUS: "-" >
| < TIMERS: "*" >
| < OVER: "/" >
| < LPAREN: "(" >
| < RPAREN: ")" >
}
ParseTreeNode Expression() :
{
ParseTreeNode ParseTree = null;
ParseTreeNode node;
}
{
( node=Simple_Expression()
{
if(ParseTree == null)
ParseTree =node;
else
{
ParseTreeNode t;
t= ParseTree;
while(t.next != null)
t=t.next;
t.next = node;
}
}
)*
{ return ParseTree;}
<EOF>
}
ParseTreeNode Simple_Expression() :
{
ParseTreeNode node;
ParseTreeNode t;
int op;
}
{
node=Term(){}
(
op=addop() t=Term()
{
ParseTreeNode newNode = new ParseTreeNode();
newNode.nodetype = op;
newNode.child[0] = node;
newNode.child[1] = t;
switch(op)
{
case PlusOP:
newNode.name = "Operator: +";
break;
case MinusOP:
newNode.name = "Operator: -";
break;
}
node = newNode;
}
)*
{ return node; }
}
int addop() : {}
{
<PLUS> { return PlusOP; }
| <MINUS> { return MinusOP; }
}
ParseTreeNode Term() :
{
ParseTreeNode node;
ParseTreeNode t;
int op;
}
{
node=Factor(){}
(
op=mulop() t=Factor()
{
ParseTreeNode newNode = new ParseTreeNode();
newNode.nodetype = op;
newNode.child[0] = node;
newNode.child[1] = t;
switch(op)
{
case TimersOP:
newNode.name = "Operator: *";
break;
case OverOP:
newNode.name = "Operator: /";
break;
}
node = newNode;
}
)*
{
return node;
}
}
int mulop() :{}
{
<TIMERS> { return TimersOP; }
| <OVER> { return OverOP; }
}
ParseTreeNode Factor() :
{
ParseTreeNode node;
Token t;
}
{
t=<ID>
{
node=new ParseTreeNode();
node.nodetype= IDstmt;
node.name = t.image;
return node;
}
|
t=<NUM>
{
node=new ParseTreeNode();
node.nodetype= NUMstmt;
node.name = t.image;
node.value= Integer.parseInt(t.image);
return node;
}
|
<LPAREN> node=Simple_Expression() <RPAREN>
{
return node;
}
}
其中 SKIP 中的定义就是在进行词法分析的同时 , 忽略掉的记号 .TOKEN 中的 , 就是需要在做词法分析的时候 , 识别的词法记号 . 当然 , 这一切都是以正则表达式来表示的 .
这个例子就有多个非终结符号 , 可以看出 , 我们需要为每个非终结符号写出一个过程 . 不同的非终结符号的识别过程中可以互相调用 .
以 Simple_Expression() 过程为例 , 它的产生式是 Expression -> Term { addop Term }, 而在 javacc 的输入文件格式是,它的识别是这样写的 node=Term(){} ( op=addop() t=Term(){ … })* 前面说过,这里的 ”*” 符号和正则表达式是一样的,就是 0 次到无限次的重复 . 那么 Simple_Expression 等于文法 Term Addop Term Addop Term Addop Term … 而 Addop 也就相当于 PLUS 和 MINUS 两个运算符号 . 这里我们在写 Expression 的文法的时候,同时还使用了赋值表达式,因为这个和 Yacc 不同的时候, Javacc 把文法识别完全地做到了函数过程中,那么如果我们要识别 Simple_Expression 的文法,就相当于按顺序识别 Term 和 Addop 两个文法 , 而识别那个文法,就相当于调用那两个非终结符的识别函数 . 正是这一点,我觉得 Javacc 的文法识别处理上就很接近程序的操作过程 , 我们不需要像 YACC 那样使用严格的文法表示格式,复杂的系统参数了 .
关于 Yacc 的使用,其实比 Javacc 要复杂,还需要考虑到和词法分析器接口的问题,这个我会在以后细细讲到 .
至于其它的文法操作解释我就不再多说了 , 如果要说 , 就是再写上十篇这样的文章也写不完 . 本文只能给读者们一个方向 , 至于深入的研究 , 还是请大家看 javacc 提供的官方文档资料 .
最后
由于国外使用 JAVA 做项目的程序员比国内多 , 那么讨论 JAVA 技术的人员也比较多 . 可能来这里读我的文章的人都是 C/C++ 程序员 , 但是关注其它领域同方向的技术也是可以让我们的知识领域更加宽广 . 关于 JavaCC 的讨论主要是在国际新闻组 comp.compilers.tools.javacc 如果大家在使用 JavaCC 做实际问题的时候遇到什么问题 , 不妨上去找找专家 .
分享到:
相关推荐
在编译原理中,语法分析是将词法单元流转换为抽象语法树(AST)的过程。这个过程涉及到对输入源代码的结构分析,确认它是否符合预定的语法规则。AST是一个树形结构,每个节点代表源代码中的一个语法结构,如表达式、...
`testcc`目录可能是测试用例或示例代码,用来演示如何使用JavaCC进行词法分析和语法解析。在实际开发中,你会在这里编写单元测试和示例输入,以确保JavaCC生成的解析器能够正确地识别和处理各种输入。 总之,JavaCC...
4. **语法分析**:JavaCC生成的解析器会解析词法分析器产生的Token序列,构建出语法树。这个过程是通过解析产生式完成的,每个产生式对应一个解析规则。 5. **自定义语法**:JavaCC的强大之处在于它的灵活性。用户...
用Javacc实现MiniC的词法分析、语法分析、语义分析。在词法分析部分根据单词的构词规则分类,输出<单词种别,单词自身值>二元式;在语法分析部分利用Javacc实现LL(1)文法,判断源语言是否符合MiniC的语法,如果不...
在这个特定的场景中,我们关注的是如何使用JavaCC来实现CMM(可能是“计算机动画建模语言”或“计算机辅助制造”等特定领域的语法)的语法分析。下面我们将深入探讨JavaCC的工作原理、CMM语法分析的关键步骤以及如何...
总的来说,JavaCC语法文件是构建编译器前端的一个重要组成部分,它能够准确地描述编程语言的语法,并生成相应的词法分析器和语法分析器。通过学习JavaCC语法文件,可以更深入地理解编译原理,并能够为特定的语言实现...
在这个“使用Javacc做的解释器”项目中,我们将深入探讨如何利用JavaCC进行词法分析和语法分析,构建一个自定义语言的解释器。 词法分析是编译器或解释器的第一步,它将源代码中的字符流转换为有意义的词法单元...
这个压缩包包含的源代码提供了学习和实践JavaCC语法分析与词法分析的实例。 JavaCC的工作原理基于上下文无关语法(Context-Free Grammar, CFG),这是一种描述编程语言结构的形式化方法。在JavaCC中,用户可以定义...
本项目“基于Minic的语法分析器(javacc)”专注于使用JavaCC工具来实现这一过程。JavaCC,全称为Java Compiler Compiler,是一款强大的、开源的词法和语法解析器生成器,它允许开发者用Java语言描述语法规则,然后...
设计MiniC的上下文无关...按照MiniC语言的语法规则检查词法分析输出的记号流是否符合这些规则,并根据这些规则所体现出的语言中的各种语法结构的层次性。把规则写入到JavaCC的 .jjt文件中,可以生成树状的层次结构。
1. 掌握 JavaCC 语法分析器工作原理; 2. 设计 MiniC 的上下文无关文法,在“Parser.jjt”文件中表示该文法,生成调试递归下降分析程序,以便对任意输入的符号串进行分析; 3. 输出语法树。 4. 以文件流的形式读入...
- JavaCC(Java Compiler Compiler)是一个自动生成词法分析器和语法分析器的工具。它读取一个语法描述文件(扩展名为`.jj`),生成Java源代码,该代码可以解析符合该语法的输入字符串。 - 在本教程中,`minijava....
本主题聚焦于使用Java实现一个语法分析器,同时也涉及到了词法分析器的构建。 **词法分析器(Lexer)**: 词法分析器是编译器的第一个阶段,它的任务是从源代码中识别出一个个的词法单元(Token),这些词法单元是...
在实际项目中,Java有许多库可以帮助我们实现词法分析和语法分析,如ANTLR、JavaCC等,它们提供了现成的工具和框架,使得开发过程更为便捷。然而,手动编写词法分析器和语法分析器有助于深入理解编译原理,提升编程...
JavaCC(Java Compiler Compiler)是一种强大的工具,用于生成解析器和词法...在项目中,`LexicalTest.java`是测试词法分析器的关键,它展示了如何使用JavaCC生成的类来处理源代码文件,并输出种别编码和对应的单词。
在Java中,语法分析通常使用自底向上或自顶向下的方法进行。自底向上方法如LL解析(Left-to-Right,Leftmost Derivation)从输入序列开始,尝试匹配文法规则。而自顶向下方法如LR解析(Left-to-Right,Rightmost ...
cmm javacc 对CMM语言的词法语法分析器的自动实现
JavaCC(Java Compiler Compiler)是一种Java语言的词法分析器和语法分析器生成工具,用于构建解析器和词法分析器。在这个特定的文件中,`whileParse`是使用JavaCC编写的解析器,用于处理包含`while`、`if`、`else`...
语法分析器,也称为解析器,是编译器或解释器的关键组成部分,负责理解程序中的结构和语义。它将输入的源代码分解成一系列的语法单元,通常是以抽象语法树的形式表示。而词法分析器,或称分词器,是解析过程的第一步...