ANTLR词法语法规则

ANTLR词法语法规则

一、四种抽象的计算机语言模式

  • 序列:是由许多元素组成的一个序列,比如数组初始化句法中的数值。
  • 选项:是指在多个可选的短语中的选择,比如编程语言中的不同语句。
  • 单词约束:一个单词的出现需要对应另一个其它短语的出现,比如左括号和右括号。
  • 嵌套短语:这是一种自相似的语言结构,例如编程语言中的嵌套算术表达式或嵌套语句块。

二、模式:序列

大部分的命令都是由以下部分组成一个序列的:关键字(保留的标识符,比如USER和RETR),一个操作数,然后是一个换行符

在ANTLR中,获取命令的序列表示为‘RETR’ INT ‘\n’,其中,INT代表了整数符号类型。

retr :'RETR' INT '\n';// match keywordinteger newline sequence

类似于我们在编程语言中用函数来标记语句块,我们使用语法规则来标记语言结构。因而,我们将RETR序列标记为retr规则。这样,在语法的其它地方,我们可以通过规则名称来快速引用RETR序列。

考虑下任意长的序列,比如简单的整数列表,像matlab中的向量,诸如[1 23]之类的。对于有限序列,我们希望这些元素一个接着一个出现,但是,我们又不可能像INT INT INT INT INT INT INT INT INT…这样列出所有可能出现的情况。

要表示一个序列中的某个元素需要出现一次或多次,我们可以使用“+”子规则操作符。例如,(INT)+代表一个由整数组成的任意长的序列。为了书写方便,写成INT+也是可以的。如果希望这个序列也可以是空的,也就是某个元素可以出现零次或多次,我们可以使用“*”操作符:INT*。这个操作符类似于编程语言中的循环结构,当然,ANTLR生成编译器的时候就是用循环结构实现的。

// 序列模式有很多中情况,其中包括两个常见的情况,带终结符的序列和带分隔符的序列。
// file规则使用终结符模式来匹配零个或多个“row ‘\n’”序列列表。符号’\n’是每个序列结束的标志。
// row规则使用分隔符模式来匹配由零个或多个’,’隔开field序列列表。符号’,’分隔开了所有的field。row能够识别像1以及1,2以及1,2,3这样的序列。

file : (row '\n')* ;       // sequence with a '\n' terminator
row : field (',' field)*; // sequence with a ',' separator
field: INT ;             // assume fields are just integers

三、模式:选项(替换项)

为了表达语言中选项这个概念,在ANTLR规则中,我们使用“|”操作符来表示“或”这个语法选择概念,我们称这种语法选择概念为选项。语法中到处都可以看到选项。

// 我们可以定义一个更加灵活的field规则,让其可以选择是整数还是字符串
field : INT | STRING ;

四、模式:符号约束

在前面,我们使用INT+来表示matlab向量中的非空的整数序列,比如[1 2 3]。要识别一个方括号括起来的向量,我们还需要表述符号之间的依赖关系。这种依赖关系是指,一旦我们在句子中看到一个符号,我们就必须能在句子的其它地方能够找到其匹配对应的符号。要表示这样的语法,我们可以在序列中同时指定这些对应的符号,通常情况下,这些对应符号会包含或分开一些其它元素。基于这些,我们能够完成向量的识别。

// 表示向量时使用到的中括号约束关系
vector : '[' INT+ ']';// [1], [1 2], [1 2 3], ...

// 表述函数调用和数组下标时候使用到的括号符号约束关系。
expr: expr '(' exprList?')'  // func call like f(), f(x), f(1,2)
   | expr '[' expr ']'      // array index like a[i], a[i][j]
   ...
   ;

注意:并不是所有的依赖性符号都是像括号那样相互对应的。C风格的语言中,有一种像 a?b:c 这样的三目运算符,其中就要求遇到了“?”符号之后必须要有一个“:”和其组成完整的运算符。

五、模式:嵌套短语

