构造一个语言识别器(翻译)

来源:互联网 发布:济南ic卡充值软件 编辑:程序博客网 时间:2024/06/11 15:45

构造一个语言识别器

 

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来生成一个JavaC++写成的可以用来识别该语言中的语句的定义识别程序。可以加入一些简单的操作符来让ANTLR自动构造中间形式的语法树,稍后可以用于执行某种转换。也可以在文法中嵌入JavaC++的动作(actions)以收集信息或是执行某种转换。

进行简单的转换时,你将构造两个文法:一个lexer文法和一个parser文法,ANTLR可以根据它们生成一个lexer(通常被称作scannertokenizer)和一个parserlexer将输入的字符流变成记号(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到底对这些文法做了什么呢?好的,看看SeriesParserANTLR将会生成下列代码(除了错误处理部分):

 

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;

  }

  }

}

}

 

考虑一下上面的代码,你就会开始发现文法和上面的代码间的一一对应关系,跟手写的代码很相似。

为了使用lexerparser,你需要一个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做些什么操作呢?你可能会构造一个ANTLRcommon 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

 

原创粉丝点击