1、LEX 介绍LEX(Lexical Analyzer Generator)即词法分析器生成工具是 1972 年贝尔实验室的M.E.Lesk 和 E.Schmidt 在 UNIX 操作系统上首次开发的。GNU 同时推出了和 LEX 完全兼容的 FLEX( Fast Lexical Analyzer Genrator)。下面用到的例子都是基于 flex 的。LEX 工作原理:LEX 通过对源文件的扫描,经过宏替换将规则部分的正则表达式转换成与之等价的DFA,并产生 DFA 的状态转换矩阵(稀疏矩阵);利用该矩阵和源文件的 C 代码产生一个名为 int yylex()的词法分析函数,将 yylex(
2、)函数拷贝到输出文件 lex.yy.c 中。函数yylex()以在缺省条件下的标准输入(stdin )作为词法分析的输入文件。输入文件(扩展名为.l)LEX输出文件 lex.yy.cLex 源文件格式为:定义部分%规则部分%用户附加的 C 语言代码例 1:int num_chars=0,num_lines=0;/*定义两个全局变量,一个及字符数,一个记行数 .注意:该语句不能顶行 */%n +num_chars+; +num_lines;. +num_chars;%int main()yylex();printf(“%d,%d”, num_chars,num_lines);int yywrap
3、()/*文件结束处理函数,当 yylex()读到文件结束标记 EOF 时,调用该函数时会,用户必须提供该函数,否则会提示编译出错 */return 1;/返回 1 表示文件扫描结束,不必再扫描别的文件lex 的输入文件分成三个段,段间用% 来分隔。由于必须存在一个规则段,第一个% 总是要求存在模式LEX 的模式是机器可读的正则表达式。表意字符 匹配字符. 除换行外的所有字符n 换行* 0 次或无限次重复前面的表达式+ 1 次或更多次重复前面的表达式? 0 次或 1 次出现前面的表达式 行的开始$ 行的结尾a|b a 或者 b(ab)+ 1 次或玩多次重复 ab“a+b“ 字符串 a+b 本身(
4、 C 中的特殊字符仍然有效) 字符类ab 除 a,b 外的任意字符ab a, , b 中的一个az 从 a 到 z 中的任意字符a-z a,-,z 中的一个a-z a, z 中的一个匹配文件结束标记表 1 :简单模式匹配在方括号( ) 中,通常的操作失去了本来含意。方括号中只保留两个操作,连字号( “ ”)和抑扬号(“ ” ) 。当把连字号用于两个字符中间时,表示字符的范围。当把抑扬号用在开始位置时,表示对后面的表达式取反。如果两个范式匹配相同的字符串,就会使用匹配长度最长的范式。如果两者匹配的长度相同,就会选用第一个列出的范式。定义部分定义部分由 c 语言的代码、模式的宏定义和条件模式的开始
5、条件说明等部分组成。其中 C代码由顶行的“%” 和“%”引入,LEX 扫描源文件时该部分将首先被拷贝到输出文件之中(去掉) “%”和“%” ,在此可以定义必要的全局变量和包含模式处理时用到的外部函数的头文件,如:%#includeint num_chars=0,num_lines=0;%此外,在定义中出现的任何非顶行文字也将直接拷贝到输出文件中,可以利用这一规定了来定义 C 的全局变量。还有定义部分开可以加上 C 语言的注释/*/,该部分也将直接拷贝到输出文件中。规则部分规则部分是 LEX 源文件的核心,它包括一组模式和生成分析器识别相应模式进行处理的C 语言动作。格式如下:C 语言代码模式
6、1 动作 1模式 2 动作 2模式 3 动作 3。 。 。 。模式 n 动作 n同定义部分一样,C 语言代码也不能顶行书写,或则是用顶行的“%”和“%”所引用。这里可以定义输出的词法分析函数 yylex()的局部变量。该部分一定出现在第一个模式之前。而每一个模式必须顶行书写,而模式对应的 C 语句必须和模式在一行。它们之间用白字符分割,若对应模式的 C 语句有多行,则可以用“”将这些 C 语句括起来。用户代码部分LEX 对用户代码部分不做任何处理,仅仅将该部分拷贝到输出文件 lex.yy.c 的尾部。在此部分可定义对模式进行处理的 C 语言函数、主函数和 yylex()要调用的 yywrap(
7、)等。若用户在其它用户模块中提供了这些函数的定义,此部分可省略,则后两部分的%可省略了。yylex()函数的匹配原则yylex()的函数原型为 int yylex(void)。它被调用后,首先它检查全局文件指针变量yyin 是否有定义,如果是则设置将要扫描的文件为用户所定义的文件,否则设为标准输入文件 stdin。接着利用 yylex()所定义的 DFA 分析被扫描的文件,如果有唯一的模式与被扫描的字符串匹配,则执行该模式的动作;如果有多个模式可以匹配相同的输入串,则yylex()选择匹配最长输入串的模式;若多个模式串匹配相等的输入串,则选择最前的模式进行匹配。例如:%program prin
8、tf(“%sn”,yytext);/*模式 1*/procedure printf(“%sn”,yytext);/*模式 2*/a-za-z,0-9* printf(“%sn”,yytext);/*模式 3*/当输入串为“programming”时,模式 1(匹配“program”)和模式 3(“programming” )都匹配,但会选择匹配串长的模式 3。当输入串为“program”时,因为模式 1 和模式 3 匹配的串长度相等故会选择模式 1.试验:在 Linux 操作系统(ubantu7.10 版本)下装有 flex 和 GCC 编译器后:1. 把例 1 的内容保存到 lextest.
9、l 中;2. 运行 flex lextest.l; 这时生成了一个 lex.yy.c 文件;3. 编译这个 lex.yy.c ,用 gcc lex.yy.c o lexyy4. lexyy=* A-Z 匹配单个字符,这个字符必须是方括号中给定字符类以外的字符。在方括号内开始处的特殊符号()表示否定。当字符不在字符类的开始处时,并不具有特殊含义,而是一个普通字符。* A-Zn 匹配单个字符,这个字符不可以是方括号中给出的字符类中的字符。与上一方式的不同在于,这里多了一个换行符,也就是说所匹配的字符不能是 26 个大写字母,也不能是换行符。根据上面的描述,在表达字符分类时,除了直接用字符以及字符范
10、围来表达外,还有一种叫做字符类表达式的,也有同样的作用,常见的一些表达式如下::alnum: :alpha: :blank: :cntrl: :digit: :graph:lower: :print: :punct: :space: :upper: :xdigit:每一个表达式都指示了一个字符分类,而且其名称与标准 C 函数 isXXXX 的名字对应。例如,:alnum:就指示了那些经由函数 isalnum()检查后返回 true 的字符,也就是任何的字母或者数字。注意,有些系统上没有给出 C 函数 isblank()的定义,所以 flex 自己定义了:blank:为一个空格或者一个 tab。
11、下面所举的几个例子,都是等价的::alnum:alpha:digit:alpha:0-9a-zA-Z0-9应该注意字符类表达式的写法。一个字符类表达式是由一对:和: 包住的,作为一个整体,在书写时不可与外层的混淆。(2)重复模式的匹配* r* r 是一个正则表达式,特殊字符 *表示 0 个或多个。因此这个模式表示匹配 0 个或多个 r。* r+ r 是一个正则表达式,特殊字符+表示 1 个或多个。因此这个模式表示匹配 1 个或多个 r。* r? r 是一个正则表达式,特殊字符?表示 0 个或 1 个。因此这个模式表示匹配 0 个或1 个 r。 (从另一个角度看,就是说模式 r 是可选的)* r
12、2,5 r 是一个正则表达式,2,5表示 2 个到 5 个。因此这个模式表示匹配 2 个到5 个 r。也就是说可以匹配rr,rrr ,rrrr,rrrrr四种重复的模式。* r2, r 是一个正则表达式, 2,省略了第二个数字,表示至少 2 个,不设上限。因此这个模式表示匹配 2 个及以上个 r。也就是说至少可以匹配rr,还可以匹配rrr,rrrr 等无限多种重复的模式。* r4 r 是一个正则表达式,4只有一个数字,表示 4 个。因此这个模式确切地匹配4 个 r,即rrrr。(3)名字替换* name 这里 name 就是在前面的定义段给出的名字。这个模式将用这个名字的定义来匹配。(4)平凡
13、(plain)文本串的匹配* “xyzfoo” 这个模式用来确切地匹配文本串:xyzfoo。注意最外层的单引号所包含的是整个模式表达式,也就是说,当希望匹配字串xyzfoo 时,在书写规则时该字串必须用双引号括住。(5)特殊单字符的匹配* x 当 x 是一个a ,b,f,n,r,t 或v时,它就解释为 ANSI-C 中的x。否则就仍然作为一个普通字符 x(一般用于诸如*字符的转义字符) 。* 0 匹配一个 NUL 字符(ASCII 码值为 0) 。* 123 匹配一个字符,其值用八进制表示为 123。* x2a 匹配一个字符,其值用十六进制表示为 2a。(6)组合模式的匹配* (r) 匹配规则
14、表达式 r,圆括号可以提高其优先级。* rs 匹配规则表达式 r,其后紧跟着表达式 s。这称为联接 (concatenation)。* r|s 或者匹配规则表达式 r,或者匹配表达式 s。* r/s 匹配模式 r,但是要求其后紧跟着模式 s。当需要判断本次匹配是否为 “最长匹配(longest match)时,模式 s 匹配的文本也会被包括进来,但完成判断后开始执行对应的动作(action)之前,这些与模式 s 相配的文本会被返还给输入。所以动作( action)只能看到模式 r 匹配到的文本。这种模式类型叫做尾部上下文( trailing context) 。 (有些r/s组合是 flex
15、不能识别的;请参看后面 deficiencies/bugs 一节中的 dangerous trailing context 的内容。 )* r 匹配模式 r,但是这个模式只出现在一行的开始处。也就是说,刚开始扫描时遇到的,或者说在刚扫描完一个换行字符后紧接着遇到的。* r$ 匹配模式 r,但是这个模式只在一行的尾部。也就是说,该模式就出现在换行之前。这个模式等价于 r/n。注意,flex 中的换行(newline)的概念,就是 C 编译器中所使用的n, flex 也采用同样的符号和解释。在 DOS 系统中,可能必须由你自己滤除输入中的r,或者明确地在模式中写成 r/rn 来代替 r$。 (在
16、unix 系统中换行是用一个字节 n 表示的,而 DOS/Windows 则采用两个字节 rn 来表示换行。 )(7)有启动条件(Start Condition)的模式匹配* r 匹配模式 r,但需要启动条件 s(后面后关于启动条件的讨论) 。模式r是类似的,匹配模式 r,只要有三个启动条件 s1,s2,s3 中的任一个即可。(启动条件简单来说,类似于 C 语言中的条件编译,满足了某个条件才启动这个模式参与匹配,否则不会启动该模式参与匹配。 )* r 匹配模式 r,在任何启动条件下都参与匹配,即使是排斥性的条件。上述还需要从实践中体会其含义(8)文件尾匹配* 匹配文件尾,即遇到了文件尾部。一般
17、说来,都应该在模式中加入文件尾模式。这样可以有机会在文件扫描完成时增加一些额外的处理。* 在有启动条件 s1 或者 s2 的情况下,匹配文件尾部。一些常见规则的编写(待续)(1)双引号字符串。“(SAFECHAR|RESTCHAR|_)*“这里需要注意的地方是中间的重复模式的写法:(r)*。r 可以是一个组合模式。中间的两个名称 SAFECHAR 和 RESTCHAR 是在定义段给出的两个字符类。此处应在实用中不断添加=创建一个简单的扫描器下列例子来自于 Flex 的手册。并在 Windows+Cygwin+bison+flex+gcc 的环境下编译运行。(1) 编辑 Flex 语法文件。/*
18、 name: example.flex */int num_lines = 0, num_chars = 0;%n +num_lines; +num_chars;. +num_chars;%int main()yylex();printf(“# of lines = %d, # of chars = %dn“, num_lines, num_chars);return 0;(2) 生成扫描器的 C 文件。$ flex example.flexThe output is lex.yy.c(3) 编译生成的 C 文件。编译时失败,出现了如下的问题:# gcc -g -Wall -lfl -o sc
19、an lex.yy.clex.yy.c:959: warning: yyunput defined but not used/cygdrive/c/DOCUME1/ADMINI1.78B/LOCALS1/Temp/ccHwCWNb.o: In function main:/cygdrive/c/home/sandbox/flex_exam_1/example.l:9: multiple definition of _main/usr/lib/gcc/i686-pc-cygwin/3.4.4/libfl.a(libmain.o):(.text+0x0): first defined here/c
20、ygdrive/c/DOCUME1/ADMINI1.78B/LOCALS1/Temp/ccHwCWNb.o: In function yylex:/cygdrive/c/home/sandbox/flex_exam_1/lex.yy.c:692: undefined reference to _yywrap/cygdrive/c/DOCUME1/ADMINI1.78B/LOCALS1/Temp/ccHwCWNb.o: In function input:/cygdrive/c/home/sandbox/flex_exam_1/lex.yy.c:1041: undefined reference
21、 to _yywrapcollect2: ld returned 1 exit status上述消息指出两个问题:(1)函数 yywrap 没有定义。(2)自定义函数 main 与连接库 fl 中的定义冲突了。第一个问题的解决办法是在第一段(定义段)中加上一个选项指令:%option noyywrap第二个问题的解决办法就是用 gcc 编译时不连接 fl 库,如下所示:# flex example.flex# lsexample.flex lex.yy.c# gcc -g -Wall -o scan lex.yy.clex.yy.c:977: warning: yyunput defined
22、but not used# lsexample.flex lex.yy.c scan.exe# ./scan.exe789234345# of lines = 2, # of chars = 11修改过的代码如下:%option noyywrap 规约到 DLDL = 读入下一个字符DL . = 规约到?但是这次的归约尝试失败了,因为没有任何符号的定义可以产生这种形式的字符串。也就是说,这种形式不能规约到任何符号。所以接着我们读入下一个字符 1。这次可以将数字 1 归约到 D 符号。接着再读入一个字符4。4 可以归约到 D,继续归约到 DL。这两次的读入和规约形成了 D Dl 这个序列,而这个
23、序列可以归约到 DL。DL . = 读入下一个字符 1DL . 1 = 1 归约到 DDL . D = 读入下一个字符 4DL . D 4 = 4 归约到 DDL . D D = 4 继续归约到 DLDL . D DL = D DL 归约到 DL察看文法我们可以很快地注意到,FN 能产生 DL . Dl 这种形式的序列,所以可以做一个归约。然后注意到 FN 可以从 S 符号产生,所以可以归约到 S,然后停止,整个分析结束。DL . DL = 归约到 FNFN = 规约到 SS = 分析结束可能你已经注意到,我们经常可以选择是否现在就做归约,还是等到读入更多的符号后再作不同的归约。移进-归约分析
24、算法有很多不同的变种,按照复杂度和能力递增的顺序是:LR(0), SLR, LALR 和 LR(1)。LR(1) 通常需要一个巨大的分析表,在实践上不具有实用性,因此 LALR 是最常使用的算法。SLR 和 LR(0)对于大部分的程序语言来说还不够强大。Bison 分析器的算法 1 Bison 适合上下文无关文法(Context-free grammar) ,并采用 LALR(1)算法Donnelly 06的文法。当 bison 读入一个终结符( token) ,它会将该终结符及其语意值一起压入堆栈。这个堆栈叫做分析器堆栈(parser stack) 。把一个 token 压入堆栈通常叫做移进
25、(shifting) 。例如,假设一个中缀计算器已经读入1 + 5 * ,下一个准备读入的是3,那么这个栈里就有四个元素,每个元素都是移进的一个终结符。但堆栈并不是每读入一个终结符就分配一个栈元素给它。当已经移进的后 n 个终结符和组(groupings)与一个文法规则相匹配时,它们会被根据那个规则结合起来。这叫做归约(reduction) 。栈中的那些终结符和组会被单个的组(grouping)替换。那个组的符号就是那个规则的结果。执行该规则的相应的动作(Action)也是归约处理的一部分,这个动作会计算这个组的语意值。例如,如果中缀计算器的分析器堆栈包含:1 + 5 * 3,并且下一个输入字
26、符是换行符,那么上述后 3 个元素可以按照下面规则归约到 15:expr: expr * expr;于是堆栈中就只包含下面三个元素了:1 + 15。此刻,另一个规约也可以执行,其结果是一个单值 16。然后这个新行终结符就可以被移进了。分析器通过移进和归约尝试着缩减整个输入到单个的组。这个组的符号就是文法中的起始符号(start-symbol) 。终结符预读Bison 分析器并不总是在后 n 个终结符与组匹配某一规则时立即就进行归约。这种策略对于大部分语言来说并不合适。相反,当可以进行归约时,分析器有时会“预读”(looks ahead)下一个终结符来决定做什么。当一个终结符被读进来后,并不会立
27、即移进堆栈,而是首先作为一个预读终结符(look-ahead token) 。此后,分析器开始对栈上的终结符和组执行一个或多个归约,而预读终结符仍然放在一边。当没有归约可做时,这个预读终结符才会被移进堆栈。这并不表示所有可能的归约都已经做了,这要取决于预读终结符的类型,一些规则可能选择推迟它们的使用。下面研究一个需要做预读的案例。这里的三条规则定义了一个表达式,可以包含二元的加法运算符和一元的后缀阶乘运算符(!),并且允许用括号进行分组。expr: term + expr| term;term: ( expr )| term !| NUMBER;假定终结符1 + 2已经读入并移进堆栈,那么接下
28、来应该做什么呢?如果接下来的终结符是),那么前三个终结符必须归约成一个 expr。这是唯一的合法情况,因为移进) 将会产生一个序列 term ),而没有任何规则允许出现这种情况。不做归约移进),堆栈上的元素序列是 1 + 2 ),2 可以归约成 NUMBER,进而归约成 term,与其后的 )形成term )的序列,检查所有规则发现没有任何规则定义了这种序列。如果下一个终结符是!记住此刻它还是预读终结符 ,那么该终结符必须立即移进堆栈以便2 !可以归约成一个 term。如果相反地分析器在移进这个阶乘符号之前进行归约,那么1 + 2就会归约成 expr。这将导致不可能移进!终结符,因为这样的话将
29、会产生一个expr !序列。同样没有任何规则定义了这种序列。预读终结符存储在变量 yychar 中。它的语意值和位置,如果有的话,存储在变量 yylval和 yylloc 中。移进-归约冲突假定我们正在分析一个语言,其中有 if-then 和 if-then-else 语句,对应的规则如下:if_stmt: IF expr THEN stmt| IF expr THEN stmt ELSE stmt;这里我们假设 IF,THEN 和 ELSE 是特别的关键字终结符。当 ELSE 终结符读入后作为一个预读终结符时,堆栈中的内容(假设输入是合法的)正好可以归约到第一条规则上。但是把它移进堆栈也是合
30、理的,因为那样根据第二条规则就会导致最后的归约。在这种情况下,移进或者归约都是合法的,称为移进-归约冲突(shift-reduce conflict) 。Bison 的设计是,用移进来解决冲突,除非有操作符优先级声明的指令。为了解释如此选择的理由,让我们与其它可选办法进行一个比较。既然分析器更倾向移进 ELSE,那么其结果是把 else 子句连接到最内层的 if 语句,从而使得下面两种输入是等价的:if x then if y then win (); else lose;if x then do; if y then win (); else lose; end;如果分析器选择归约而不是移进
31、,那么其结果将是把 else 子句连接到最外层的 if 语句,从而导致下面两个输入是等价的:if x then if y then win (); else lose;if x then do; if y then win (); end; else lose;冲突的存在是因为文法有二义性:简单的嵌套的 if 语句的任一种解析都是合理的。已有的惯例是这种二义性的解决是通过把 else 子句连接到最内层的 if 语句而获得的;Bison 是选择移进而不是归约来实现的。 (一种更清晰的做法是写出无二义性的文法,但对于这种情况来说是非常困难的。 )这种特殊的二义性首次出现在 Algol 60 的规范
32、中,被称作dangling else ambiguity。对于可预见的合法的移进-归约冲突,为避免 bison 发出的警告,可以使用 %expect n 声明。那么只要移进-规约冲突的数量为 n,就不会有警告产生。操作符优先级可能出现移进-归约冲突的其它地方还有算术表达式。此时移进就不总是更好的解决办法了。Bison 通过声明操作符的优先级来指定何时移进何时归约。何时需要优先级考虑下面的二义文法片断(其二义性体现在1 2 * 3可以用两种不同的方式进行分析):expr: expr - expr| expr * expr| expr = NE LE GE%left + -%left * /这里
33、NE 代表 not equal(不等于) ,LE 表示小于等于,GE 表示大于等于。优先级如何工作优先级声明的第一个效果就是赋予了终结符不同的优先级水平。第二个效果就是给某些规则赋予了优先级水平:每个规则从它的最后的终结符得到其优先级。当已读入的终结符和组符合某个规则时,理论上讲它可以进行归约。它最后的一个终结符可能被指定了优先级,这个优先级就成为该规则的优先级。最终,冲突的解决是通过比较规则的优先级与它的预读终结符的优先级实现的。若该终结符的优先级高,那么就采用移进。过规则的优先级较高,那么就选择归约。若它们具有相同的优先级,那么就基于该优先级的关联性来作出选择。选项-v可以让 Bison
34、产生详细的输出,其中有冲突是怎样解决的信息。并非所有的规则和终结符都具有优先级。若规则或预读终结符都没有优先级,那么缺省采用移进解决冲突 。与上下文相关的优先级经常有操作符的优先级依靠上下文。起初这听起来有些奇怪(outlandish) ,但这的确非常普通。例如,典型地一个减号作为一元操作符有非常高的优先级,而作为二元操作符则具有较低的优先级(比乘法低) 。对于给定的终结符,声明%left,%right 和%noassoc 只能使用一次,所以这种方式下一个终结符只有一个优先级。对于与上下文相关的优先级,需要一个新增的机制:用于规则的%prec 修饰符。%prec 修饰符声明了某个规则的优先级,
35、通过指定某个终结符而该终结符的优先级将用于该规则。没有必要在该规则出现这个终结符。就是说这个终结符可以是臆造的,在系统中可能并没有实际的对应体,只是为了用于指定该规则的优先级。下面是优先级的语法:%prec terminal-symbol 并且这个声明必须写在该规则的后面看下面的例子 。这个声明的效果就是把该终结符所具有的优先级赋予该规则,而这个优先级将会覆盖在普通方式下推断出来的该规则的优先级。这个更改过的规则优先级会影响规则如何解决冲突。下面就是解决一元的负号的问题。首先,定义一个名为 UMINUS 的虚构的终结符,并为之声明一个优先级。实际上并没有这种类型的终结符,但是这个终结符仅仅为其
36、的优先级服务。.%left + -%left *%left UMINUS现在 UMINUS 的优先级可如此地用于规则:exp: .| expr - exp.| - exp %prec UMINUS分析器的状态函数 yyparse 用一个有限状态机(finite-state)实现。压入分析器堆栈的值并不是简单地终结符类型码。它们代表靠近堆栈顶部的整个的终结符和非终结符的序列。当前状态收集关于前一个输入的所有信息,而这个输入与决定下一步作什么有关。每次预读入一个终结符后,分析器当前状态与预读终结符的类型一起,到表中查找。对应的表项可能是:移进这个预读终结符。这种情况下,它也会指定新的分析器状态,并
37、被压入到分析器栈的顶部。或者这个表项可能是:用规则 n 进行归约。这就意味着一定数量的终结符或组会被从堆栈顶部取走,并用一个组取代。换句话说,那些数量的状态被从堆栈弹出,一个新的状态被压栈。另外一个可能是:这个表项会告诉说,这个预读终结符对当前状态来说是错误的。这将导致开始一个错误处理。归约-归约冲突归约-归约冲突(reduce-reduce conflict)发生在有两个或以上的规则适用于同一个输入序列时。这通常表明了一个严重的文法错误。例如,这里有一个错误的尝试,试图定义一个具有 0 个或多个单词(word)的组:sequence: /* empty */ printf (“empty sequencen”); | maybeword| sequence word printf (“added word %sn”, $2); ;maybeword: /* empty */ printf (“empty maybewordn”); | word printf (“single word %sn”, $1); ;