嵌套短语有一种自相似的语言结构,即其子短语和其本身有着同样的结构。表达式是一种典型的自相似语言结构,表达式是由运算符分隔开的嵌套子表达式组成的。类似的,while语句块也是一个嵌套在其它外层语句块中的语句块。我们在语法上用递归规则来表示自相似的语言结构。所以,当我们的伪代码在表述一个规则的时候引用到了自己,我们就需要使用递归规则(自引用)。

让我们看看对于代码块语法,嵌套是怎么工作的。一个while语句是由一个关键字“while”,跟一个括号括起来的条件表达式,跟一个语句组成的。当然,我们把用大括号括起来的多条语句看成是一条语句。那么,要表达这样的语法,就应该像这样:

stat: 'while' '(' expr')' stat  // match WHILE statement
   | '{' stat* '}'              // match block of statements in curlies
   ...                          // and other kinds of statements
   ;

while语句的循环体可以是一条语句也可以是用大括号括起来的多条语句。stat规则是一个直接递归的规则,因为它在其第一个选项和第二个选项中都直接引用了自己。如果我们把stat的第二个选项改动一下,让第二个选项单独成为一个规则block,那么规则stat和block之间就形成了非直接递归。

stat: 'while' '(' expr')' stat  // match WHILE statement
   |block                       // match a block of statements
   ...                          // and other kinds of statements
   ;

block: '{' stat* '}';   // match block of statements in curlies

大多数平凡语言都有许多自相似的结构,进而会产生许多递归规则。我们以一个简单的语言例子为例,这个例子中表达式只有三种类型:下标数组,括号表达式,整数。下面是我们如何在ANTLR中表述这种表达式语法:

expr: ID '[' expr ']'   // a[1], a[b[1]]
   | '('expr ')'        // (1), (a[1]), (((1)))
   | INT                // 1, 94117
   ;

下面展示了两个输入样例的语法树:

在这里插入图片描述

语法树的内部节点就是规则引用,而叶子节点就是符号引用。从树的根节点开始到任何节点的路径都表示了那个节点所表示的元素的调用栈(或者叫做ANTLR生成的递归下降分析器的调用栈)。路径代表了递归,对于同样的规则,不同嵌套的子树会有不同的引用。

六、ANTLR核心语法总结

  • x 匹配一个符号,规则或子规则x
  • x y … z 匹配一个规则序列
  • (…|…|…) 带有多个选项的子规则
  • x? 匹配零次或一次x
  • x* 匹配零次或多次x
  • x+ 匹配一次或多次x
  • r:…; 定义规则r
  • r:…|…|…; 定义一个带有多个选项的规则r

6.1 序列

这是由符号和子短语组成的任意长的有限的序列,例如变量声明语法(类型后面加上标识符)以及整数列表等。下面是一些实现这种模式的例子:

x y ... z       // x followed by y, ..., z

'[' INT+ ']'   // Matlab vector of integers

6.2 带终结符的序列

这是由符号和子短语组成的任意长的,可能是空的序列,以一个符号结束,通常情况系这个符号是分号或换行符,例如C风格的编程语言中的语句以及以换行符终结的数据行。下面是一些实现这种模式的例子:

(statement ';')*   // Java statement list

(row '\n')*        // Lines of data

6.3 带分隔符的序列

这是由符号的子短语组成的任意长的非空的序列,用一个特定的符号分隔开,通常这个符号是逗号,分号或句号。例如函数参数列表,函数调用列表,或者是分开却不终止的程序语句。下面是一些实现这种模式的例子:

expr (',' expr)*      // function call arguments

( expr (',' expr)* )?

// optional function call arguments

'/'? name ('/'name)*  // simplified directory name

stat ('.' stat)*      // SmallTalk statement list

6.4 选项

这是由一系列可选择的短语组成的,例如类型说明,语句,表达式或者XML的标签。下面是一些实现这种模式的例子:

type : 'int' | 'float';

stat : ifstat | whilestat | 'return'expr ';' ;

expr : '(' expr ')'| INT | ID ;

tag : '<' Name attribute* '>'| '<' '/' Name '>';

