构造一个语言识别器
ANTLR: ANother Tool for Language Recognition (http://www.antlr.org)
原文出处:http://www.jguru.com/faq/view.jsp?EID=78
中英文单词对照:
grammar: 文法
syntax: 语法
action: 动作
lexer: 记号识别器
parser: 分析器
token: 记号
AST: 抽象语法树
lexer grammar: 记号识别器文法
parser grammar: 分析器文法
tree grammar: 分析树文法
tree parser: 分析树分析器
walk: 遍历
为了构造一个语言识别器,可以用文法(grammar)来说明那种语言的结构,然后用ANTLR来生成一个Java或C++写成的可以用来识别该语言中的语句的定义识别程序。可以加入一些简单的操作符来让ANTLR自动构造中间形式的语法树,稍后可以用于执行某种转换。也可以在文法中嵌入Java或C++的动作(actions)以收集信息或是执行某种转换。
进行简单的转换时,你将构造两个文法:一个lexer文法和一个parser文法,ANTLR可以根据它们生成一个lexer(通常被称作scanner或tokenizer)和一个parser。lexer将输入的字符流变成记号(tokens)流,而parser将在记号流上应用文法结构(语法)(grammatical structure (syntax))。这里有个简单的lexer可以用来匹配逗号(commas)、整数(integers)和标识(identifiers):
class IntAndIDLexer extends Lexer;
INT : ('0'..'9')+ ;
ID : ('a'..'z')+ ;
COMMA: ',' ;
通过反复地向lexer请求一个记号(token),parser将会看到一个记号流(a stream of tokens)。不仅如此,parser还会验证这个记号流是否有正确的句法结构。如果你的文法结构的定义是“一个由整数和标识组成,并且由逗号分隔的系列”,那么你可能需要一个看起来象这样的文法:
class SeriesParser extends Parser;
/** Match an element (INT or ID) with possibly a
* bunch of ", element" pairs to follow matching
* input that looks like 32,a,size,28923,i
*/
series : element (COMMA element)* ;
/** Match either an INT or ID */
element: INT | ID ;
你可能会想要在文法里嵌入动作(actions),这样,当parser看到了特定的输入结构时,这些代码片就会被执行。如果你想要打印一共发现了多少个元素,你可以象下面这样添加动作:
class SeriesParser extends Parser;
// I'm using Java...
/** Match an element (INT or ID) with possibly a
* bunch of ", element" pairs to follow matching
* input that looks like 32,a,size,28923,i
*/
series
{ /* this is considered an initialization action
* and is done before recognition of this rule
* begins. These look like local variables to
* the resulting method SeriesParser.series()
*/
int n = 1; // how many elements? At least 1
}
: element (COMMA element {n++;})*
{System.out.println("there were "+n+" elements");}
;
/** Match either an INT or ID */
element: INT | ID ;
那么ANTLR到底对这些文法做了什么呢?好的,看看SeriesParser,ANTLR将会生成下列代码(除了错误处理部分):
public class T extends antlr.LLkParser implements TTokenTypes {
// I cut out the usual set of constructors and a few
// other details.
/** Match an element (INT or ID) with possibly a
* bunch of ", element" pairs to follow matching
* input that looks like 32,a,size,28923,i
*/
public final void series() {
element(); // match an element
_loop3:
do {
if ((LA(1)==COMMA)) {
match(COMMA);
element();
}
else {
break _loop3;
}
} while (true);
}
/** Match either an INT or ID */
public final void element() {
switch ( LA(1)) {
case INT:
{
match(INT);
break;
}
case ID:
{
match(ID);
break;
}
}
}
}
考虑一下上面的代码,你就会开始发现文法和上面的代码间的一一对应关系,跟手写的代码很相似。
为了使用lexer和parser,你需要一个main()方法来创建它们的实例,把它们联系起来并且调用parser里的规则:
main(String[] args) {
DataInputStream input = new DataInputStream(System.in);
// attach lexer to the input stream
IntAndIDexer lexer = new IntAndIDexer(input);
// Create parser attached to lexer
SeriesParser parser = new SeriesParser(lexer);
// start up the parser by calling the rule
// at which you want to begin parsing.
parser.series();
}
为了打印parser匹配到的记号的文本值,必须对相应项目进行标示。这个标示将会指向由lexer构造的Token对象。在一个动作里,可以获得记号对象的文本值:
class SeriesParser extends Parser;
series : element (COMMA element)* ;
element
: a:INT {System.out.println(a.getText());}
| b:ID {System.out.println(b.getText());}
;
输入 32,a,size,28923,i, 将会得到以下输出:
32
a
size
28923
i
更加复杂的转换一般需要对输入进行多次遍历,所以程序员一般会构造一棵叫抽象语法树(AST)的中间形式语法树,它是输入文本的机构化表现。你可以用手写代码来遍历语法树,也可以用ANTLR语法树文法来描述语法树的结构。嵌入在语法树文法中的动作将会在tree parser解析到输入语法树的相关位置时被执行。
怎样才能构造一棵简单语法树呢?很简单!告诉ANTLR去构造语法树它就会这么做,每个输入记号占一个节点;也就是说,ANTLR会构造一个由输入记号组成的链表。为了让事情更有趣,在COMMA记号后面加上一个‘!’来指出逗号并不需要被包括在输入语法树中:
class SeriesParser extends Parser;
options {
buildAST = true;
}
series : element (COMMA! element)* ;
element: INT | ID ;
我们可以对AST做些什么操作呢?你可能会构造一个ANTLR的common AST的子类并且加入一个walk()方法或别的什么,不过更好的办法是用另一个文法来描述AST的结构。语法树文法(tree grammar)就像是可执行的对你的中间形式进行描述的注释。这里有一个很小的文法,它可以匹配由我们的parser grammar生成的语法树:
class SeriesTreeParser extends TreeParser;
/** Match a flat tree (a list) of one or more INTs or IDs.
* This rule differs from SeriesParser.series(), which
* is in a different grammar.
*/
series : ( INT | ID )+ ;
注意!语法树文法(tree grammar)要比分析器文法(parser grammer)简单。一般来说,你会构造相对简单的语法树以便于遍历,而不是面对包含了所有空白和其它语法修饰的输入文本,对人类而言,前者更容易接受。为了调用你的tree parser,需要增大main()方法:
main(String[] args) {
DataInputStream input = new DataInputStream(System.in);
// attach lexer to the input stream
IntAndIDexer lexer = new IntAndIDexer(input);
// Create parser attached to lexer
SeriesParser parser = new SeriesParser(lexer);
// start up the parser by calling the rule
// at which you want to begin parsing.
parser.series();
// Get the tree out of the parser
AST resultTree = parser.getAST();
// Make an instance of the tree parser
SeriesTreeParser treeParser = new SeriesTreeParser();
// Begin tree parser at only rule
treeParser.series(resultTree);
}
你可以往tree grammar里加入动作,就像往parser grammar里加动作一样简单。为了象在parser grammar里那样打印整数和标识的列表,需要加入几个动作:
class SeriesTreeParser extends TreeParser;
series
: ( a:INT {System.out.println(a.getText());}
| b:ID {System.out.println(b.getText());}
)+
;
如果你还想再遍历AST一遍,用别的动作吗?一个办法是定义另一个一样的规则,但是对应不同的动作:
class SeriesTreeParser extends TreeParser;
series
: ( a:INT {System.out.println(a.getText());}
| b:ID {System.out.println(b.getText());}
)+
;
/** Sum up all the integers for fun. */
passTwo
{
int sum = 0;
}
: ( a:INT {sum += Integer.parseInt(a.getText());}
| ID
)+
{System.out.println("sum is "+sum);}
;
于是,你的main()方法需要在调用series规则后调用这个新规则:
main(String[] args) {
...
// Get the tree out of the parser
AST resultTree = parser.getAST();
// Make an instance of the tree parser
SeriesTreeParser treeParser = new SeriesTreeParser();
treeParser.series(resultTree); // walk AST once
treeParser.passTwo(resultTree); // walk AST again!!
}
你将会看到下面的输出:
32
a
size
28923
i
sum is 28955