一、加法器功能:
整数、浮点数的加减运算
二、实现形式:
支持赋值语句和表达式计算,遇到不能识别给出提示。生成对应程序的抽象语法树AST(以字符形式表示各节点)和最后结果
说明:
整数为一般语言的整数定义int,以’0’开头的非零整数(如023)为非法;
浮点数,一般意义上即double,带小数点和尾数的;
这里由于支持赋值语句,所以需要规定变量名命名规则,即以字母或下划线开头的字母、下划线、数字组成的字符串;
这里个语句见以回车或换行来区分,没有用‘;’来分隔。
如下为运行时示例:
着色的为输入的赋值语句和表达式计算语句,黑色的为生成的语法树节点和最后的结果
三、具体过程:
1、 编写描述加法语法结构的文法文件,文件名为Expr.g,.g为ANTLR识别的文法文件的后缀。 ANTLR中是使用DSL(domain-specific language)语言来描述文法的,这样一种专门设计来描述其它语言的语言通常叫做元语言(metalanguage),ANTLR支持EBNF(Extended BNF),这种文法是上下文无关文法(context-free grammar),允许可选项和重复的元素。在这个加法器的例子中就是用这种文法来描述的。
其文法Expr.g见附件,其中各部分功能如下:
(1) 开头为文法名称,这个名称需要与文法文件名相同(不带后缀),ANTLR中支持四种类型文法:lexer,parser,combined和tree grammar。在这里grammar Expr;表示是lexer和grammar的混合(combined)文法,里面既有词法文法又有语法文法,一般情况除了是tree grammar都不用给出grammarType。
(2) 接着的是文法的options section部分,这里option可以有language、output、backtrack、ASTLabelType等等。在加法的这个例子中,声明如下:
options {
output=AST;
ASTLabelType=CommonTree; // type of $stat.tree ref etc...
}
由于要生成AST树,所以要指明output为AST,output用于指定生成的数据结构,现在(即ANTLR v3)只能生成AST或template。
而ASTLabelType则是指明生成的树的种类,因为默认情况下,ANTLR会将每个节点生成为Object类型,那样后续工作进行时会有很多的类型转换。而一般可以指定为CommonTree类型从而省去了一些类型转换。
(3) 接下来是一些grammar actions,这些对应actions生成的method可以被打包到生成的class文件中。
其语法形式是:
@action-name {…..}
@action-scope-name{…..}//这个用在combined grammar中
具体如: @members{}、@header{}等
在本例中如下:
//the fllowing two actions set the package for the generated Lexer.java and Parser.java
@lexer::header{
package output;
}
@parser::header{
package output;
}
分别用于配置生成的词法文件和语法文件的环境配置。
(4) 接下来就是描述这个加法器的一些规则:
prog: ( stat {System.out.println(
$stat.tree==null?"null":$stat.tree.toStringTree());} )+ ;
stat: expr NEWLINE -> expr
| ID '=' expr NEWLINE -> ^('=' ID expr)
| NEWLINE ->
;
// END:stat
// START:expr
expr: multExpr (('+'^|'-'^) multExpr)*
;
multExpr
: atom ('*'^ atom)*
;
atom:
INT
| DOUBLE
| ID
| '('! expr ')'!
;
// END:expr
其中有5个rules,分别为:prog、stat、expr、multExpr和atom;
prog作为程序的开始,描述了(stat{…})+;即大于等于一个stat的可识别形式,其中{…}中为嵌入在文法中的以目标语言编写(本例中为Java)的一些操作,本例中为向控制台以StringTree形式输出这些树节点。
stat为实际上的识别语句,有3个可选项(alternative),分别为:
expr NEWLINE
ID ‘=’ expr NEWLINE
NEWLINE
其中NEWLINE和ID为词法识别的token,NEWLINE这里为了方便用来代替‘;’,ID为标识符,用于记录变量;expr为表达式,这本是又是一个rule:
expr: multExpr (('+'^|'-'^) multExpr)*
是识别加减表达式的语法,即为multExpr后面跟着大于等于零个(('+'^|'-'^) multExpr),+multExpr或是-multExpr,用这种有点递归的方式识别出了表达式的语法;multExpr为进一步的一个rule,可以用来识别比加减运算符优先级高的乘法表达式:
: atom(‘*’^atom)*
atom是最为原子的rule,可直接由最简单的token来实现,其中除了INT和DOUBLE外,ID和'('! expr ')'!也作为可选项是因为multExpr也支持内层的嵌套。
最后还要说一点,上面的->,^,!符号是用来构造AST树的,->是将对应的alternative 构造成一个tree node,^用来表示这个节点要被设成树的根节点,!表示只生成这个功能而不生成对应节点。
(5) 最后是词法的token识别,如下:
// START:tokens
ID : ('a'..'z'|'A'..'Z'|'_')('a'..'z'|'A'..'Z'|'_'|'0'..'9')* ;
INT : ('1'..'9')('0'..'9')*|'0' ;
DOUBLE : (('1'..'9')('0'..'9')*|'0') ('.')('0'..'9')+;
NEWLINE:'/r'? '/n' ;
WS : (' '|'/t'|'/n'|'/r')+ {skip();} ;
// END:tokens
ID为标识符,字母或下划线开头,由字母数字下划线组成的字符串;
INT为整形,0或是1..9开头的数字串;
DOUBLE为浮点型,含整数、小数点和尾数部分;
NEWLINE为换行,? 表示可选,即'/r'可选,'/n'一定包含;
WS为空白字符,包括空格、换行、回车等,{..}中为嵌入的目标语言操作,本例中为忽略这个token。
2、 使用ANTLR来生成Lexer.java 、Parser.java和 Expr.tokens,这三个文件默认都生成在output文件夹中,本例中先是new –>Java project,并加入Liabries :antlrworks-1.4.jar(带works图形工具的ANTLR最新版本包)链接库,在src(eclipse中新Java project内都有这个)下new ->file为Expr.g,按照步骤一中完成文法文件的编写,通过ANTLRWorks来生成上述三个文件。
这时如果调用生成类的方法来测试的,只能由输入程序段得到AST,输出的结果为一些树节点,没有最后计算结果,因为并没有对AST进行visit进而加入一些操作来计算出最后结果。
3、 编写tree grammar文法文件,visit AST,加上一些actions,使得一些表达式能返回结果,其文法文件见附件:Eval.G, 各部分功能简介如下:
(1) 开始这部分属于声明部分:
tree grammar Eval;
options {
tokenVocab=Expr;
ASTLabelType=CommonTree;
}
// START:members
@header {
package output;
import java.util.HashMap;
}
@members {
/** Map variable name to Double object holding value */
HashMap memory = new HashMap();
}
这是个tree grammar,用来visit前面已经生成的AST树,tokenVocab=Expr;是指示tree grammar Eval中的ID、token和grammar Expr中的ID、token有相同的含义,从而这个tree grammar才可以正确的访问grammar Expr生成的AST树;@header option是在tree grammar生成的class中声明包和导入一些Java库函数,以便下面调用;@members option是用目标语言(本例是Java)声明或是创建一些全局变量或是对象,本例中是创建一个hash对象用来存放visit过程中的double类型变量信息的。
(2) 这部分是walk tree的主体部分:
// START:stat
prog: stat+ ;
stat: expr
{System.out.println($expr.value);}
| ^('=' ID expr)
{memory.put($ID.text, new Double($expr.value));}
;
// END:stat
// START:expr
expr returns [double value]
: ^('+' a=expr b=expr) {$value = a+b;}
| ^('-' a=expr b=expr) {$value = a-b;}
| ^('*' a=expr b=expr) {$value = a*b;}
| ID
{
Double v = (Double)memory.get($ID.text);
if ( v!=null ) $value = v.doubleValue();
else System.err.println("undefined variable "+$ID.text);
}
| INT
{$value =(double)Integer.parseInt($INT.text);}
| DOUBLE
{$value = Double.parseDouble($DOUBLE.text);}
;
rule prog没有什么antion动作,只是识别一些赋值语句或是表达式;
rule stat做了两件事:
1、 匹配expr的话就输出expr的value。
2、 匹配的是赋值语句的话,则将结果映射到变量中去并保存到hash表中。
rule expr如果匹配的是算式,则标准化为<result=a operator b>形式;如果匹配了ID、INT、DOUBLE则执行对应的actions。
在上一个文法文件Expr.g中有构造AST树的文法,如:
ID '=' expr NEWLINE -> ^('=' ID expr)
->符右边的为构造的AST节点,由于要表示一个二维的树节点不好表示,所以ANTLR中用这种一维的stream形式来表示一个节点,在tree grammar文件中就直接对这些个节点进行操作;就如上面这个被构造的节点,expr如果匹配的是:
multExpr+multExpr
在tree grammar中会有{$value = a+b;}这种操作,并返回double型的结果,其结果保存在之前申请的hash表中,而如上面所示:
| ^('=' ID expr)
{memory.put($ID.text, new Double($expr.value));}
^表示'='将被置成根节点,ID和expr为其子节点,其操作为将ID的text值赋成expr的value值,并将其保存在hash表中。
4、 最后需要一个主程序来调用生成的各个类来实现程序的功能,主程序见附件Test.java。
首先通过接受控制台输入,调用lexer来生成token流,parser类接受tokens可以得到一个result tree,然后创建一个Eval类型的visit来按rule prog来访问这个树,最后得到结果。
PS:修改下,添加下附件Caculator。。。整个工程文件都打包在附件中,感兴趣的话可以测试一下(需要加入antlrworks-1.4.jar外部链接库),附件中工程文件夹Caculator下src中为程序文件,Expr.g、Eval.g为编写的文法文件,output文件夹下 Lexer.java、Parser.java、Eval.java、Expr.tokens、Eval.tokens都是由文法文件通过ANTLRWorks生成的,Test.java含有main函数,是测试主程序。
笑话了,怎么只能上传图片格式!!
我放在下载空间里了,地址:http://download.csdn.net/source/2798964