6.5 符号约束

一个符号的出现需要另一个或多个子序列符号的出现来对应,例如小括号,中括号,大括号,尖括号的匹配等。下面是一些实现这种模式的例子:

'(' expr ')'         // nested expression

ID '[' expr ']'         // array index

'{' stat* '}'  // statements grouped in curlies

'<' ID (','ID)* '>' // generic type specifier

6.6 递归短语

这是一种自相似的语言结构,例如表达式结构,Java类嵌套,代码块嵌套以及Python中的函数嵌套定义等。下面是一些实现这种模式的例子:

expr : '(' expr ')'| ID ;

classDef : 'class' ID '{'(classDef|method|field) '}' ;

七、处理优先级,左递归以及相关性

在自顶向下的语法中,利用递归下降分析器来识别表达式一直都是一件很麻烦的事情,首先,因为很多自然语法都是模糊不清的,其次,大部分自然定义都使用一种特殊的递归——左递归。我们会在后面仔细地讨论左递归,但是现在,我们只需要知道在传统模式下,自顶向下的语法分析器是无法处理左递归的。

为了更好地表述这个问题,想象一个简单的数学表达式语言,其只包含乘法和加法操作符,且只有整数作为原子项。表达式是自相似的,所以我们很自然能够想到一个乘法表达式是由乘号(*)连接两个子表达式组成的。类似的,一个加法表达式是由加号(+)连接两个子表达式组成的。我们也可以把一个单独的整数算作一个表达式。从字面上来将这个想法转换成语法规则,那么这个规则看起来应该像下面这样:

expr : expr '*' expr    // match subexpressions joined with '*' operator
    | expr '+' expr     // match subexpressions joined with '+' operator
    | INT               // matches simple integer atom

但问题是,这样的语法定义对于一些特定的输入是具有歧义的。换句话说,这样的规则可能存在对同一条输入的多种理解方式。对于只有一个整数或者只有一个操作符的表达式,例如1+2和1*2这种,这个规则可以正常工作。

在这里插入图片描述

问题在于,当识别输入像1+23这样的表达式时,这条规则会存在2种不同的方式来解析。这两种解析是不一样的,因为中间的语法树的表示先计算23的结果再加上1,而右边的语法树表示先计算1+2的结果再乘以3。这涉及到一个算符优先级的问题,传统的语法将无法识别这种优先级。

ANTLR通过这些算符规则定义的顺序来解决这种歧义,允许我们通过这种方式来隐式地指定算符优先级。expr规则先定义了乘法选项后定义了加法选项,所以ANTLR在处理类似于1+2*3这样的语法歧义的时候会先选择乘法选项

默认情况下,ANTLR是从左到右来结合运算符的,这正好符合*和+的运算规律(从左到右计算)。但是,一些运算符(比如幂运算符)却是需要从右到左结合的,这种情况下,我们就需要用到assoc选项来手动来指定运算符的结合方向。例如,下面是一个幂运算表达式规则的例子,能够正确地将234这样的输入识别成2(34):

expr : expr '^'<assoc=right>expr// ^ operator is right associative
   | INT
   ;

下面的语法分析树展示了“^”运算符的左结合和右结合这两种结合方式的差别。右边的语法树才是我们通常对幂运算的理解方式,但是作为语言设计者而言,可以根据自己的设计指定任意一种结合方式。

在这里插入图片描述

// 将幂运算也加入我们的表达式规则中,我们需要将“^”规则选项放在“*”和“+”的前面,因为它的优先级比乘号和加号要高(例如,1+2^3的结果应该是9)。

expr : expr '^'<assoc=right>expr    // ^ operator is right associative
   | expr '*'expr                   // match subexpressions joined with '*'operator
   | expr '+'expr                   // match subexpressions joined with '+'operator
   | INT                            // matches simple integer atom
   ;

八、识别通用词法结构

