星期四, 四月 12, 2007

LEX 从易到难

Lex 入门

First!
lex程序的结构是这样的!
定义
%%
规则
%%
用户代码

一个 Lex 程序分为三个段:第一段是 C 和 Lex 的全局声明,第二段包括模式(C 代码),第三段是补充的 C 函数。 这些段以%%来分界。 下面是一个行数与字数的统计工具。

int num_lines = 0, num_chars = 0;
%%
n ++num_lines; ++num_chars;
. ++num_chars;

%%
main()
{
yylex();
printf( "# of lines = %d, # of chars = %dn",
num_lines, num_chars );
}

Second!
对First内容的回顾
C 和 Lex 的全局声明
这一段中我们可以增加 C 变量声明。这里我们将为字数统计程序声明一个整型变量,来保存程序统计出来的字数。我们还将进行 Lex 的标记声明。

字数统计程序的声明
%{
int wordCount = 0;
%}
chars [A-za-z_'."]
numbers ([0-9])+
delim [" "nt]
whitespace {delim}+
words {chars}+
%%

两个百分号标记指出了 Lex 程序中这一段的结束和三段中第二段的开始。

Lex 的模式匹配规则
让我们看一下 Lex 描述我们所要匹配的标记的规则。(我们将使用 C 来定义标记匹配后的动作。)继续看我们的字数统计程序,下面是标记匹配的规则。
字数统计程序中的 Lex 规则
{words} { wordCount++; /*
increase the word count by one*/ }
{whitespace} { /* do
nothing*/ }
{numbers} { /* one may
want to add some processing here*/ }
%%
C 代码
Lex 编程的第三段,也就是最后一段覆盖了 C 的函数声明(有时是主函数)。注意这一段必须包括 yywrap() 函数。 Lex 有一套可供使用的函数和变量。 其中之一就是 yywrap。一般来说,yywrap() 的定义如下例。我们将在 高级 Lex 中探讨这一问题。
字数统计程序的 C 代码段
void main()
{
yylex(); /* start the
analysis*/
printf(" No of words:
%dn", wordCount);
}
int yywrap()
{
return 1;
}

Lex 编程的基本元素就这样搞定了,它将帮助你编写简单的词法分析程序。
Third
高级Lex
Lex 有几个函数和变量提供了不同的信息,可以用来编译实现复杂函数的程序。下表中列出了一些变量和函数,以及它们的使用。 详尽的列表请参考 Lex 手册。
Lex 变量
yyin FILE* 类型。 它指向 lexer 正在解析的当前文件。
yyout FILE* 类型。 它指向记录 lexer 输出的位置。 缺省情况下,yyin 和 yyout 都指向标准输入和输出。
yytext 匹配模式的文本存储在这一变量中(char*)。
yyleng 给出匹配模式的长度。
yylineno 提供当前的行数信息。(lexer不一定支持。)

Lex 函数
yylex() 这一函数开始分析。 它由 Lex 自动生成。
yywrap() 这一函数在文件(或输入)的末尾调用。如果函数的返回值是1,就停止解析。 因此它可以用来解析多个文件。代码可以写在第三段,这就能够解析多个文件。 方法是使用 yyin 文件指针(见上表)指向不同的文件,直到所有的文件都被解析。最后,yywrap() 可以返回 1 来表示解析的结束。
yyless(int n) 这一函数可以用来送回除了前�n? 个字符外的所有读出标记。
yymore() 这一函数告诉 Lexer 将下一个标记附加到当前标记后。
到此为止,可能你看到lex程序还会范晕,没关系,下面我们接着来,分析一个类pascal语法的极简析器!
/* 这个就是注释了*/
/* scanner for a toy Pascal-like language */
申明部分开始
%{ 内的东西会原封不动地出现在输出文件中 }%
%{
/* need this for the call to atof() below */
#include <math.h>
%}
DIGIT [0-9]
ID [a-z][a-z0-9]*
%%
模式部分开始
{DIGIT}+ {
printf( "An integer: %s (%d)n", yytext,
atoi( yytext ) );
}
{DIGIT}+"."{DIGIT}* {
printf( "A float: %s (%g)n", yytext,
atof( yytext ) );
}
if|then|begin|end|procedure|function {
printf( "A keyword: %sn", yytext );
}
{ID} printf( "An identifier: %sn", yytext );
"+"|"-"|"*"|"/" printf( "An operator: %sn", yytext );
"{"[^}n]*"}" /* eat up one-line comments */
[ tn]+ /* eat up whitespace */
. printf( "Unrecognized character: %sn", yytext );
%%
补充部分开始
main( argc, argv )
int argc;
char **argv;
{
++argv, --argc; /* skip over program name */
if ( argc > 0 )
yyin = fopen( argv[0], "r" );
else
yyin = stdin;
yylex();
}
想要真正了解lex, [[正则表达式]] 是关键!
Four
yytext 匹配模式的文本存储变量, 可以通过在申明阶段使用%pointer或%array来控制是一个字符指针还是一个字符数组。指针模式与数组模式各有特点,导致在yytex申明上也不一样,具体请参考lex手册!
在模式阶段中
模式 动作
[ t]+ putchar( ' ' );
[ t]+$ /* ignore this token */
模式部分是正则表达式,动作部分是处理方法,动作部分如果时{开头,那么,动作将会持续到},如果动作中出现了括号{},开始采用 %{ %}来表示动作去区段。动作部分如果时 |,就表示与下一条规则执行相同的动作。
好的,我们来看一个更为实用一点的lex程序。
我们先定义三个动作:
ECHO 将yytext输出
BEGIN 开始一个条件处理块
REJECT 指示简析器对当前规则不做处理,而是采用第二匹配规则。
int word_count = 0;
%%
frob special(); REJECT;
[^ tn]+ ++word_count;
如果frob没有REJECT动作,frob将不会被计数,因为解析器在通常情况下,每个被匹配的对象只会对一个动作生效,多个REJECT也是允许的,会寻找下一个最配的规则来做处理。所以,下面的规则会把输入的"abcd"处理后输出"abcdabcaba".
%%
a |
ab |
abc |
abcd ECHO; REJECT;
.|n /* eat up any unmatched character */

`yymore()' 告诉解析器下一次匹配的规则,满足的部分将会添加到当前yytext值得后面而不是替换它。 例如,指定的输入"mega-kludge"经过下面的程序处理后将会输出"mega-mega-kludge"。
%%
mega- ECHO; yymore();
kludge ECHO;
第一个 "mega-" 被满足并且输出. 然后 "kludge" 满足, 但是并没有替换之前的"mega-"而是"kludge"附加到他的后面,然后输出的其实是"mega-kludge".
yymore()需要两件事情需要注意。第一,yymnore()依赖于表现当前匹配项的长度yyleng的值,所以使用yymore不允许改变yyleng的值。第二,yymore()的使用会使解析器付出一点点性能的代价。
有yymore()就有yyless()
yyless(n) 返回当前匹配项除了开始的n个字符内的所有的内容到输入缓存区,解析器处理下一个匹配时,它们将会被重新解析。yyless将会导致yytext与yyleng的调整。(yyleng将会等于=n) 如输入"foobar"被下面的程序处理后,将会输出"boobarbar". 因为前n=3个字符foo外的字符bar被重新返回到输入缓存区了。
%%
foobar ECHO; yyless(3);
[a-z]+ ECHO;
参数0对于yyless将会导致整个当前匹配将会被重新解析。除非你改变了解析器本来的处理流程(如使用begin),这将会导致循环结束。需要注意的是,yyless是一个宏,并且在flex输入文件中使用,不能在其他源文件中使用。
unput(c) 将字符c放回到输入流中,该字符可以重新被解析。下面的动作将当前的匹配值附上括号后重新进行匹配。
{
int i;
/* Copy yytext because unput() trashes yytext */
char *yycopy = strdup( yytext );
unput( ')' );
for ( i = yyleng - 1; i >= 0; --i )
unput( yycopy[i] );
unput( '(' );
free( yycopy );
}
注意: 由于每次unput()将指定的字符添加到输入源的开头,所以将字符串添加到输入源开头必须从后道前处理。一个比较重要的潜在问题是使用unput()的时候,如果采用了%pointer指针模式保存yytext,unput会破坏yytext的内容,从最右边的字符开始将会破坏左边的一个字符。如果在unput()后要用到yytext,你首先必须复制一份yytext,或者用%array模式来保存yytext. 最后你不能放一个EOF去试图标志输入流的结束。
input 从输入源中读取下一个字符。例如,下面有的例子将会吃掉C语言注释
%%
"/*" {
register int c;
for ( ; ; )
{
while ( (c = input()) != '*' &&
c != EOF )
; /* eat up text of comment */
if ( c == '*' )
{
while ( (c = input()) == '*' )
;
if ( c == '/' )
break; /* found the end */
}
if ( c == EOF )
{
error( "EOF in comment" );
break;
}
}
}
注意: 如果简析器采用用C++编译,input()被yyinput()的替代,因为input()与C++中的流名称input冲突。
YY_FLUSH_BUFFER 刷新解析器内部缓存以便于下一次的匹配工作,首先它会使用YY_INPUT填充缓存区。这是通用yy_flush_buffer()的一个特例,将会在多输入缓存中描述。
yyterminate()可以在动作内部返回描述区域中使用,它将终止解析器并返回0给解析器调用者,表示操作完成。缺省情况下,到达文件结束位置也会被调用,它是一个宏,并且可能重定义。

Lex进阶

模式
模式在第一阶段或第二个阶段使用,也就是在申明或规则阶段中出现,模式定义了匹配的目标,目标被匹配后将会执行动作。
对于模式不想做太多说明,使用正则表达式定义,可以参看 regex 或 pcre.

开始条件
lex提供了根据条件激活规则的机制。在<sc>前缀的规则将会在解析器在"sc"的开始条件下被匹配。
<STRING>[^"]*        { /* eat up the string body ... */
...
}
将会在启动条件"STRING"的情况下被激活。
<INITIAL,STRING,QUOTE>.        { /* handle an escape ... */
...
}
将会在 "INITIAL", "STRING", "QUOTE"三者之一的条件下被激活。

开始条件在输入源的定义(第一)部分被申明,在‘%s' 或 ’%x'后跟随着名字列表。 %s申明了包含的开始条件,%x申明了排他的开始条件。开始条件被BEGIN动作激活。直到下一个BEGIN动作,满足开始条件名称的规则将会被规则,不满足启动条件的规则将不会被执行。

如果是包含条件,没有开始条件的规则也会被激活执行,如果时排他条件,只有满足开始条件的规则才会被执行。
具有相同排他条件的规则的集合可以使解析器独立于其他的规则。 因此,排他条件可以容易地创建微型解析器处理输入源中的独立与其他部分的一部分(如,注释)。如果对于包含与排他条件还有混淆,可以看下面的例子。
%s example
%%

<example>foo do_something();

bar something_else();

等同于

%x example
%%

<example>foo do_something();

<INITIAL,example>bar something_else();
上面的程序中如果没有<INITIAL,example>,在example条件下bar规则将永远不会被激活。如果使用<example>,将会导致只能在exmaple开始条件下激活,而INITIAL条件下不会被激活。而第一个程序中在任何条件下bar都被会激活。因为第一个程序用example时%s,时包含条件。页可以通过特殊开始条件<*>来配置任何开始条件,上面的程序还可以写为:
%x example
%%

<example>foo do_something();

<*>bar something_else();
缺省规则(显示任何未被匹配的字符)在开始条件下仍然生效。等同于:
<*>.|\n     ECHO;
‘BEGIN(0)’在无开始条件的规则激活条件下返回原始状态,这个状态同于开始条件下的'INITIAL',所以‘BEGIN(INITIAL)'等同于’BEGIN(0)'。
BEGIN行为在规则部分的开头是默认的代码(BEGIN actions can also be given as indented code at the beginning of the rules section.请翻译) 例如,下面的代码将会仅需SPECIAL开始条件,不管合适yylex()被调用并且全局变量enter_special是true。
        int enter_special;

%x SPECIAL
%%
if ( enter_special )
BEGIN(SPECIAL);

<SPECIAL>blahblahblah
...more rules follow...
为了说明开始条件,我们用两种方法处理"123.456".缺省将会被解析为 '123','.','456'三个标记,如果expect-floats后面将会被解析为浮点数 123.456
%{
#include <math.h>
%}
%s expect

%%
expect-floats BEGIN(expect);

<expect>[0-9]+"."[0-9]+ {
printf( "found a float, = %fn",
atof( yytext ) );
}
<expect>n {
/* that's the end of the line, so
* we need another "expect-number"
* before we'll recognize any more
* numbers
*/
BEGIN(INITIAL);
}

[0-9]+ {

printf( "found an integer, = %dn",
atoi( yytext ) );
}

"." printf( "found a dotn" );
下面的代码能够是被C语言注释并且统计行数。
%x comment
%%
int line_num = 1;

"/*" BEGIN(comment);

<comment>[^*n]* /* eat anything that's not a '*' */
<comment>"*"+[^*/n]* /* eat up '*'s not followed by '/'s */
<comment>n ++line_num;
<comment>"*"+"/" BEGIN(INITIAL);
实际上,编写高速解析程序的办法时在每个规则中做尽可能多的匹配。

This scanner goes to a bit of trouble to match as much text as possible with each rule. In general, when attempting to write a high-speed scanner try to match as much possible in each rule, as it's a big win.

注意: 开始条件的名字实际上时一个整形值并且能够被保存,所以,上面的代码可以扩展为:
%x comment foo
%%
int line_num = 1;
int comment_caller;

"/*" {
comment_caller = INITIAL;
BEGIN(comment);
}

...

<foo>"/*" {
comment_caller = foo;
BEGIN(comment);
}

<comment>[^*n]* /* eat anything that's not a '*' */
<comment>"*"+[^*/n]* /* eat up '*'s not followed by '/'s */
<comment>n ++line_num;
<comment>"*"+"/" BEGIN(comment_caller);
而且,可能易使用YY_START宏来访问当前的开始条件。如上面的赋值条件可以改写为
comment_caller = YY_START
YYSTATE是YY_START的别名(因为AT&T lex使用了YYSTATE)。
注意 开始条件没有他们的名字空间; %s 与 %x 申明与 #define形式一样。

到这里,时一个使用排他开始条件如何匹配C风格的引用字符串的处理。包含的扩展的转义,但不包括检查,因为代码太长。
%x str

%%
char string_buf[MAX_STR_CONST];
char *string_buf_ptr;

" string_buf_ptr = string_buf; BEGIN(str);

<str>" { /* saw closing quote - all done */
BEGIN(INITIAL);
*string_buf_ptr = '0';
/* return string constant token type and
* value to parser
*/
}

<str>n {
/* error - unterminated string constant */
/* generate error message */
}

<str>\[0-7]{1,3} {
/* octal escape sequence */
int result;

(void) sscanf( yytext + 1, "%o", &result );

if ( result > 0xff )
/* error, constant is out-of-bounds */

*string_buf_ptr++ = result;
}

<str>\[0-9]+ {
/* generate error - bad escape sequence; something
* like '48' or '0777777'
*/
}

<str>\n *string_buf_ptr++ = 'n';
<str>\t *string_buf_ptr++ = 't';
<str>\r *string_buf_ptr++ = 'r';
<str>\b *string_buf_ptr++ = 'b';
<str>\f *string_buf_ptr++ = 'f';

<str>\(.|n) *string_buf_ptr++ = yytext[1];

<str>[^\n"]+ {
char *yptr = yytext;

while ( *yptr )
*string_buf_ptr++ = *yptr++;
}
通常,如上面的例子中所看到你,会有许多相同开始条件的处理。开始条件范围可以简化重复操作。

<SCs>{
}

SCs 是一个或开始条件的列表。在这个开始条件范围内,每个规则将会自动具有前缀 `<SCs>' 直到 `}' 与开始的 `{' 匹配. 例如

<ESC>{
"\n" return 'n';
"\r" return 'r';
"\f" return 'f';
"\0" return '0';
}

等价于

<ESC>"\n"  return 'n';
<ESC>"\r" return 'r';
<ESC>"\f" return 'f';
<ESC>"\0" return '0';

开始条件页可以嵌套,下面时三个管理开始条件堆栈的参数。

`void yy_push_state(int new_state)'
将当前的开始条件压栈,切换到 new_state 与使用 `BEGIN new_state'类似。
`void yy_pop_state()'
从栈顶弹出,类似于 BEGIN.
`int yy_top_state()'
返回栈顶值,不改变栈内容。

开始条件栈动态增长,没有固定限制,如果内容用尽,程序竟会终止。

为了使用开始条件栈,需要使用 `%option stack' 指令。



多输入缓存区


一些允许include文件解析器的解析器要求从几个输入流中读取内容。YY_INPUT只在结束缓存时被调用,碰到 include 后需要切换输入源,而解析一个描述也许需要很长时间。为了解决此类问题,解析器提供了创建并在多个输入缓存中创建的机制。输入缓存可以通过下面的方式创建:

YY_BUFFER_STATE yy_create_buffer( FILE *file, int size )

参数为与缓存关联的输入文件指针,以及足够的可维持size字符(如果不确定,size可以使用YY_BUF_SIZE)。返回一个YY_BUFFER_STATE,可以传递到其他的处理过程。YY_BUFFER_STATE是一个不可见结构yy_buffer_state的指针,所以可以安全地使用`((YY_BUFFER_STATE) 0)'来初始化YY_BUFFER_STATE,如果你愿意,你可以在解析器之外的源程序中引用这个不透明结构来正确的申明输入缓存。可以通过下面的参数来选择一个缓存区。

void yy_switch_to_buffer( YY_BUFFER_STATE new_buffer )

切换解析器的输入缓存将会导致记接下来的匹配项来自于新的缓存中。yy_switch_to_buffer可能出现在yywrap中为继续解析做准备,替换打开一个新的文件并执行yyin. 通过yy_switch_to_buffer 或 yywrap切换输入源不改变开始条件。


void yy_delete_buffer( YY_BUFFER_STATE buffer )

用于收回与缓存关联的空间。你可以使用下面的函数清空当前内容:
void yy_flush_buffer( YY_BUFFER_STATE buffer )

此函数废弃缓存内容,下一个解析器试图匹配一个内容时将会使用YY_INPUT来更新缓存区。

`yy_new_buffer()' 是 `yy_create_buffer()' 的一个别名,用于提供C++使用new 与 delete操作创建与销毁动态对象的兼容性。

最后, YY_CURRENT_BUFFER 宏返回 YY_BUFFER_STATE 指针,表示当前的缓存。

这里是一个扩展include使用的一个解析器 (`<<EOF>>' 特性将会在以后讨论):

/* "incl" 状态用于获取include的文件名 */
%x incl

%{
#define MAX_INCLUDE_DEPTH 10
YY_BUFFER_STATE include_stack[MAX_INCLUDE_DEPTH];
int include_stack_ptr = 0;
%}

%%
include BEGIN(incl);

[a-z]+ ECHO;
[^a-zn]*n? ECHO;

<incl>[ t]* /* eat the whitespace */
<incl>[^ tn]+ { /* got the include file name */
if ( include_stack_ptr >= MAX_INCLUDE_DEPTH )
{
fprintf( stderr, "Includes nested too deeply" );
exit( 1 );
}

include_stack[include_stack_ptr++] =
YY_CURRENT_BUFFER;

yyin = fopen( yytext, "r" );

if ( ! yyin )
error( ... );

yy_switch_to_buffer(
yy_create_buffer( yyin, YY_BUF_SIZE ) );

BEGIN(INITIAL);
}

<<EOF>> {
if ( --include_stack_ptr < 0 )
{
yyterminate();
}

else
{
yy_delete_buffer( YY_CURRENT_BUFFER );
yy_switch_to_buffer(
include_stack[include_stack_ptr] );
}
}

提供三个过程来实现内存字符串而不是文件输入缓存的解析。它们都要创建一个输入缓存来解析字符串,并且返回YY_BUFFER_STATE (可以在完成解析后用 `yy_delete_buffer()' 删除).,也可以通过`yy_switch_to_buffer()'来切换, 下一次调用`yylex()' 将会解析字符串。
`yy_scan_string(const char *str)' 解析0结尾字符串。
`yy_scan_bytes(const char *bytes, int len)' 解析bytes开始的len个字符(可能包含 0 字符)

注意,上面的两个函数会创建字符串或字节串的副本。(这也许时期望的,因为`yylex()' 会修改被解析缓存的内容) 可以使用下面的方式来拒绝使用副本:
`yy_scan_buffer(char *base, yy_size_t size)'
将会从base开始解析,包含size个字节, 最后的两个字节必须是 YY_END_OF_BUFFER_CHAR (ASCII NUL)。他们不会被解析, 解析范围从 `base[0]' 到 `base[size-2]'(包含)。如果你没能按照这种规定使用base(如,忘记了最后的两个YY_END_OF_BUFFER_CHAR字节), `yy_scan_buffer()' 将会返回空指针而不创建YY_BUFFER_STATE。yy_size_t类型是个整型,可以转化为整数来反映buffer的长度。


文件结束规则

特殊规则 "<<EOF>>" 只是规则在文件结束位置发生且yywrap()返回非0值。(如,没有更多的文件要处理). 这个动作必须完成下面四件事情之一:
赋值给yyin一个新的文件 (早期版本的flex, 此操作后必须调用特殊动作 YY_NEW_FILE; 这个操作已经不需要了);
执行一个返回申明;
执行一个特殊的`yyterminate()' 动作;
或者使用`yy_switch_to_buffer()' 切换到一个新的输入缓存区.

<<EOF>> 不能与其他模式一起使用;它也许仅在开始条件列表申明。如果指定了不合法 <<EOF>> 规则, 它将会应用到所有的开始条件而不仅是 <<EOF>> 动作. 指定 <<EOF>> 规则仅在 initial 开始条件下匹配,就是用:
<INITIAL><<EOF>>

下面的规则可以发现象不关闭的注释类的问题。
%x quote
%%

...other rules for dealing with quotes...

<quote><<EOF>> {
error( "unterminated quote" );
yyterminate();
}
<<EOF>> {
if ( *++filelist )
yyin = fopen( *filelist, "r" );
else
yyterminate();
}


没有评论: