BISONFlex.docx
- 文档编号:7065353
- 上传时间:2023-01-16
- 格式:DOCX
- 页数:31
- 大小:39.20KB
BISONFlex.docx
《BISONFlex.docx》由会员分享,可在线阅读,更多相关《BISONFlex.docx(31页珍藏版)》请在冰豆网上搜索。
BISONFlex
FLEX
什么是FLEX?
它是一个自动化工具,可以按照定义好的规则自动生成一个C函数yylex(),也成为扫描器(Scanner)。
这个C函数把文本串作为输入,按照定义好的规则分析文本串中的字符,找到符合规则的一些字符序列后,就执行在规则中定义好的动作(Action)。
例如在规则中可以这样定义:
如果遇到一个换行字符\n,那么就把行计数器的值加一。
Flex文件就是一个文本文件,内容包括定义好的一系列词法规则。
文件的命名习惯上以小写字母l(L)来作为文件后缀。
如果为了清晰,也可以用.flx或者.flex作为文件的后缀名。
Flex文件完成后,就执行下列命令:
$flexexample.flex
这个命令执行后将生成一个C文件,默认文件名为lex.yy.c。
这个C文件主要内容就是函数yylex()的定义。
如果要直接将这个文件编译成为一个可执行程序,还有一些要注意的地方。
如果在Flex文件中没有提供main()函数的定义,那么这个C文件中不会有main()函数。
此时单独编译这个C文件的时候,一定要加上-lfl的连接库参数;若提供了main()函数,就不必要提供这个连接库参数了。
连接库libfl提供了一个缺省的main函数。
缺省的main()函数中只是简单地调用yyflex()函数,而自己提供的main()函数则可以根据需要加入许多其他的处理代码。
Flex文件
词法规范定义文件给出了单词构成规则。
词法文件在习惯上用字母l(即L的小写)来作为后缀。
Flex文件由三个部分组成。
或者说三个段。
三个段之间用两个%%分隔。
定义段(definitions)
%%
规则段(rules)
%%
用户代码段(usercode)
定义段(definitionssection)
定义段包含着一些简单名字的定义(namedefinitions),旨在简化扫描器的规范。
定义名字的方法如下:
namedefinition
名字可以由字母或下划线开头,后跟零个或多个字母、数字、下划线、或短横线。
名字的定义则从其后的第一个非空白字符(non-white-space)开始直到行尾。
下面是一个例子,定义了一个名字DIGIT,其定义就是指一个数字,如下所示:
DIGIT[0-9]
当在后面引用这个名字时,用一对花括号({})括住该名字即可。
它会被展开成一对圆括号括住的该名字的定义,即:
{name}展开成(definition)
例如:
{DIGIT}+"."{DIGIT}*
就等价于:
([0-9])+"."([0-9])*
定义段中还可以加入启动条件(startconditions)的声明。
顾名思义,启动条件就如同C语言中的条件编译一样,根据指定的启动条件去激活一条规则,并用这条规则去匹配读入的字符。
关于启动条件,后面还有更详细的介绍。
规则段(rulessection)
规则由模式(pattern)和动作(action)两个部分组成。
模式就是一个正则表达式,FLEX加入了一些自己的扩展。
而动作一般就是一些C语句。
模式指出了一个单词是如何构成的,当分析出一个符合该规则的单词时,就执行相应的动作。
模式一定要位于一行的开头处,不能有缩进。
而动作的开头一定要与模式在同一行。
当动作是用一对花括号{}括起来时,可以将左花括号放在与规则相同的行,而其余部分则可以从下一行开始。
用户代码段(usercode)
所有用户代码都被原样拷贝到文件lex.yy.c中。
在这里可以定义一些辅助函数或代码,供扫描器yylex()调用,或者调用扫描器(一般来说就是main()了)。
这一部分是可有可无的。
如果没有的话,Flex文件中第二个%%是可以省略的。
在定义段或者规则段中,任何一行有缩进的文本或者包含在一对%{和%}之间的文本,都被原样拷贝到最后生成的C代码文件中(当然%{和%}会被移走)。
在书写时%{和%}都必须在一行的开始处,不能缩进。
在规则段中,第一条规则之前的任何未缩进的文本或者在%{和%}之间的文本,可以用来为扫描器声明一些本地变量和代码。
一旦进入扫描器的代码,这些代码就会被执行。
规则段内其他的缩进的文本或者%{和%}之间的文本还是被原样拷贝输出,但是他们的含义是尚未有明确定义,很可能引起编译时(compile-time)错误(这一特性是为了与POSIX兼容而提供的)。
在定义段中,没有缩进的注释也会被原样拷贝到最后生成的C代码文件中,例如以/*开始的一行注释,直到遇到*/,这中间的文本会被原样拷贝输出。
模式及其分类
模式采用正则表达式来书写。
正则表达式大致可以分为如下几类(从上到下,优先级依次递减):
(1)单字符匹配
*‘x’匹配字符x。
*‘.’匹配任意一个字符(字节),除了换行符。
*‘[xyz]’匹配单个字符,这个字符是方括号中给出的字符类(characterclass)中的一个。
*‘[abj-oZ]’匹配单个字符,这个字符是方括号中给出的字符类中的一个。
与上一方式的区别是指定字符类时用到了一个范围表示法:
j-o,这表示按照26个英文字母的顺序,从字母j开始一直到字母o共6个字母。
这里减号(-)表示范围。
如果减号本身也要作为一个匹配字符时,最好用转义字符(\)去除其特殊含义。
由于花括号({})在模式中用来引用名字,以及作为模式定义之后的动作(Action)定义块的首尾界定符,因此如果要在字符类中匹配花括号,必须用转义字符(\)去除其特殊含义。
下面这个例子定义了一个所有可打印字符的字符类:
[[:
alnum:
][:
blank:
]]\t+\-*/&!
_'?
@^`~$\\()%|.;[\]\{\}:
#<>=]
*‘[^A-Z]’匹配单个字符,这个字符必须是方括号中给定字符类以外的字符。
在方括号内开始处的特殊符号(^)表示否定。
当字符^不在字符类的开始处时,并不具有特殊含义,而是一个普通字符。
*‘[^A-Z\n]’匹配单个字符,这个字符不可以是方括号中给出的字符类中的字符。
与上一方式的不同在于,这里多了一个换行符,也就是说所匹配的字符不能是26个大写字母,也不能是换行符。
根据上面的描述,在表达字符分类时,除了直接用字符以及字符范围来表达外,还有一种叫做字符类表达式的,也有同样的作用,常见的一些表达式如下:
[:
alnum:
][:
alpha:
][:
blank:
][:
cntrl:
][:
digit:
][:
graph:
]
[:
lower:
][:
print:
][:
punct:
][:
space:
][:
upper:
][:
xdigit:
]
每一个表达式都指示了一个字符分类,而且其名称与标准C函数isXXXX的名字对应。
例如,[:
alnum:
]就指示了那些经由函数isalnum()检查后返回true的字符,也就是任何的字母或者数字。
注意,有些系统上没有给出C函数isblank()的定义,所以flex自己定义了[:
blank:
]为一个空格或者一个tab。
下面所举的几个例子,都是等价的:
[[:
alnum:
]]
[[:
alpha:
][:
digit:
]]
[[:
alpha:
]0-9]
[a-zA-Z0-9]
应该注意字符类表达式的写法。
一个字符类表达式是由一对[:
和:
]包住的,作为一个整体,在书写时不可与外层的[]混淆。
(2)重复模式的匹配
*‘r*’r是一个正则表达式,特殊字符`*'表示0个或多个。
因此这个模式表示匹配0个或多个r。
*‘r+’r是一个正则表达式,特殊字符`+'表示1个或多个。
因此这个模式表示匹配1个或多个r。
*‘r?
’r是一个正则表达式,特殊字符`?
'表示0个或1个。
因此这个模式表示匹配0个或1个r。
(从另一个角度看,就是说模式r是可选的)
*‘r{2,5}’r是一个正则表达式,{2,5}表示2个到5个。
因此这个模式表示匹配2个到5个r。
也就是说可以匹配`rr',`rrr',`rrrr',`rrrrr'四种重复的模式。
*‘r{2,}’r是一个正则表达式,{2,}省略了第二个数字,表示至少2个,不设上限。
因此这个模式表示匹配2个及以上个r。
也就是说至少可以匹配`rr',还可以匹配`rrr',`rrrr'等无限多种重复的模式。
*‘r{4}’r是一个正则表达式,{4}只有一个数字,表示4个。
因此这个模式确切地匹配4个r,即`rrrr'。
(3)名字替换
*‘{name}’这里name就是在前面的定义段给出的名字。
这个模式将用这个名字的定义来匹配。
(4)平凡(plain)文本串的匹配
*‘“[xyz]\″foo”’这个模式用来确切地匹配文本串:
[xyz]\″foo。
注意最外层的单引号所包含的是整个模式表达式,也就是说,当希望匹配字串[xyz]\″foo时,在书写规则时该字串必须用双引号括住。
(5)特殊单字符的匹配
*‘\x’当x是一个`a',`b',`f',`n',`r',`t'或`v'时,它就解释为ANSI-C中的\x。
否则就仍然作为一个普通字符x(一般用于诸如`*'字符的转义字符)。
*‘\0’匹配一个NUL字符(ASCII码值为0)。
*‘\123’匹配一个字符,其值用八进制表示为123。
*‘\x2a’匹配一个字符,其值用十六进制表示为2a。
(6)组合模式的匹配
*‘(r)’匹配规则表达式r,圆括号可以提高其优先级。
*‘rs’匹配规则表达式r,其后紧跟着表达式s。
这称为联接(concatenation)。
*‘r|s’或者匹配规则表达式r,或者匹配表达式s。
*‘r/s’匹配模式r,但是要求其后紧跟着模式s。
当需要判断本次匹配是否为“最长匹配(longestmatch)时,模式s匹配的文本也会被包括进来,但完成判断后开始执行对应的动作(action)之前,这些与模式s相配的文本会被返还给输入。
所以动作(action)只能看到模式r匹配到的文本。
这种模式类型叫做尾部上下文(trailingcontext)。
(有些‘r/s’组合是flex不能识别的;请参看后面deficiencies/bugs一节中的dangeroustrailingcontext的内容。
)
*‘^r’匹配模式r,但是这个模式只出现在一行的开始处。
也就是说,刚开始扫描时遇到的,或者说在刚扫描完一个换行字符后紧接着遇到的。
*‘r$’匹配模式r,但是这个模式只在一行的尾部。
也就是说,该模式就出现在换行之前。
这个模式等价于r/\n。
注意,flex中的换行(newline)的概念,就是C编译器中所使用的\n,flex也采用同样的符号和解释。
在DOS系统中,可能必须由你自己滤除输入中的\r,或者明确地在模式中写成r/\r\n来代替r$。
(在unix系统中换行是用一个字节\n表示的,而DOS/Windows则采用两个字节\r\n来表示换行。
)
(7)有启动条件(StartCondition)的模式匹配
*‘r’匹配模式r,但需要启动条件s(后面后关于启动条件的讨论)。
模式‘
(启动条件简单来说,类似于C语言中的条件编译,满足了某个条件才启动这个模式参与匹配,否则不会启动该模式参与匹配。
)
*‘<*>r’匹配模式r,在任何启动条件下都参与匹配,即使是排斥性的条件。
[上述还需要从实践中体会其含义]
(8)文件尾匹配
*‘<
一般说来,都应该在模式中加入文件尾模式。
这样可以有机会在文件扫描完成时增加一些额外的处理。
*‘
一些常见规则的编写(待续)
(1)双引号字符串。
[\"]({SAFECHAR}|{RESTCHAR}|[_])*[\"]
这里需要注意的地方是中间的重复模式的写法:
(r)*。
r可以是一个组合模式。
中间的两个名称SAFECHAR和RESTCHAR是在定义段给出的两个字符类。
[此处应在实用中不断添加]
=========================================
创建一个简单的扫描器
下列例子来自于Flex的手册。
并在Windows+Cygwin+bison+flex+gcc的环境下编译运行。
(1) 编辑Flex语法文件。
/*name:
example.flex*/
intnum_lines=0,num_chars=0;
%%
\n++num_lines;++num_chars;
.++num_chars;
%%
intmain()
{
yylex();
printf("#oflines=%d,#ofchars=%d\n",num_lines,num_chars);
return0;
}
(2) 生成扫描器的C文件。
$flexexample.flex
Theoutputislex.yy.c
(3) 编译生成的C文件。
编译时失败,出现了如下的问题:
#gcc-g-Wall-lfl-oscanlex.yy.c
lex.yy.c:
959:
warning:
'yyunput'definedbutnotused
/cygdrive/c/DOCUME~1/ADMINI~1.78B/LOCALS~1/Temp/ccHwCWNb.o:
Infunction`main':
/cygdrive/c/home/sandbox/flex_exam_1/example.l:
9:
multipledefinitionof`_main'
/usr/lib/gcc/i686-pc-cygwin/3.4.4/../../../libfl.a(libmain.o):
(.text+0x0):
firstdefinedhere
/cygdrive/c/DOCUME~1/ADMINI~1.78B/LOCALS~1/Temp/ccHwCWNb.o:
Infunction`yylex':
/cygdrive/c/home/sandbox/flex_exam_1/lex.yy.c:
692:
undefinedreferenceto`_yywrap'
/cygdrive/c/DOCUME~1/ADMINI~1.78B/LOCALS~1/Temp/ccHwCWNb.o:
Infunction`input':
/cygdrive/c/home/sandbox/flex_exam_1/lex.yy.c:
1041:
undefinedreferenceto`_yywrap'
collect2:
ldreturned1exitstatus
上述消息指出两个问题:
(1)函数yywrap没有定义。
(2)自定义函数main与连接库fl中的定义冲突了。
第一个问题的解决办法是在第一段(定义段)中加上一个选项指令:
%optionnoyywrap
第二个问题的解决办法就是用gcc编译时不连接fl库,如下所示:
#flexexample.flex
#ls
example.flexlex.yy.c
#gcc-g-Wall-oscanlex.yy.c
lex.yy.c:
977:
warning:
'yyunput'definedbutnotused
#ls
example.flexlex.yy.cscan.exe
#./scan.exe
789
234
345#oflines=2,#ofchars=11
修改过的代码如下:
%optionnoyywrap<====防止出现yywrap的问题
%{
intnum_lines=0,num_chars=0;
%}
%%
\n++num_lines;++num_chars;
.++num_chars;
%%
intmain()
{
yylex();
printf("#oflines=%d,#ofchars=%d\n",
num_lines,num_chars);
return0;
}
更改扫描器yylex()的名字
我们还可以更改Flex自动生成的词法分析函数yylex()的名字、参数以及返回值,也就是说yylex这个名字仅仅是一个默认的名称,是可以改成其他名称的。
方法很简单,只需要对宏YY_DECL做一个重定义即可:
#defineYY_DECLfloatlexscan(floata,floatb)
上述的宏定义就表明:
当运行Flex生成C代码时,词法分析函数的名字叫做lexscan(不再是yylex了),有两个浮点型参数a和b,函数的返回值是浮点型。
如果与Bison联用的话,还是不要更改的好,因为Bison要求词法分析函数的名称是yylex。
[应该也是可以改的,但其实际的方法还需在实践中得来。
]
词法分析函数yylex()会使用全局变量yyin读取字符。
一些思考
(1)在H248协议的BNF文本中,需要分析很多的数字,有十六进制的,有十进制的,有长的数字也有短的数字。
虽然在H248协议看来,各种不同的数字有着不同的意义,但是在Flex词法扫描器看来,它们有什么不同呢?
特别是同样的一个0xab这样的只有两位数字的十六进制数,在H248协议和BISON看来,其有不同的含义和类型,但是在Flex看来却没有什么不同。
假设Bison分别将其定义为Token_A和Token_B,那么当Flex分析出这么一个单词时,返回给Bison的数字类型是A还是B?
(2)在H248协议中,有一种表达式是由多个参数组成的,其中每个参数至多出现一次,且参数间次序是任意的。
此外其中有两个参数是必须的。
这种情况下如何给出Bison文法规则定义呢?
文法分析概览
利用BNF写出的文法规则,可以用来对输入的文本进行文法分析。
一条BNF文法规则,左边是一个非终结符(Symbol或者non-terminal),右边则定义该非终结符是如何构成的,也称为产生式(Production),产生式中可能包含非终结符,也可能包含终结符(terminal),也可能二者都有。
在所有文法规则中,必有一个开始的规则,该规则左边的部分叫做开始符号(startsymbol)。
一个规则的写法如下:
Symbol:
=Production
下面是一个BNF文法定义的例子。
FN是fractionalnumber的意思,DL是digitlist的意思,S是startsymbol。
S:
='-'FN|FN
FN:
=DL|DL'.'DL
DL:
=D|DDL
D:
='0'|'1'|'2'|'3'|'4'|'5'|'6'|'7'|'8'|'9'
一个非终结符可能有多个产生式,相互间用竖线(|)隔开。
每一条BNF产生式,都有自己的启动集(startset)。
启动集里的元素就是每个Production中的第一个部分,比如上例S规则的启动集就是{'-'}以及{FN}。
利用BNF文法来分析目标文本,其分析方法比较流行的有几种,下面作一概述[Garshol03]。
LL(k)分析
LL分析又称为自顶向下的分析(top-downparsing),也有叫递归下降分析(recursive-descentparsing)。
也是最简单的一种分析方式。
它工作的方式类似于找出一个产生式可以从哪一个终结符开始。
当分析时,从起始符号开始,比较输入中的第一个终结符和启动集,看哪一个产生式规则被使用了。
当然,两个启动集之间不能拥有同一个终结符。
如果有的话,就没有办法决定选择哪个产生式规则了。
Ll文法通常用数字来分类,比如LL
(1),LL(0)等。
这个数字告诉你,在一个文法规则中的任何点可以允许一次察看的终结符的最大数量。
LL(0)就不需要看任何终结符,分析器总是可以选择正确的产生式规则。
它只适用于所有的非终结符都只有一个产生规则。
只有一个产生规则意味着只有一个字符串。
[不用看当前的终结符是什么就可以决定是哪一个产生规则,说明这个规则是为一个固定的字符串所写的。
]这种文法是没有什么意义的。
最常见也是比较有用的事LL
(1)文法。
它只需要看一个终结符,然后就可以决定使用哪一个产生规则。
而LL
(2)则可以查看两个终结符,还有LL(k)文法等等。
对于某个固定的k值,也存在着根本不是LL(k)的文法,而且还很普遍。
下面来分析一下本章开头给出的例子。
首先看下面这条规则:
D:
='0'|'1'|'2'|'3'|'4'|'5'|'6'|'7'|'8'|'9'
上述规则有十个产生式,每个产生式的启动集是一个数字终结符构成的集合{'0'}、{'1'}、……、{'9'}。
这是一个很好的LL
(1)文法,因为我们只要看一个终结符,就可以选择一个正确的产生式。
例如,如果看到一个终结符,其内容是3,那么就采用上面第四个产生式,即D:
='3'。
接下来分析DL规则。
DL:
=D|DDL
上述规则有两个产生式,启动集是{D},{D}。
很不幸,两个产生式的启动集相同。
这就表示只看第一个输入中的第一个终结符不能选择正确的产生式。
然而可以通过欺骗来绕过这个问题:
如果输入中第二个终结符不是一个数字,那么就选择第一个产生式,但如果两者都是数字就必须选择第二个产生式。
换句话说,这意味着这是一条好的LL
(2)文法规则。
实际上这里有些东西被简化了。
再分析下FN规则吧。
它的情况更糟糕。
FN:
=DL|DL'.'DL
它有两条产生式,而且启动集相同,均为{DL}。
然而这次不像DL规则那么幸运了。
咋一看,似乎通过LL
(2)可以分辨应该使用哪一个产生式。
但是很不幸,我们无法确定在读到终结符('.')之前,需要读多少个数字才算是DL符号的最后一个数字。
[想想吧,分析器这么工作着:
读入第一个终结符,一看是相同的DL符号,那么就读第二个终结符吧;读入第二个终结符,两者合起来一看,还是一样的DL符号;读入第三个终结符,前三个终结符合起来看,仍然是相同的DL符号。
但是DL符号表指示数字表示没有长度限制的。
]没有任何一个给定的k值,这都不符合LL(k)文法,因为数字表总能突破这个k的长度。
最后看看启动符号规则。
有点意外,它产生规则的选择很简单。
S:
='-'FN|FN
它有两个产生规则,两者的启动集是{'-'}和{FN}。
因此,如果输入中第一个终结符是'-',那么就选择第一个产生式,否则选择第二个产生式。
所以这是一个LL
(1)文法。
从上述的LL分析看,只有FN和DL规则引起了问题。
但是不必绝望。
大部分的非LL(k)文法都可以容易地转换为LL
(1)文法。
下面以当前的这个例子来看看如何转换有问题的FN和DL。
对于FN符号来说,它的两个产生式都开始于DL,但是第二个产生式其后续的是一个小数点终结符('.'),以及另外一个数字表。
那么这很容易解决:
可以将FN改变为一个产生式,其以DL开始,后跟一个FP(fractionalpart)符号。
而FP符号则定义成或者为空,或者为小数点后跟着一个数字表,如下所示:
FN:
=DLFP
FP:
=@|'.'DL
上述@符号表示为空。
现在FN文法没有任何问题了,因为它现在只有一个产生式。
而FP也不会有问题,因为它的两个产生式的启动集是不同的:
前者是输入的尾端,后者是小数点终结符。
DL符
- 配套讲稿:
如PPT文件的首页显示word图标,表示该PPT已包含配套word讲稿。双击word图标可打开word文档。
- 特殊限制:
部分文档作品中含有的国旗、国徽等图片,仅作为作品整体效果示例展示,禁止商用。设计者仅对作品中独创性部分享有著作权。
- 关 键 词:
- BISONFlex