鉴于词法规则和语法规则拥有着类似的结构,ANTLR中允许将这两个规则写在同一个语法文件中。但是,毕竟词法分析和语法分析在语言识别中是两个完全不同的阶段,所以我们必须告诉ANTLR哪条规则应该属于哪个阶段。我们通过规则名称的第一个字符来识别这两种规则,第一个字符是大写字母的是词法规则,第一个字符是小写的是语法规则。例如,ID是一个词法规则名称,而expr则是一个语法规则名称。

8.1 匹配标识符

在语法的伪代码中,一个基本的标识符被描述为一个非空的,由大写和小写字母组成的字母序列。我们知道可以使用“(…)+”的方式来表示这样的序列模式。因为组成标识符的元素可以是小写字母也可以是大写字母,所以这里应该有一个选择操作符来描述这种模式。

ID : ('a'..'z'|'A'..'Z')+ ; // match 1-or-more upper or lowercaseletters

区间运算符:’a’… ‘z’,这个短语代表了一个’a’到’z’之间的任意字符。这个区间也就是ASCII码上面的97到122。如果需要用到Unicode来指定字符,我们需要用到’\uXXXX’这种形式,其中的XXXX就是十六进制的Unicode字符编码。

作为字符集的缩写形式,ANTLR也支持一些类似于正则表达式的写法。

ID : [a-zA-Z]+ ; // match 1-or-more upper or lowercase letters

// 但是,像ID这样的规则有时候会和其它的词法规则或者符号引用发生冲突,比如ID会和引用’enum’冲突。

grammar KeywordTest;

enumDef : 'enum' '{' ... '}' ;

...

FOR : 'for' ;

...

ID : [a-zA-Z]+ ; // does NOT match 'enum' or 'for'

规则ID同时也能匹配上一些关键字,比如enum和for,换句话说,同一个输入串可能会被多条规则同时匹配。为了让这个问题更加清楚,我们来考虑下ANTLR是怎样来处理这种混合的词法/语法规则的。首先,ANTLR会将所有的词法规则(包括词汇引用)和语法规则,然后,像’enum’这样的词汇引用会转换成词法规则,并插入到语法规则之后,明确定义的词法规则之前

ANTLR的词法分析器是通过词法规则出现的顺序来解决这种歧义的。也就是说,你的ID规则必须定义在所有的关键字规则后面,就像上面例子中定义在for后面那样。ANTLR会将所有的词汇引用隐含地生成词法规则并将这些规则放在明确定义的词法规则之前,所以直接词汇引用一般都有较高的优先级。在这种情况下,’enum’将自动比ID的优先级要高

// 由于ANTLR自动将所有词法规则都放在语法规则后面,像下面的例子这样重新调整顺序依然能够获得同样的解析结果:

grammar KeywordTestReordered;

FOR : 'for' ;

ID : [a-zA-Z]+ ; // does NOT match 'enum' or 'for'

...

enumDef : 'enum' '{' ... '}' ;

...

8.2 匹配数字

要描述像10这样的整数是一件非常容易的事情,因为它仅仅是数码的序列。

INT : '0'..'9'+ ; // match 1 or more digits

或者

INT : [0-9]+ ; // match 1 or more digits

然而,浮点数的表达方式就显得非常复杂了。但是,我们可以通过忽略指数形式来简化浮点数的表示。浮点数就是一个数码的序列,后面跟上一个小数点,然后跟上一个可选择的小数部分,或者是由小数点开始,后面跟上一连串的数码序列。只有一个小数点这样的表示方法是非法的。所以,我们使用选择模式和序列模式来表示浮点数规则。

FLOAT: DIGIT+ '.' DIGIT*    // match 1. 39. 3.14159 etc...
   | '.' DIGIT+          // match .1 .14159
   ;

fragment DIGIT : [0-9] ;             // match single digit

这里,我们使用了一个协助规则DIGIT,这样我们只需要写一遍[0-9] 就可以了。使用fragment前缀来定义,ANTLR就会知道这条规则仅仅用来组成其它词法规则,而不会单独使用。DIGIT本身并不是我们需要的符号,也就是说,我们在语法分析器中是看不到DIGIT这个符号的。

8.3 匹配字符串

计算机语言中最经常出现的下一个通用结构就是字符串,比如”Hello”。大多数情况下都是使用双引号,但是也有些语言会使用单引号,甚至像Python这样的语言两种符号都使用。不管使用什么符号来分割字符串,我们使用同样的规则来表示字符串内部。在伪代码中,字符串就是在双引号之间由任意字符组成的序列。

STRING : '"' .*? '"' ; // match anything in "..."

其中,通配符“.”匹配任意一个字符。所以,“.*”就能匹配任意长的可以为空的字符串。当然,这种匹配也可以直接匹配到文件末尾,但是这么做往往是没有意义的。所以,ANTLR使用标准正则表达式符号(后缀:?)来表示采用非贪婪子规则的策略。非贪婪的意思是“不断匹配字符,直到匹配上词法规则中子规则的后面跟着的元素”。更精确地说,非贪婪子规则值匹配尽可能少的字符,尽管这条规则有可能匹配更多的字符。相比之下,“.*”就被认为是贪婪子规则,因为它会匹配掉所有循环内部的字符(这种情况下,作为通配符使用)。

我们的STRING规则还是不完整的,因为目前我们的规则中不允许出现双引号。为了支持字符串中出现双引号,许多语言都会定义一种以反斜杠开头的转义序列。要让双引号引起来的字符串中也出现双引号,我们使用“\””。我们需要如下定义来支持转义字符:

STRING: '"' (ESC|.)*?'"' ;

fragment ESC : '\\"' | '\\\\' ; // 2-char sequences \" and \\

ANTLR本身支持转义字符,所以需要对反斜杠进行转义,所以我们可以看到上面使用“\”来代表一个反斜杠。

现在,我们的STRING规则的循环中可以通过片段规则ESC来匹配转义序列,以及通过“.”通配符匹配任意单个字符了。而“*?”子规则操作符会在匹配规则中后跟元素(没有转义的双引号)时结束匹配“(ESC|.)*?”的循环。

8.4 匹配注释和空白字符

当词法分析器匹配了我们定义的符号之后,就会通过符号流将识别到的符号提交给语法分析器。然后,语法分析器就会根据语法结构来检查这个符号流。但是,当词法分析器遇到注释和空白字符时,我们希望词法分析器直接忽略它们,这样的话,语法分析器就不用考虑怎么处理这些到处都可能会出现的注释和空白字符了。例如,WS代表是一个词法规则中的空白字符,像下面这么定义语法规则的话是一件非常可怕的事情:

assign : ID (WS|COMMENT)? '=' (WS|COMMENT)? expr (WS|COMMENT)? ;

定义这些需要丢弃的符号和定义非丢弃的符号是一样的,我们只需要简单地使用skip命令来指明词法分析器需要将这些符号忽略就行了。例如,下面是匹配类C语言中单行注释和多行注释的规则:

LINE_COMMENT  : '//' .*? '\r'? '\n' -> skip ; // Match"//" stuff '\n'

COMMENT       : '/*' .*? '*/' ->skip ;       // Match "/*" stuff "*/"

在LINE_COMMENT规则中,“.*?”匹配“//”后面直到第一个出现的换行符(回车符前面的那个换行符是为了匹配Windows下的换行符)之前的所有字符。在COMMENT规则中,“.*?”匹配了“/*”“*/”之间的所有字符。

词法分析器通过“->”操作符来接收命令,skip只是诸多命令中的一种。例如,我们还可以通过channel命令将这些符号传递给语法分析器的“隐藏通道”。

下面来看看怎样处理我们这一解中的最后一个通用符号,空白字符。一些编程语言把空白字符作为符号的分隔符,而其他语言则是直接忽略空白字符。(Python是一个例外,因为Python将空白字符作为语法结构的一部分了:换行符作为命令的结束符,缩进(Tab或者空格)作为指定嵌套结构的符号。)下面是让ANTLR忽略空白字符的例子:

WS : (''|'\t'|'\r'|'\n')+ -> skip ; // match 1-or-more whitespace but discard

或者

WS : [\t\r\n]+ -> skip ; // match 1-or-more whitespace but discard

当换行符既是需要忽略的空白字符又是作为命令的结束符时,我们就会遇到一个问题。换行符是上下文相关的。在一个语法结构中,我们需要忽略换行符,但是再另外一个语法结构中,我们又需要将换行符传递给语法分析器,这样语法分析器才能知道一条命令是否结束。
例如,在Python中,f()后面跟一个换行符意味着我们需要执行指令,调用f()。但是我们又可以在圆括号中插入一个额外的换行符。Python会等到遇到“)”后面的换行符才会执行函数调用。

def f(): print "hi"

f() // hi

f(
     ) // hi

8.5 符号类别总结

  • 标点符号:对标点符号和运算符最简单的处理就是直接在语法规则中引用它们。
call : ID '(' exprList ')' ;

定义符号的标签规则,例如定义LP来代表左括号。

call : ID LP exprList RP ;

LP : '(' ;

RP : ')' ;
  • 关键字:关键字就是保留的标识符,和标点符号的处理一样,我们可以直接引用也可以定义标签规则。
returnStat : 'return' expr ';'
  • 标识符:标识符在几乎所有语言中看起来都差不多,可以再加一些改动,比如规定首字符以及设定是否可以使用Unicode字符。
ID : ID_LETTER (ID_LETTER | DIGIT)* ; // From C language

fragment ID_LETTER : 'a'..'z'|'A'..'Z'|'_' ;

fragment DIGIT : '0'..'9' ;
  • 数字:例子中是整数和简单浮点数的定义。
INT : DIGIT+ ;

FLOAT
    : DIGIT+ '.' DIGIT*
    | '.' DIGIT+
    ;
  • 字符串:匹配使用双引号引起来的字符串。
STRING : '"' ( ESC | . )*? '"' ;

fragment ESC : '\\' [btnr"\\] ; // \b, \t, \n etc...
  • 注释:识别并丢弃注释。
LINE_COMMENT : '//' .*? '\n' -> skip ;

COMMENT : '/*' .*? '*/' -> skip ;
  • 空白字符:匹配词法中的空白字符并丢弃这些字符。
WS : [ \t\n\r]+ -> skip ;

九 词法分析和语法分析之间的界限

由于ANTLR的词法分析规则也可以使用递归,所以词法分析器几乎和语法分析器一样智能。换句话说,我们能够在词法分析器中实现语法结构。或者,从另一个极端上说,我们可以把字符当作符号,然后利用语法分析器将语法规则运用在字符流上。

  • 应该在词法分析器中匹配那些需要丢弃的元素,这样语法分析器就无需关心这些内容了。比如编程语言中的空白字符和注释就需要识别并剔除。否则的话,语法分析器就不得不花很大的功夫来检查两个符号之间的空白字符或注释了。

  • 应该在词法分析中匹配通用符号,比如标识符,关键字,字符串,以及数字。语法分析器的开销比词法分析器要大,所以我们最好不要给语法分析器太大压力,也就是说,把数码组合在一起并识别出数字再传给语法分析器。

  • 将语法分析器不需要区分的词法结构合并成一个词法结构。举个例子,如果我们的程序处理整数和浮点数的过程是一样的话,那么我们最好就把整数和浮点数合并一个符号:数字。在这种情况下,将分的那么细的词法规则传给语法分析器没有任何意义。

  • 将所有语法分析器可以等同对待的内容合并成一个内容。例如,如果我们的语法分析器并不关心一个XML标签的内容,那么词法分析器就应该将两个尖括号之间的所有内容都合并成一个单独的符号类型:标签。

  • 从另一方面来说,如果语法分析器需要量一个文本分成几部分来处理,那么词法分析器就应该传递已经分好的各元素符号给语法分析器。例如,如果语法分析器需要解析IP地址的各个元素,词法分析器就应该传递IP地址的各个部分(整数和点)。

我们所说的语法分析器不需要区分某些词法结构,或者语法分析器并不关心一个词法结构内部组成,我们实际上是指我们最终的程序不关心这些。即我们的程序对这些词法结构有着同样的处理或者翻译。

为了更好地说明预期应用是怎样影响我们的词法和语法识别的,我们以处理一个Web服务器上的日志文件为例,这种文件每行一条记录。我们将通过不断增加程序的需求来观察词法分析和语法分析之间的界限是怎样变动的。假设这个日志文件每一行由请求IP,HTTP协议命令以及返回代码组成。下面是一行日志记录的例子:

192.168.209.85 "GET /download/foo.html HTTP/1.0" 200

如果我们的程序只需要计算这个文件中一共有多少条记录,除了换行符为终结符的序列外,我们完全可以忽略其它所有词法结构。

file : NL+ ; // parser rulematching newline (NL) sequence

STUFF : ~'\n'+-> skip ; // match and discard anything but a '\n'

NL : '\n' ; // return NL to parser or other invoking code

词法分析器并不需要识别那么多类型的结构,并且语法分析器也只需要识别由换行符结束的序列就可以了。(“~x”操作符匹配除了x外所有符号。)

接下来,我们给程序加一个需要从日志文件中识别IP地址列表的功能。这意味着我们需要一条词法规则来匹配IP地址,同时,我们也就需要一条规则来匹配一条记录剩下来的部分。

IP : INT '.' INT '.' INT '.' INT ; // 192.168.209.85

INT : [0-9]+ ; // match IP octet or HTTP result code

STRING: '"' .*? '"' ; // matches the HTTP protocol command

NL : '\n' ; // match log file record terminator

WS : ' ' ->skip ; // ignore spaces

然后我们通过组合完整的符号组合来让语法分析器识别日志文件中的一条记录。

file : row+ ; // parser rule matching rows of log file

row : IP STRING INT NL ; // match log file record

再添加一点我们程序的功能,我们需要将文本的IP地址转换为32位整数。使用一些很方便的库函数,例如split(‘.’),我们可以将识别到的文本IP地址传递给语法分析器并在语法分析器中完成分离和转换。但是,我们最好在词法分析过程中识别IP地址的各个部分,让后将这些部分作为符号传给语法分析器。

file : row+ ; // parser rule matching rows of log file

row : ip STRING INT NL ; // match log file record

ip : INT '.' INT '.' INT '.' INT ; // match IPs in parser

INT : [0-9]+ ; // match IP octet or HTTP result code

STRING: '"' .*? '"' ; // matches the HTTP protocol command

NL : '\n' ; // match log file record terminator

WS : ' ' ->skip ; // ignore spaces

从我们将词法规则IP改为语法规则ip可以看出,这两者之间的分割线可以很容易发生变动。

继续添加功能。如果程序需要处理HTTP协议命令字符串中的内容的时候,我们将遵循类似的思维过程。如果我们的程序并不关系字符串是由哪些部分组成的,那么我们直接将整个字符串作为一个单独的符号传递给语法分析器就可以了。但是,我们如果需要分离出其中具体的各个部分,最好在我们的词法分析器中就识别这些个部分再传递给语法分析器。

INT '.' INT ; // match IPs in parser

INT : [0-9]+ ; // match IP octet or HTTP result code

STRING: '"' .*? '"' ; // matches the HTTP protocol command

NL : '\n' ; // match log file record terminator

WS : ' ' ->skip ; // ignore spaces

从我们将词法规则IP改为语法规则ip可以看出,这两者之间的分割线可以很容易发生变动。

继续添加功能。如果程序需要处理HTTP协议命令字符串中的内容的时候,我们将遵循类似的思维过程。如果我们的程序并不关系字符串是由哪些部分组成的,那么我们直接将整个字符串作为一个单独的符号传递给语法分析器就可以了。但是,我们如果需要分离出其中具体的各个部分,最好在我们的词法分析器中就识别这些个部分再传递给语法分析器。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值